diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 24d2fb3a09..1857d27caa 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -180,7 +180,7 @@ export function blockToNode( /** * Converts an internal (prosemirror) content node to a BlockNote InlineContent array. */ -function contentNodeToInlineContent(contentNode: Node) { +export function contentNodeToInlineContent(contentNode: Node) { const content: InlineContent[] = []; let currentContent: InlineContent | undefined = undefined; diff --git a/packages/core/src/api/serialization/clipboardHandlerExtension.ts b/packages/core/src/api/serialization/clipboardHandlerExtension.ts index ef19fef292..8f1545ef0e 100644 --- a/packages/core/src/api/serialization/clipboardHandlerExtension.ts +++ b/packages/core/src/api/serialization/clipboardHandlerExtension.ts @@ -2,9 +2,12 @@ import { BlockSchema } from "../../extensions/Blocks/api/blockTypes"; import { BlockNoteEditor } from "../../BlockNoteEditor"; import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; +import { Fragment, Slice } from "prosemirror-model"; import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer"; import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; import { markdown } from "../formatConversions/formatConversions"; +import { blockToNode } from "../nodeConversions/nodeConversions"; +import { parseExternalHTML } from "./html/parseExternalHTML"; const acceptedMIMETypes = [ "blocknote/html", @@ -73,9 +76,24 @@ export const createClipboardHandlerExtension = ( } if (format !== null) { - editor._tiptapEditor.view.pasteHTML( - event.clipboardData!.getData(format!) - ); + // Use custom parser if only text/html is available (i.e. when + // pasting content from external source) + if (format === "text/html") { + const html = event.clipboardData!.getData(format); + const parsedNodes = parseExternalHTML(html, editor).map( + (block) => blockToNode(block, schema) + ); + + editor._tiptapEditor.view.dispatch( + editor._tiptapEditor.view.state.tr.replaceSelection( + new Slice(Fragment.from(parsedNodes), 0, 0) + ) + ); + } else { + editor._tiptapEditor.view.pasteHTML( + event.clipboardData!.getData(format!) + ); + } } return true; diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/basic/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/nested/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/customParagraph/styled/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/customParagraph/styled/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/image/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/basic/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/image/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/image/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/basic/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/image/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/button/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/image/button/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/button/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/image/button/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/button/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/image/button/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/button/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/image/button/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/nested/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/image/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/nested/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/image/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/image/nested/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/image/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/image/nested/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/image/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/basic/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/nested/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/paragraph/styled/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/paragraph/styled/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/basic/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/nested/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/nested/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/styled/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/styled/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/styled/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleCustomParagraph/styled/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleCustomParagraph/styled/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/basic/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/basic/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/basic/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/basic/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/basic/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/button/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/button/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/button/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/button/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/button/internal.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/external.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/nested/external.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/external.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/nested/external.html diff --git a/packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/internal.html b/packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/nested/internal.html similarity index 100% rename from packages/core/src/api/serialization/html/__snapshots__/simpleImage/nested/internal.html rename to packages/core/src/api/serialization/html/__snapshots__/copy/simpleImage/nested/internal.html diff --git a/packages/core/src/api/serialization/html/htmlConversion.test.ts b/packages/core/src/api/serialization/html/htmlConversion.test.ts index 52a0833fa3..3853c965cf 100644 --- a/packages/core/src/api/serialization/html/htmlConversion.test.ts +++ b/packages/core/src/api/serialization/html/htmlConversion.test.ts @@ -15,6 +15,7 @@ import { uploadToTmpFilesDotOrg_DEV_ONLY } from "../../../extensions/Blocks/node import { createInternalHTMLSerializer } from "./internalHTMLSerializer"; import { createExternalHTMLExporter } from "./externalHTMLExporter"; import { defaultProps } from "../../../extensions/Blocks/api/defaultProps"; +import { parseExternalHTML } from "./parseExternalHTML"; // This is a modified version of the default image block that does not implement // a `serialize` function. It's used to test if the custom serializer by default @@ -99,7 +100,7 @@ function convertToHTMLAndCompareSnapshots( const serializer = createInternalHTMLSerializer(tt.schema, editor); const internalHTML = serializer.serializeBlocks(blocks); const internalHTMLSnapshotPath = - "./__snapshots__/" + + "./__snapshots__/copy/" + snapshotDirectory + "/" + snapshotName + @@ -109,7 +110,7 @@ function convertToHTMLAndCompareSnapshots( const exporter = createExternalHTMLExporter(tt.schema, editor); const externalHTML = exporter.exportBlocks(blocks); const externalHTMLSnapshotPath = - "./__snapshots__/" + + "./__snapshots__/copy/" + snapshotDirectory + "/" + snapshotName + @@ -454,3 +455,196 @@ describe("Convert custom blocks with non-exported inline content to HTML", () => convertToHTMLAndCompareSnapshots(blocks, "simpleCustomParagraph", "nested"); }); }); + +function parseHTMLAndCompareSnapshots(html: string, snapshotName: string) { + const blocks = parseExternalHTML(html, editor); + + const snapshotPath = "./__snapshots__/paste/" + snapshotName + ".json"; + expect(JSON.stringify(blocks)).toMatchFileSnapshot(snapshotPath); +} + +describe("Parse HTML", () => { + it("Parse basic block types", async () => { + const html = `

Heading 1

+

Heading 2

+

Heading 3

+

Paragraph

+
Image Caption
+

None Bold Italic Underline Strikethrough All

`; + + parseHTMLAndCompareSnapshots(html, "parse-basic-block-types"); + }); + + it("Parse nested lists", async () => { + const html = ` +
    +
  1. + Numbered List Item +
      +
    1. + Nested Numbered List Item +
    2. +
    3. + Nested Numbered List Item +
    4. +
    +
  2. +
  3. + Numbered List Item +
  4. +
`; + + parseHTMLAndCompareSnapshots(html, "parse-nested-lists"); + }); + + it("Parse nested lists with paragraphs", async () => { + const html = ` +
    +
  1. +

    Numbered List Item

    +
      +
    1. +

      Nested Numbered List Item

      +
    2. +
    3. +

      Nested Numbered List Item

      +
    4. +
    +
  2. +
  3. +

    Numbered List Item

    +
  4. +
`; + + parseHTMLAndCompareSnapshots(html, "parse-nested-lists-with-paragraphs"); + }); + + it("Parse mixed nested lists", async () => { + const html = ` +
    +
  1. + Numbered List Item +
      +
    • +

      Nested Bullet List Item

      +
    • +
    • +

      Nested Bullet List Item

      +
    • +
    +
  2. +
  3. + Numbered List Item +
  4. +
`; + + parseHTMLAndCompareSnapshots(html, "parse-mixed-nested-lists"); + }); + + it("Parse divs", async () => { + const html = `
Single Div
+
+ Div +
Nested Div
+
Nested Div
+
+
Single Div
+
+
Nested Div
+
Nested Div
+
`; + + parseHTMLAndCompareSnapshots(html, "parse-divs"); + }); + + it("Parse fake image caption", async () => { + const html = `
+ +

Image Caption

+
`; + + parseHTMLAndCompareSnapshots(html, "parse-fake-image-caption"); + }); + + it("Parse deep nested content", async () => { + const html = `
+ Outer 1 Div Before +
+ Outer 2 Div Before +
+ Outer 3 Div Before +
+ Outer 4 Div Before +

Heading 1

+

Heading 2

+

Heading 3

+

Paragraph

+
Image Caption
+

Bold Italic Underline Strikethrough All

+ Outer 4 Div After +
+ Outer 3 Div After +
+ Outer 2 Div After +
+ Outer 1 Div After +
`; + + parseHTMLAndCompareSnapshots(html, "parse-deep-nested-content"); + }); + + it("Parse div with inline content and nested blocks", async () => { + const html = `
+ None Bold Italic Underline Strikethrough All +
Nested Div
+

Nested Paragraph

+
`; + + parseHTMLAndCompareSnapshots(html, "parse-div-with-inline-content"); + }); +}); diff --git a/packages/core/src/api/serialization/html/parseExternalHTML.ts b/packages/core/src/api/serialization/html/parseExternalHTML.ts new file mode 100644 index 0000000000..76f1ad37ec --- /dev/null +++ b/packages/core/src/api/serialization/html/parseExternalHTML.ts @@ -0,0 +1,131 @@ +import { InlineContent } from "../../../extensions/Blocks/api/inlineContentTypes"; +import { + BlockSchema, + SpecificPartialBlock, +} from "../../../extensions/Blocks/api/blockTypes"; +import { DOMParser, Schema } from "prosemirror-model"; +import { contentNodeToInlineContent } from "../../nodeConversions/nodeConversions"; +import { BlockNoteEditor } from "../../../BlockNoteEditor"; + +const blockRegex = + /^(address|blockquote|body|center|dir|div|dl|fieldset|form|h[1-6]|hr|isindex|menu|noframes|noscript|ol|p|pre|table|ul|dd|dt|frameset|li|tbody|td|tfoot|th|thead|tr|html)$/i; + +function isBlockLevel(element: Element) { + return blockRegex.test(element.nodeName); +} + +// Current problem: Even if a parse function hits, we still need to parse +// potentially nested blocks. But some of the element's children should be +// parsed as inline content, and some should be parsed as blocks. We need to +// somehow separate what should be parsed as inline content and what should be +// parsed as blocks. +// +// We could say that we just don't parse children if a parse function hits and +// leave the responsibility getting child blocks to the parse function. But this +// causes issues with e.g. a numbered list in a bullet list, as the numbered +// list item block has to know about the bullet list block in the schema. This +// also means code for parsing the 2 list types will basically be duplicated +// across their respective blocks. And in addition to that, we still have the +// issue of making sure that elements that should be blocks are not parsed as +// inline content. +export function parseExternalHTML( + html: string, + editor: BlockNoteEditor +) { + const element = document.createElement("div"); + element.innerHTML = html; + const parseFunctions = Object.fromEntries( + Object.entries(editor.schema).map(([blockType, blockSpec]) => [ + blockType, + blockSpec.fromExternalHTML, + ]) + ); + const parser = DOMParser.fromSchema(editor._tiptapEditor.schema); + const schema = editor._tiptapEditor.schema; + + return parseExternalHTMLHelper(element, parseFunctions, parser, schema); +} + +function parseExternalHTMLHelper( + element: HTMLElement, + parseFunctions: Record< + string, + ( + element: HTMLElement, + getInlineContent: (element: HTMLElement) => InlineContent[] + ) => SpecificPartialBlock | undefined + >, + parser: DOMParser, + schema: Schema +): SpecificPartialBlock[] { + // Function to parse an element's children as inline content. This is used by + // the parse functions, i.e. it is the responsibility of the parse functions + // to decide what should be parsed as inline content and what should be parsed + // as blocks. + function getInlineContent(element: HTMLElement) { + const inlineChildElements: (Element | Node)[] = []; + let numNonElementNodes = 0; + // We need to convert both text and inline elements to inline content. + for (let i = 0; i < element.childNodes.length; i++) { + const node = element.childNodes.item(i); + + if (node.nodeType !== Node.ELEMENT_NODE) { + numNonElementNodes++; + } + + if (node.nodeType === Node.TEXT_NODE) { + inlineChildElements.push(node); + } else { + const e = element.children.item(i - numNonElementNodes)!; + + if (!isBlockLevel(e)) { + inlineChildElements.push(e); + } + } + } + + const parent = document.createElement("div"); + parent.append(...inlineChildElements); + + const content: InlineContent[] = []; + content.push( + ...contentNodeToInlineContent( + parser.parse(parent, { + // TODO: While the type of node used doesn't matter much, just needs + // to have `content: "inline*"`, it shouldn't be hardcoded to + // "paragraph" either. + topNode: schema.nodes["paragraph"].create(), + }) + ) + ); + return content; + } + + const block = Object.values(parseFunctions) + .map((parseFunction) => parseFunction(element, getInlineContent)) + .find((parsed) => parsed !== undefined); + + if (block !== undefined) { + return [block]; + } + + // Currently only parses child elements if none of the parse functions hit on + // the element itself. This leaves the responsibility of parsing child + // elements to the parse functions. However, this is incorrect as that means + // that e.g. a numbered list in a bullet list will not be parsed correctly as + // the parsed blocks are of different types. + const blocks: SpecificPartialBlock[] = []; + + for (let i = 0; i < element.childElementCount; i++) { + blocks.push( + ...parseExternalHTMLHelper( + element.children.item(i) as HTMLElement, + parseFunctions, + parser, + schema + ) + ); + } + + return blocks; +} diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index f576928b0a..60c8d739ac 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -1,13 +1,14 @@ import { Attribute, Attributes, Editor, Node } from "@tiptap/core"; -import { ParseRule } from "prosemirror-model"; import { BlockConfig, BlockNoteDOMAttributes, + BlockSchema, BlockSchemaWithBlock, BlockSpec, Props, PropSchema, SpecificBlock, + SpecificPartialBlock, TipTapNode, TipTapNodeConfig, } from "./blockTypes"; @@ -102,45 +103,11 @@ export function parse< "render" | "toExternalHTML" > ) { - const rules: ParseRule[] = [ + return [ { tag: "div[data-content-type=" + blockConfig.type + "]", }, ]; - - // if (blockConfig.parse) { - // rules.push({ - // tag: "*", - // getAttrs(node: string | HTMLElement) { - // if (typeof node === "string") { - // return false; - // } - // - // const block = blockConfig.parse?.(node); - // - // if (block === undefined) { - // return false; - // } - // - // return block.props || {}; - // }, - // getContent(node, schema) { - // const block = blockConfig.parse?.(node as HTMLElement); - // - // if (block !== undefined && block.content !== undefined) { - // return Fragment.from( - // typeof block.content === "string" - // ? schema.text(block.content) - // : inlineContentToNodes(block.content, schema) - // ); - // } - // - // return Fragment.empty; - // }, - // }); - // } - - return rules; } // Used to figure out which block should be rendered. This block is then used to @@ -337,6 +304,10 @@ export function createBlockSpec< blockContentDOMAttributes ); }, + fromExternalHTML: + (blockConfig.fromExternalHTML as ( + element: HTMLElement + ) => SpecificPartialBlock) || (() => undefined), }; } diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index ecd0cca0a0..7fe2eda6fd 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -158,9 +158,9 @@ export type BlockConfig< dom: HTMLElement; contentDOM?: HTMLElement; }; - // parse?: ( - // element: HTMLElement - // ) => SpecificPartialBlock | undefined; + fromExternalHTML?: ( + element: HTMLElement + ) => SpecificPartialBlock | undefined; }; // Defines a single block spec, which includes the props that the block has and @@ -195,6 +195,10 @@ export type BlockSpec< dom: HTMLElement; contentDOM?: HTMLElement; }; + fromExternalHTML: ( + element: HTMLElement, + getInlineContent: (element: HTMLElement) => InlineContent[] + ) => SpecificPartialBlock | undefined; }; // Utility type. For a given object block schema, ensures that the key of each diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts index 96edffb2c5..70dbeb17c6 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts @@ -82,19 +82,16 @@ const HeadingBlockContent = createTipTapBlock<"heading", true>({ parseHTML() { return [ { - tag: "h1", - attrs: { level: 1 }, - node: "heading", - }, - { - tag: "h2", - attrs: { level: 2 }, - node: "heading", - }, - { - tag: "h3", - attrs: { level: 3 }, - node: "heading", + tag: "div[data-content-type=" + this.name + "]", + getAttrs: (element) => { + if (typeof element === "string") { + return false; + } + + return { + level: element.getAttribute("data-level"), + }; + }, }, ]; }, @@ -117,4 +114,19 @@ export const Heading = { propSchema: headingPropSchema, toInternalHTML: defaultBlockToHTML, toExternalHTML: defaultBlockToHTML, + fromExternalHTML: (element, getInlineContent) => { + for (let level = 1; level <= 3; level++) { + if (element.tagName === `H${level}`) { + return { + type: "heading", + props: { + level: level, + } as any, + content: getInlineContent(element) as any, + }; + } + } + + return; + }, } satisfies BlockSpec<"heading", typeof headingPropSchema, true>; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts index 66c44c2300..a447e99323 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts @@ -361,16 +361,16 @@ export const Image = createBlockSpec({ dom: figure, }; }, - // parse: (element) => { - // if (element.tagName === "IMG") { - // return { - // type: "image", - // props: { - // url: element.getAttribute("src") || "", - // }, - // }; - // } - // - // return; - // }, + fromExternalHTML: (element) => { + if (element.tagName === "IMG") { + return { + type: "image", + props: { + url: element.getAttribute("src") || "", + }, + }; + } + + return; + }, }); diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts index 8c618cf8ab..13855d54c4 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts @@ -47,50 +47,8 @@ const BulletListItemBlockContent = createTipTapBlock<"bulletListItem", true>({ parseHTML() { return [ - // Case for regular HTML list structure. { - tag: "li", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.tagName === "UL") { - return {}; - } - - return false; - }, - node: "bulletListItem", - }, - // Case for BlockNote list structure. - { - tag: "p", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.getAttribute("data-content-type") === "bulletListItem") { - return {}; - } - - return false; - }, - priority: 300, - node: "bulletListItem", + tag: "div[data-content-type=" + this.name + "]", }, ]; }, @@ -116,4 +74,20 @@ export const BulletListItem = { propSchema: bulletListItemPropSchema, toInternalHTML: defaultBlockToHTML, toExternalHTML: defaultBlockToHTML, + fromExternalHTML: (element, getInlineContent) => { + const parent = element.parentElement; + + if (parent === null) { + return undefined; + } + + if (parent.tagName === "UL" && element.tagName === "LI") { + return { + type: "bulletListItem", + content: getInlineContent(element) as any, + }; + } + + return undefined; + }, } satisfies BlockSpec<"bulletListItem", typeof bulletListItemPropSchema, true>; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts index c0c6c25ec3..e85915b816 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts @@ -69,52 +69,8 @@ const NumberedListItemBlockContent = createTipTapBlock< parseHTML() { return [ - // Case for regular HTML list structure. - // (e.g.: when pasting from other apps) { - tag: "li", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.tagName === "OL") { - return {}; - } - - return false; - }, - node: "numberedListItem", - }, - // Case for BlockNote list structure. - // (e.g.: when pasting from blocknote) - { - tag: "p", - getAttrs: (element) => { - if (typeof element === "string") { - return false; - } - - const parent = element.parentElement; - - if (parent === null) { - return false; - } - - if (parent.getAttribute("data-content-type") === "numberedListItem") { - return {}; - } - - return false; - }, - priority: 300, - node: "numberedListItem", + tag: "div[data-content-type=" + this.name + "]", }, ]; }, @@ -140,6 +96,22 @@ export const NumberedListItem = { propSchema: numberedListItemPropSchema, toInternalHTML: defaultBlockToHTML, toExternalHTML: defaultBlockToHTML, + fromExternalHTML: (element, getInlineContent) => { + const parent = element.parentElement; + + if (parent === null) { + return undefined; + } + + if (parent.tagName === "OL" && element.tagName === "LI") { + return { + type: "numberedListItem", + content: getInlineContent(element) as any, + }; + } + + return undefined; + }, } satisfies BlockSpec< "numberedListItem", typeof numberedListItemPropSchema, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts index dbacc61e63..c22a5bd110 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts @@ -17,9 +17,7 @@ export const ParagraphBlockContent = createTipTapBlock<"paragraph", true>({ parseHTML() { return [ { - tag: "p", - priority: 200, - node: "paragraph", + tag: "div[data-content-type=" + this.name + "]", }, ]; }, @@ -42,4 +40,11 @@ export const Paragraph = { propSchema: paragraphPropSchema, toInternalHTML: defaultBlockToHTML, toExternalHTML: defaultBlockToHTML, + fromExternalHTML: (element, getInlineContent) => { + if (element.tagName === "P") { + return { type: "paragraph", content: getInlineContent(element) as any }; + } + + return; + }, } satisfies BlockSpec<"paragraph", typeof paragraphPropSchema, true>;