From f61aac286d24f4708944dc628ff9156bf71f6e11 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 24 Sep 2024 15:19:50 +0200 Subject: [PATCH 01/17] Added more tests for selections & refactored code --- .../selectionLeavesBlockChildren.html | 1 - .../selectionSpansBlocksChildren.html | 1 - .../external/childToParent.html | 1 + .../external/childrenToNextParent.html | 1 + .../childrenToNextParentsChildren.html | 1 + .../external/multipleChildren.html} | 0 .../external/multipleStyledText.html | 1 + .../external/nestedImage.html | 1 + .../external/styledText.html | 1 + .../external/tableAllCells.html | 1 + .../external/tableCell.html | 1 + .../external/tableCellText.html | 1 + .../external/tableRow.html | 1 + .../external/unstyledText.html | 1 + .../internal/childToParent.html | 1 + .../internal/childrenToNextParent.html | 1 + .../childrenToNextParentsChildren.html | 1 + .../internal/multipleChildren.html | 1 + .../internal/multipleStyledText.html | 1 + .../internal/nestedImage.html | 1 + .../internal/styledText.html | 1 + .../internal/tableAllCells.html | 1 + .../internal/tableCell.html | 1 + .../internal/tableCellText.html | 1 + .../internal/tableRow.html | 1 + .../internal/unstyledText.html | 1 + .../api/exporters/html/htmlConversion.test.ts | 392 ++++++++++++------ 27 files changed, 290 insertions(+), 127 deletions(-) delete mode 100644 packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html rename packages/core/src/api/exporters/html/{__snapshots_fragment_edge_cases__/selectionWithinBlockChildren.html => __snapshots_selection_html__/external/multipleChildren.html} (100%) create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html diff --git a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html b/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html deleted file mode 100644 index 5bad4edb4..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionLeavesBlockChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Paragraph 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html b/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html deleted file mode 100644 index 5a9420782..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionSpansBlocksChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Paragraph 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html new file mode 100644 index 000000000..a899fb60f --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html @@ -0,0 +1 @@ +

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html new file mode 100644 index 000000000..851b8973d --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html new file mode 100644 index 000000000..ba81a3a20 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionWithinBlockChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleChildren.html similarity index 100% rename from packages/core/src/api/exporters/html/__snapshots_fragment_edge_cases__/selectionWithinBlockChildren.html rename to packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleChildren.html diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html new file mode 100644 index 000000000..45a65883e --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html @@ -0,0 +1 @@ +

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html new file mode 100644 index 000000000..4e181d251 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html @@ -0,0 +1 @@ +

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html new file mode 100644 index 000000000..a8f4e4c2b --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html @@ -0,0 +1 @@ +

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html new file mode 100644 index 000000000..5d6537744 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html new file mode 100644 index 000000000..5d6537744 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html new file mode 100644 index 000000000..5d6537744 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html new file mode 100644 index 000000000..5d6537744 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html new file mode 100644 index 000000000..a5db078fd --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html @@ -0,0 +1 @@ +

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html new file mode 100644 index 000000000..e5edcc48a --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html @@ -0,0 +1 @@ +

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html new file mode 100644 index 000000000..6fa14919d --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html new file mode 100644 index 000000000..798ff745d --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html new file mode 100644 index 000000000..8ea4d8235 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html new file mode 100644 index 000000000..1bf8795a5 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html @@ -0,0 +1 @@ +

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html new file mode 100644 index 000000000..e69392705 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html @@ -0,0 +1 @@ +

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html new file mode 100644 index 000000000..7962432c6 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html @@ -0,0 +1 @@ +

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html new file mode 100644 index 000000000..82b7157b1 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html new file mode 100644 index 000000000..82b7157b1 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html new file mode 100644 index 000000000..82b7157b1 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html new file mode 100644 index 000000000..82b7157b1 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html new file mode 100644 index 000000000..d13d82566 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html @@ -0,0 +1 @@ +

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts index a08367f00..397f025c5 100644 --- a/packages/core/src/api/exporters/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -1,8 +1,24 @@ import { TextSelection } from "prosemirror-state"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vitest"; import { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; -import { addIdsToBlocks, partialBlocksToBlocksForTesting } from "../../.."; +import { + addIdsToBlocks, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + ExternalHTMLExporter, + InternalHTMLSerializer, + partialBlocksToBlocksForTesting, +} from "../../.."; import { PartialBlock } from "../../../blocks/defaultBlocks"; import { BlockSchema } from "../../../schema/blocks/types"; import { InlineContentSchema } from "../../../schema/inlineContent/types"; @@ -107,21 +123,155 @@ describe("Test HTML conversion", () => { } }); -// Fragments created from ProseMirror selections don't always conform to the -// schema. This is because ProseMirror preserves the full ancestry of selected -// nodes, but not the siblings of ancestor nodes. These tests are to verify that -// Fragments like this are exported to HTML properly, as they can't be created -// from Block objects like all the other test cases (Block object conversions -// always conform to the schema). -describe("Test ProseMirror fragment edge case conversion", () => { +// These tests are meant to test the copying of user selections in the editor. +// The test cases used for the other HTML conversion tests are not suitable here +// as they are represented in the BlockNote API, whereas here we want to test +// ProseMirror/TipTap selections directly. +describe("Test ProseMirror selection HTML conversion", () => { + const initialContent: PartialBlock[] = [ + { + type: "heading", + props: { + level: 2, + textColor: "red", + }, + content: "Heading 1", + children: [ + { + type: "paragraph", + content: "Nested Paragraph 1", + }, + { + type: "paragraph", + content: "Nested Paragraph 2", + }, + { + type: "paragraph", + content: "Nested Paragraph 3", + }, + ], + }, + { + type: "heading", + props: { + level: 2, + textColor: "red", + }, + content: "Heading 2", + children: [ + { + type: "paragraph", + content: "Nested Paragraph 1", + }, + { + type: "paragraph", + content: "Nested Paragraph 2", + }, + { + type: "paragraph", + content: "Nested Paragraph 3", + }, + ], + }, + { + type: "heading", + props: { + level: 2, + textColor: "red", + }, + content: [ + { + type: "text", + text: "Bold", + styles: { + bold: true, + }, + }, + { + type: "text", + text: "Italic", + styles: { + italic: true, + }, + }, + { + type: "text", + text: "Regular", + styles: {}, + }, + ], + children: [ + { + type: "image", + props: { + url: "https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg", + }, + children: [ + { + type: "paragraph", + content: "Nested Paragraph", + }, + ], + }, + ], + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell"], + }, + ], + }, + // Not needed as selections starting in table cells will get snapped to + // the table boundaries. + // children: [ + // { + // type: "table", + // content: { + // type: "tableContent", + // rows: [ + // { + // cells: ["Table Cell", "Table Cell"], + // }, + // { + // cells: ["Table Cell", "Table Cell"], + // }, + // ], + // }, + // }, + // ], + }, + ]; + let editor: BlockNoteEditor; + let serializer: InternalHTMLSerializer< + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema + >; + let exporter: ExternalHTMLExporter< + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema + >; const div = document.createElement("div"); - beforeEach(() => { - editor = BlockNoteEditor.create(); + + beforeAll(async () => { + editor = BlockNoteEditor.create({ initialContent }); editor.mount(div); + + await initializeESMDependencies(); + serializer = createInternalHTMLSerializer(editor.pmSchema, editor); + exporter = createExternalHTMLExporter(editor.pmSchema, editor); }); - afterEach(() => { + afterAll(() => { editor.mount(undefined); editor._tiptapEditor.destroy(); editor = undefined as any; @@ -129,122 +279,114 @@ describe("Test ProseMirror fragment edge case conversion", () => { delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; }); - // 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. In the schema, `blockContainer` nodes should - // contain a single `blockContent` node, so this edge case needs to be tested. - describe("No block content", () => { - const blocks: PartialBlock[] = [ - { - type: "paragraph", - content: "Paragraph 1", - children: [ - { - type: "paragraph", - content: "Nested Paragraph 1", - }, - { - type: "paragraph", - content: "Nested Paragraph 2", - }, - { - type: "paragraph", - content: "Nested Paragraph 3", - }, - ], - }, - { - type: "paragraph", - content: "Paragraph 2", - children: [ - { - type: "paragraph", - content: "Nested Paragraph 1", - }, - { - type: "paragraph", - content: "Nested Paragraph 2", - }, - { - type: "paragraph", - content: "Nested Paragraph 3", - }, - ], - }, - ]; + // Sets the editor selection to the given start and end positions, then + // exports the selected content to HTML and compares it to a snapshot. + function testSelection(testName: string, startPos: number, endPos: number) { + editor.dispatch( + editor._tiptapEditor.state.tr.setSelection( + TextSelection.create(editor._tiptapEditor.state.doc, startPos, endPos) + ) + ); - beforeEach(() => { - editor.replaceBlocks(editor.document, blocks); - }); + const copiedFragment = + editor._tiptapEditor.state.selection.content().content; - it("Selection within a block's children", async () => { - // Selection starts and ends within the first block's children. - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create(editor._tiptapEditor.state.doc, 18, 80) - ) - ); - - const copiedFragment = - editor._tiptapEditor.state.selection.content().content; - - await initializeESMDependencies(); - const exporter = createExternalHTMLExporter(editor.pmSchema, editor); - const externalHTML = exporter.exportProseMirrorFragment( - copiedFragment, - {} - ); - expect(externalHTML).toMatchFileSnapshot( - "./__snapshots_fragment_edge_cases__/" + - "selectionWithinBlockChildren.html" - ); - }); + const internalHTML = serializer.serializeProseMirrorFragment( + copiedFragment, + {} + ); + const externalHTML = exporter.exportProseMirrorFragment(copiedFragment, {}); - it("Selection leaves a block's children", async () => { - // Selection starts and ends within the first block's children and ends - // outside, at a shallower nesting level in the second block. - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create(editor._tiptapEditor.state.doc, 18, 97) - ) - ); - - const copiedFragment = - editor._tiptapEditor.state.selection.content().content; - - await initializeESMDependencies(); - const exporter = createExternalHTMLExporter(editor.pmSchema, editor); - const externalHTML = exporter.exportProseMirrorFragment( - copiedFragment, - {} - ); - expect(externalHTML).toMatchFileSnapshot( - "./__snapshots_fragment_edge_cases__/" + - "selectionLeavesBlockChildren.html" - ); - }); + expect(internalHTML).toMatchFileSnapshot( + `./__snapshots_selection_html__/internal/${testName}.html` + ); + expect(externalHTML).toMatchFileSnapshot( + `./__snapshots_selection_html__/external/${testName}.html` + ); + } - it("Selection spans multiple blocks' children", async () => { - // Selection starts and ends within the first block's children and ends - // within the second block's children. - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create(editor._tiptapEditor.state.doc, 18, 163) - ) - ); - - const copiedFragment = - editor._tiptapEditor.state.selection.content().content; - await initializeESMDependencies(); - const exporter = createExternalHTMLExporter(editor.pmSchema, editor); - const externalHTML = exporter.exportProseMirrorFragment( - copiedFragment, - {} - ); - expect(externalHTML).toMatchFileSnapshot( - "./__snapshots_fragment_edge_cases__/" + - "selectionSpansBlocksChildren.html" - ); + const testCases: { testName: string; startPos: number; endPos: number }[] = [ + // Selection spans all of first heading's children. + { + testName: "multipleChildren", + startPos: 16, + endPos: 78, + }, + // Selection spans from start of first heading to end of its first child. + { + testName: "childToParent", + startPos: 3, + endPos: 34, + }, + // Selection spans from start of first heading's first child to end of + // second heading's content (does not include second heading's children). + { + testName: "childrenToNextParent", + startPos: 16, + endPos: 93, + }, + // Selection spans from start of first heading's first child to end of + // second heading's last child. + { + testName: "childrenToNextParentsChildren", + startPos: 16, + endPos: 159, + }, + // Selection spans "Regular" text inside third heading. + { + testName: "unstyledText", + startPos: 175, + endPos: 182, + }, + // Selection spans "Italic" text inside third heading. + { + testName: "styledText", + startPos: 169, + endPos: 175, + }, + // Selection spans third heading's content (does not include third heading's + // children). + { + testName: "multipleStyledText", + startPos: 165, + endPos: 182, + }, + // Selection spans from start of third heading to end of it's last + // descendant. + { + testName: "nestedImage", + startPos: 165, + endPos: 205, + }, + // Selection spans text in first cell of the table. + { + testName: "tableCellText", + startPos: 216, + endPos: 226, + }, + // Selection spans first cell of the table. + { + testName: "tableCell", + startPos: 215, + endPos: 227, + }, + // Selection spans first row of the table. + { + testName: "tableRow", + startPos: 229, + endPos: 241, + }, + // Selection spans all cells of the table. + { + testName: "tableAllCells", + startPos: 259, + endPos: 271, + }, + ]; + + for (const testCase of testCases) { + it(testCase.testName, () => { + testSelection(testCase.testName, testCase.startPos, testCase.endPos); }); - }); + } }); From d1131736a043db8fa0b5159e18ad2d3950c6cf94 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 25 Sep 2024 10:46:48 +0200 Subject: [PATCH 02/17] Added test cases & block ID mocking --- .../external/image.html | 1 + .../external/partialChildToParent.html | 1 + .../internal/childToParent.html | 2 +- .../internal/childrenToNextParent.html | 2 +- .../internal/childrenToNextParentsChildren.html | 2 +- .../internal/image.html | 1 + .../internal/multipleChildren.html | 2 +- .../internal/multipleStyledText.html | 2 +- .../internal/nestedImage.html | 2 +- .../internal/partialChildToParent.html | 1 + .../internal/styledText.html | 2 +- .../internal/tableAllCells.html | 2 +- .../internal/tableCell.html | 2 +- .../internal/tableCellText.html | 2 +- .../internal/tableRow.html | 2 +- .../internal/unstyledText.html | 2 +- .../src/api/exporters/html/htmlConversion.test.ts | 15 +++++++++++++++ 17 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html new file mode 100644 index 000000000..1a8682186 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html @@ -0,0 +1 @@ +BlockNote image \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html new file mode 100644 index 000000000..f4d82fbae --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html @@ -0,0 +1 @@ +

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html index e5edcc48a..a03524e2c 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html @@ -1 +1 @@ -

Heading 1

Nested Paragraph 1

\ No newline at end of file +

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html index 6fa14919d..da64f213c 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html @@ -1 +1 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html index 798ff745d..94071cc1c 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html @@ -1 +1 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html new file mode 100644 index 000000000..62de5c145 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html @@ -0,0 +1 @@ +
BlockNote image
\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html index 8ea4d8235..c5aa9c397 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html @@ -1 +1 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html index 1bf8795a5..e39f722d8 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html @@ -1 +1 @@ -

BoldItalicRegular

\ No newline at end of file +

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html index e69392705..35587c6f9 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html @@ -1 +1 @@ -

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file +

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html new file mode 100644 index 000000000..0f924d986 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html @@ -0,0 +1 @@ +

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html index 7962432c6..fcbabf88d 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html @@ -1 +1 @@ -

Italic

\ No newline at end of file +

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html index 82b7157b1..f819e6a17 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html @@ -1 +1 @@ -

Table Cell

\ No newline at end of file +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html index 82b7157b1..f819e6a17 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html @@ -1 +1 @@ -

Table Cell

\ No newline at end of file +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html index 82b7157b1..f819e6a17 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html @@ -1 +1 @@ -

Table Cell

\ No newline at end of file +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html index 82b7157b1..f819e6a17 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html @@ -1 +1 @@ -

Table Cell

\ No newline at end of file +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html index d13d82566..8f21bb3a4 100644 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html @@ -1 +1 @@ -

Regular

\ No newline at end of file +

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts index 397f025c5..1cce678ba 100644 --- a/packages/core/src/api/exporters/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -263,6 +263,8 @@ describe("Test ProseMirror selection HTML conversion", () => { const div = document.createElement("div"); beforeAll(async () => { + (window as any).__TEST_OPTIONS = (window as any).__TEST_OPTIONS || {}; + editor = BlockNoteEditor.create({ initialContent }); editor.mount(div); @@ -318,6 +320,13 @@ describe("Test ProseMirror selection HTML conversion", () => { startPos: 3, endPos: 34, }, + // Selection spans from middle of first heading to the middle of its first + // child. + { + testName: "partialChildToParent", + startPos: 6, + endPos: 23, + }, // Selection spans from start of first heading's first child to end of // second heading's content (does not include second heading's children). { @@ -351,6 +360,12 @@ describe("Test ProseMirror selection HTML conversion", () => { startPos: 165, endPos: 182, }, + // Selection spans the image block content. + { + testName: "image", + startPos: 185, + endPos: 186, + }, // Selection spans from start of third heading to end of it's last // descendant. { From b5dbe306f09750b9c5bea1434c37255eb9b2ab2a Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 1 Oct 2024 12:44:10 +0200 Subject: [PATCH 03/17] Changed to use default PM behaviour --- .../core/src/api/exporters/copyExtension.ts | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/core/src/api/exporters/copyExtension.ts b/packages/core/src/api/exporters/copyExtension.ts index 8a168387a..7d2c0cc26 100644 --- a/packages/core/src/api/exporters/copyExtension.ts +++ b/packages/core/src/api/exporters/copyExtension.ts @@ -1,6 +1,7 @@ import { Extension } from "@tiptap/core"; -import { Node } from "prosemirror-model"; +import { DOMSerializer, Node } from "prosemirror-model"; import { NodeSelection, Plugin } from "prosemirror-state"; +import { __serializeForClipboard } from "prosemirror-view"; import { EditorView } from "prosemirror-view"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; @@ -22,16 +23,47 @@ async function selectedFragmentToHTML< externalHTML: string; plainText: string; }> { + // let selectedFragment = view.state.doc.slice( + // view.state.selection.from, + // view.state.selection.to, + // false + // ).content; + // console.log(selectedFragment); + // + // const children = []; + // for (let i = 0; i < selectedFragment.childCount; i++) { + // children.push(selectedFragment.child(i)); + // } + // const isWithinBlockContent = + // children.find( + // (child) => + // child.type.name === "blockContainer" || + // child.type.name === "blockGroup" || + // child.type.spec.group === "blockContent" + // ) === undefined; + // if (!isWithinBlockContent) { + // selectedFragment = view.state.doc.slice( + // view.state.selection.from, + // view.state.selection.to, + // true + // ).content; + // } const selectedFragment = view.state.selection.content().content; - - const internalHTMLSerializer = createInternalHTMLSerializer( - view.state.schema, - editor - ); - const internalHTML = internalHTMLSerializer.serializeProseMirrorFragment( - selectedFragment, - {} - ); + // console.log(selectedFragment); + const s = __serializeForClipboard(view, selectedFragment); + console.log(s); + + // 1. Why did we use the internal serializer to put HTML on the clipboard and not the defualt logic? + // 2. Will we lose context from parent nodes if we e.g. only select block content? + + // const internalHTMLSerializer = createInternalHTMLSerializer( + // view.state.schema, + // editor + // ); + // const internalHTML = internalHTMLSerializer.serializeProseMirrorFragment( + // selectedFragment, + // {} + // ); await initializeESMDependencies(); const externalHTMLExporter = createExternalHTMLExporter( @@ -45,7 +77,7 @@ async function selectedFragmentToHTML< const plainText = await cleanHTMLToMarkdown(externalHTML); - return { internalHTML, externalHTML, plainText }; + return { internalHTML: s.dom.outerHTML, externalHTML, plainText }; } const copyToClipboard = < From 6e4a3f1af1e7600bb541bd8296f3127c74a136c2 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 1 Oct 2024 14:17:15 +0200 Subject: [PATCH 04/17] fix row pasting --- packages/core/src/blocks/TableBlockContent/TableBlockContent.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index e47a62b2d..1e45066cf 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -46,7 +46,6 @@ const TableParagraph = Node.create({ parseHTML() { return [ - { tag: "td" }, { tag: "p", getAttrs: (element) => { From 1eabc4ab0c985fa429c915b1acf69d7eff6aa746 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 1 Oct 2024 14:21:23 +0200 Subject: [PATCH 05/17] Cleaned up code --- .../core/src/api/exporters/copyExtension.ts | 83 +++++++++---------- .../exporters/html/externalHTMLExporter.ts | 15 ++-- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/packages/core/src/api/exporters/copyExtension.ts b/packages/core/src/api/exporters/copyExtension.ts index 7d2c0cc26..1ed601e62 100644 --- a/packages/core/src/api/exporters/copyExtension.ts +++ b/packages/core/src/api/exporters/copyExtension.ts @@ -1,16 +1,18 @@ import { Extension } from "@tiptap/core"; -import { DOMSerializer, Node } from "prosemirror-model"; +import { Node } from "prosemirror-model"; import { NodeSelection, Plugin } from "prosemirror-state"; -import { __serializeForClipboard } from "prosemirror-view"; import { EditorView } from "prosemirror-view"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; import { initializeESMDependencies } from "../../util/esmDependencies"; import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; -import { createInternalHTMLSerializer } from "./html/internalHTMLSerializer"; import { cleanHTMLToMarkdown } from "./markdown/markdownExporter"; +// use a dynamic import because we want to access +// __serializeForClipboard which is not exposed in types +let pmView: any; + async function selectedFragmentToHTML< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -23,47 +25,37 @@ async function selectedFragmentToHTML< externalHTML: string; plainText: string; }> { - // let selectedFragment = view.state.doc.slice( - // view.state.selection.from, - // view.state.selection.to, - // false - // ).content; - // console.log(selectedFragment); - // - // const children = []; - // for (let i = 0; i < selectedFragment.childCount; i++) { - // children.push(selectedFragment.child(i)); - // } - // const isWithinBlockContent = - // children.find( - // (child) => - // child.type.name === "blockContainer" || - // child.type.name === "blockGroup" || - // child.type.spec.group === "blockContent" - // ) === undefined; - // if (!isWithinBlockContent) { - // selectedFragment = view.state.doc.slice( - // view.state.selection.from, - // view.state.selection.to, - // true - // ).content; - // } - const selectedFragment = view.state.selection.content().content; - // console.log(selectedFragment); - const s = __serializeForClipboard(view, selectedFragment); - console.log(s); + let selectedFragment = view.state.doc.slice( + view.state.selection.from, + view.state.selection.to, + false + ).content; + + const children = []; + for (let i = 0; i < selectedFragment.childCount; i++) { + children.push(selectedFragment.child(i)); + } - // 1. Why did we use the internal serializer to put HTML on the clipboard and not the defualt logic? - // 2. Will we lose context from parent nodes if we e.g. only select block content? + const isWithinBlockContent = + children.find( + (child) => + child.type.name === "blockContainer" || + child.type.name === "blockGroup" || + child.type.spec.group === "blockContent" + ) === undefined; + if (!isWithinBlockContent) { + selectedFragment = editor.prosemirrorView.state.doc.slice( + editor.prosemirrorView.state.selection.from, + editor.prosemirrorView.state.selection.to, + true + ).content; + } - // const internalHTMLSerializer = createInternalHTMLSerializer( - // view.state.schema, - // editor - // ); - // const internalHTML = internalHTMLSerializer.serializeProseMirrorFragment( - // selectedFragment, - // {} - // ); + // console.log(selectedFragment); + const internalHTML: string = pmView.__serializeForClipboard( + view, + view.state.selection.content() + ).dom.outerHTML; await initializeESMDependencies(); const externalHTMLExporter = createExternalHTMLExporter( @@ -72,12 +64,12 @@ async function selectedFragmentToHTML< ); const externalHTML = externalHTMLExporter.exportProseMirrorFragment( selectedFragment, - {} + { simplifyBlocks: !isWithinBlockContent } ); const plainText = await cleanHTMLToMarkdown(externalHTML); - return { internalHTML: s.dom.outerHTML, externalHTML, plainText }; + return { internalHTML, externalHTML, plainText }; } const copyToClipboard = < @@ -129,6 +121,9 @@ export const createCopyToClipboardExtension = < ) => Extension.create<{ editor: BlockNoteEditor }, undefined>({ name: "copyToClipboard", + onCreate: async () => { + pmView = await import("prosemirror-view"); + }, addProseMirrorPlugins() { return [ new Plugin({ diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts index beb81487c..fe170f730 100644 --- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts +++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts @@ -41,7 +41,7 @@ export interface ExternalHTMLExporter< ) => string; exportProseMirrorFragment: ( fragment: Fragment, - options: { document?: Document } + options: { document?: Document; simplifyBlocks?: boolean } ) => string; } @@ -70,7 +70,7 @@ export const createExternalHTMLExporter = < ) => HTMLElement; exportProseMirrorFragment: ( fragment: Fragment, - options: { document?: Document } + options: { document?: Document; simplifyBlocks?: boolean } ) => string; exportBlocks: ( blocks: PartialBlock[], @@ -87,16 +87,19 @@ export const createExternalHTMLExporter = < // but additionally runs it through the `simplifyBlocks` rehype plugin to // convert the internal HTML to external. serializer.exportProseMirrorFragment = (fragment, options) => { - const externalHTML = deps.unified + let externalHTML: any = deps.unified .unified() - .use(deps.rehypeParse.default, { fragment: true }) - .use(simplifyBlocks, { + .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)); From f5e34666af713b2eaf9334c215d48c0e153ea903 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 1 Oct 2024 14:26:51 +0200 Subject: [PATCH 06/17] Lint fix --- .../core/src/api/exporters/html/htmlConversion.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts index 1cce678ba..77d20dbdf 100644 --- a/packages/core/src/api/exporters/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -305,9 +305,14 @@ describe("Test ProseMirror selection HTML conversion", () => { expect(externalHTML).toMatchFileSnapshot( `./__snapshots_selection_html__/external/${testName}.html` ); + + // TODO: Add a snapshot comparison for document JSON after paste. } const testCases: { testName: string; startPos: number; endPos: number }[] = [ + // TODO: Consider adding test cases for nested blocks & double nested blocks. + // TODO: Add test case for copying 2 paragraphs as this was a bug in the past. + // TODO: Add test case for copying multiple list items as this was a bug in the past. // Selection spans all of first heading's children. { testName: "multipleChildren", @@ -400,7 +405,7 @@ describe("Test ProseMirror selection HTML conversion", () => { ]; for (const testCase of testCases) { - it(testCase.testName, () => { + it(`${testCase.testName}`, () => { testSelection(testCase.testName, testCase.startPos, testCase.endPos); }); } From addd40ff3ae9ed6999cf9f4a3523fc9d27aa93d8 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 1 Oct 2024 14:50:23 +0200 Subject: [PATCH 07/17] fix table pasting --- packages/core/src/editor/transformPasted.ts | 34 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts index cea35c8e0..cb21f25a7 100644 --- a/packages/core/src/editor/transformPasted.ts +++ b/packages/core/src/editor/transformPasted.ts @@ -1,4 +1,4 @@ -import { Fragment, Slice } from "@tiptap/pm/model"; +import { Fragment, Schema, Slice } from "@tiptap/pm/model"; import { EditorView } from "@tiptap/pm/view"; // helper function to remove a child from a fragment @@ -12,6 +12,35 @@ function removeChild(node: Fragment, n: number) { return Fragment.from(children); } +/** + * Wrap adjacent tableRow items in a table. + * + * This makes sure the content that we paste is always a table (and not a tableRow) + * A table works better for the remaing paste handling logic, as it's actually a blockContent node + */ +export function wrapTableRows(f: Fragment, schema: Schema) { + const newItems: any[] = []; + for (let i = 0; i < f.childCount; i++) { + if (f.child(i).type.name === "tableRow") { + if ( + newItems.length > 0 && + newItems[newItems.length - 1].type.name === "table" + ) { + // append to existing table + const prevTable = newItems[newItems.length - 1]; + const newTable = prevTable.copy(prevTable.content.addToEnd(f.child(i))); + newItems[newItems.length - 1] = newTable; + } else { + // create new table to wrap tableRow with + const newTable = schema.nodes.table.create(undefined, f.child(i)); + newItems.push(newTable); + } + } + } + f = Fragment.from(newItems); + return f; +} + /** * fix for https://github.com/ProseMirror/prosemirror/issues/1430#issuecomment-1822570821 * @@ -23,6 +52,8 @@ function removeChild(node: Fragment, n: number) { */ export function transformPasted(slice: Slice, view: EditorView) { let f = Fragment.from(slice.content); + f = wrapTableRows(f, view.state.schema); + for (let i = 0; i < f.childCount; i++) { if (f.child(i).type.spec.group === "blockContent") { const content = [f.child(i)]; @@ -54,6 +85,5 @@ export function transformPasted(slice: Slice, view: EditorView) { f = f.replaceChild(i, container); } } - return new Slice(f, slice.openStart, slice.openEnd); } From ed345af2bfb3e73434de13636c9ea5d3a0601346 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 1 Oct 2024 15:35:58 +0200 Subject: [PATCH 08/17] fix bug --- packages/core/src/editor/transformPasted.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts index cb21f25a7..334a1aab8 100644 --- a/packages/core/src/editor/transformPasted.ts +++ b/packages/core/src/editor/transformPasted.ts @@ -35,6 +35,8 @@ export function wrapTableRows(f: Fragment, schema: Schema) { const newTable = schema.nodes.table.create(undefined, f.child(i)); newItems.push(newTable); } + } else { + newItems.push(f.child(i)); } } f = Fragment.from(newItems); From d2ead8d318f6b976b7be274b9a9b2c0e7ff66c7f Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 1 Oct 2024 15:36:16 +0200 Subject: [PATCH 09/17] fix dom serializer cache being overwritten --- .../core/src/api/exporters/html/externalHTMLExporter.ts | 6 +++++- .../core/src/api/exporters/html/internalHTMLSerializer.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts index fe170f730..2d6a859d9 100644 --- a/packages/core/src/api/exporters/html/externalHTMLExporter.ts +++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts @@ -63,7 +63,11 @@ export const createExternalHTMLExporter = < ); } - const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & { + // 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 } diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts index d1357774b..923319067 100644 --- a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts +++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts @@ -7,7 +7,6 @@ import { serializeNodeInner, serializeProseMirrorFragment, } 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`. @@ -48,7 +47,11 @@ export const createInternalHTMLSerializer = < schema: Schema, editor: BlockNoteEditor ): InternalHTMLSerializer => { - const serializer = DOMSerializer.fromSchema(schema) as DOMSerializer & { + // 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 } From ac20ceb40b4a424109b200cb097742f8d2b4d8d1 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 1 Oct 2024 18:50:23 +0200 Subject: [PATCH 10/17] Updated copying code and tests --- .../core/src/api/exporters/copyExtension.ts | 23 +++--- .../childToParent.html | 1 + .../childrenToNextParent.html | 1 + .../childrenToNextParentsChildren.html | 1 + .../external/childToParent.html | 1 - .../external/childrenToNextParent.html | 1 - .../childrenToNextParentsChildren.html | 1 - .../external/image.html | 1 - .../external/multipleChildren.html | 1 - .../external/multipleStyledText.html | 1 - .../external/nestedImage.html | 1 - .../external/partialChildToParent.html | 1 - .../external/styledText.html | 1 - .../external/tableAllCells.html | 1 - .../external/tableCell.html | 1 - .../external/tableCellText.html | 1 - .../external/tableRow.html | 1 - .../external/unstyledText.html | 1 - .../__snapshots_selection_html__/image.html | 1 + .../internal/childToParent.html | 1 - .../internal/childrenToNextParent.html | 1 - .../childrenToNextParentsChildren.html | 1 - .../internal/image.html | 1 - .../internal/multipleChildren.html | 1 - .../internal/multipleStyledText.html | 1 - .../internal/nestedImage.html | 1 - .../internal/partialChildToParent.html | 1 - .../internal/styledText.html | 1 - .../internal/tableAllCells.html | 1 - .../internal/tableCell.html | 1 - .../internal/tableCellText.html | 1 - .../internal/tableRow.html | 1 - .../internal/unstyledText.html | 1 - .../multipleChildren.html | 1 + .../multipleStyledText.html | 1 + .../nestedImage.html | 1 + .../partialChildToParent.html | 1 + .../styledText.html | 1 + .../tableAllCells.html | 1 + .../tableCell.html | 1 + .../tableCellText.html | 1 + .../tableRow.html | 1 + .../unstyledText.html | 1 + .../api/exporters/html/htmlConversion.test.ts | 77 ++++++++++--------- .../TableBlockContent/TableBlockContent.ts | 10 ++- packages/core/src/schema/blocks/createSpec.ts | 23 ++++-- packages/react/src/schema/ReactBlockSpec.tsx | 24 ++++-- 47 files changed, 111 insertions(+), 88 deletions(-) create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html create mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html diff --git a/packages/core/src/api/exporters/copyExtension.ts b/packages/core/src/api/exporters/copyExtension.ts index 1ed601e62..c50f333aa 100644 --- a/packages/core/src/api/exporters/copyExtension.ts +++ b/packages/core/src/api/exporters/copyExtension.ts @@ -25,6 +25,15 @@ async function selectedFragmentToHTML< externalHTML: string; plainText: string; }> { + // Uses default ProseMirror clipboard serialization. + const internalHTML: string = pmView.__serializeForClipboard( + view, + view.state.selection.content() + ).dom.innerHTML; + + // 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 selectedFragment = view.state.doc.slice( view.state.selection.from, view.state.selection.to, @@ -44,19 +53,13 @@ async function selectedFragmentToHTML< child.type.spec.group === "blockContent" ) === undefined; if (!isWithinBlockContent) { - selectedFragment = editor.prosemirrorView.state.doc.slice( - editor.prosemirrorView.state.selection.from, - editor.prosemirrorView.state.selection.to, + selectedFragment = view.state.doc.slice( + view.state.selection.from, + view.state.selection.to, true ).content; } - // console.log(selectedFragment); - const internalHTML: string = pmView.__serializeForClipboard( - view, - view.state.selection.content() - ).dom.outerHTML; - await initializeESMDependencies(); const externalHTMLExporter = createExternalHTMLExporter( view.state.schema, @@ -67,7 +70,7 @@ async function selectedFragmentToHTML< { simplifyBlocks: !isWithinBlockContent } ); - const plainText = await cleanHTMLToMarkdown(externalHTML); + const plainText = cleanHTMLToMarkdown(externalHTML); return { internalHTML, externalHTML, plainText }; } diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html new file mode 100644 index 000000000..4ede3b121 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html @@ -0,0 +1 @@ +

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html new file mode 100644 index 000000000..82b08dd73 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html new file mode 100644 index 000000000..529b1be46 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html deleted file mode 100644 index a899fb60f..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childToParent.html +++ /dev/null @@ -1 +0,0 @@ -

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html deleted file mode 100644 index 851b8973d..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParent.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html deleted file mode 100644 index ba81a3a20..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/childrenToNextParentsChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html deleted file mode 100644 index 1a8682186..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/image.html +++ /dev/null @@ -1 +0,0 @@ -BlockNote image \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleChildren.html deleted file mode 100644 index 8bb03060e..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html deleted file mode 100644 index 45a65883e..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/multipleStyledText.html +++ /dev/null @@ -1 +0,0 @@ -

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html deleted file mode 100644 index 4e181d251..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/nestedImage.html +++ /dev/null @@ -1 +0,0 @@ -

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html deleted file mode 100644 index f4d82fbae..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/partialChildToParent.html +++ /dev/null @@ -1 +0,0 @@ -

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html deleted file mode 100644 index a8f4e4c2b..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/styledText.html +++ /dev/null @@ -1 +0,0 @@ -

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html deleted file mode 100644 index 5d6537744..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableAllCells.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html deleted file mode 100644 index 5d6537744..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCell.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html deleted file mode 100644 index 5d6537744..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableCellText.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html deleted file mode 100644 index 5d6537744..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/tableRow.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html deleted file mode 100644 index a5db078fd..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/external/unstyledText.html +++ /dev/null @@ -1 +0,0 @@ -

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html new file mode 100644 index 000000000..4fb5a3a5e --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html deleted file mode 100644 index a03524e2c..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childToParent.html +++ /dev/null @@ -1 +0,0 @@ -

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html deleted file mode 100644 index da64f213c..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParent.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html deleted file mode 100644 index 94071cc1c..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/childrenToNextParentsChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html deleted file mode 100644 index 62de5c145..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/image.html +++ /dev/null @@ -1 +0,0 @@ -
BlockNote image
\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html deleted file mode 100644 index c5aa9c397..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html deleted file mode 100644 index e39f722d8..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/multipleStyledText.html +++ /dev/null @@ -1 +0,0 @@ -

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html deleted file mode 100644 index 35587c6f9..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/nestedImage.html +++ /dev/null @@ -1 +0,0 @@ -

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html deleted file mode 100644 index 0f924d986..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/partialChildToParent.html +++ /dev/null @@ -1 +0,0 @@ -

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html deleted file mode 100644 index fcbabf88d..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/styledText.html +++ /dev/null @@ -1 +0,0 @@ -

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html deleted file mode 100644 index f819e6a17..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableAllCells.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html deleted file mode 100644 index f819e6a17..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCell.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html deleted file mode 100644 index f819e6a17..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableCellText.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html deleted file mode 100644 index f819e6a17..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/tableRow.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html deleted file mode 100644 index 8f21bb3a4..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/internal/unstyledText.html +++ /dev/null @@ -1 +0,0 @@ -

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html new file mode 100644 index 000000000..299369835 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html new file mode 100644 index 000000000..b57986e6a --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html @@ -0,0 +1 @@ +

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html new file mode 100644 index 000000000..10fec96f9 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html @@ -0,0 +1 @@ +

BoldItalicRegular

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html new file mode 100644 index 000000000..c099c9291 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html @@ -0,0 +1 @@ +

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html new file mode 100644 index 000000000..a70005fea --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html @@ -0,0 +1 @@ +

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html new file mode 100644 index 000000000..9a1354555 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html new file mode 100644 index 000000000..9a1354555 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html new file mode 100644 index 000000000..b2a08a378 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html new file mode 100644 index 000000000..9a1354555 --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html new file mode 100644 index 000000000..a0398fd0a --- /dev/null +++ b/packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html @@ -0,0 +1 @@ +

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts index 77d20dbdf..5f1c6dbec 100644 --- a/packages/core/src/api/exporters/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -1,4 +1,4 @@ -import { TextSelection } from "prosemirror-state"; +import { NodeSelection, TextSelection } from "prosemirror-state"; import { afterAll, afterEach, @@ -10,15 +10,7 @@ import { } from "vitest"; import { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; -import { - addIdsToBlocks, - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema, - ExternalHTMLExporter, - InternalHTMLSerializer, - partialBlocksToBlocksForTesting, -} from "../../.."; +import { addIdsToBlocks, partialBlocksToBlocksForTesting } from "../../.."; import { PartialBlock } from "../../../blocks/defaultBlocks"; import { BlockSchema } from "../../../schema/blocks/types"; import { InlineContentSchema } from "../../../schema/inlineContent/types"; @@ -30,6 +22,7 @@ import { customStylesTestCases } from "../../testUtil/cases/customStyles"; import { defaultSchemaTestCases } from "../../testUtil/cases/defaultSchema"; import { createExternalHTMLExporter } from "./externalHTMLExporter"; import { createInternalHTMLSerializer } from "./internalHTMLSerializer"; +import { Node } from "prosemirror-model"; async function convertToHTMLAndCompareSnapshots< B extends BlockSchema, @@ -127,7 +120,7 @@ describe("Test HTML conversion", () => { // The test cases used for the other HTML conversion tests are not suitable here // as they are represented in the BlockNote API, whereas here we want to test // ProseMirror/TipTap selections directly. -describe("Test ProseMirror selection HTML conversion", () => { +describe("Test ProseMirror selection clipboard HTML", () => { const initialContent: PartialBlock[] = [ { type: "heading", @@ -249,28 +242,23 @@ describe("Test ProseMirror selection HTML conversion", () => { }, ]; + let pmView: any; let editor: BlockNoteEditor; - let serializer: InternalHTMLSerializer< - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema - >; - let exporter: ExternalHTMLExporter< - DefaultBlockSchema, - DefaultInlineContentSchema, - DefaultStyleSchema - >; const div = document.createElement("div"); + document.body.append(div); + + beforeEach(() => { + editor.replaceBlocks(editor.document, initialContent); + }); beforeAll(async () => { + pmView = await import("prosemirror-view"); (window as any).__TEST_OPTIONS = (window as any).__TEST_OPTIONS || {}; - editor = BlockNoteEditor.create({ initialContent }); + editor = BlockNoteEditor.create(); editor.mount(div); await initializeESMDependencies(); - serializer = createInternalHTMLSerializer(editor.pmSchema, editor); - exporter = createExternalHTMLExporter(editor.pmSchema, editor); }); afterAll(() => { @@ -289,24 +277,43 @@ describe("Test ProseMirror selection HTML conversion", () => { TextSelection.create(editor._tiptapEditor.state.doc, startPos, endPos) ) ); + if ( + "node" in editor._tiptapEditor.view.state.selection && + (editor._tiptapEditor.view.state.selection.node as Node).type.spec + .group === "blockContent" + ) { + editor.dispatch( + editor._tiptapEditor.state.tr.setSelection( + new NodeSelection( + editor._tiptapEditor.view.state.doc.resolve( + editor._tiptapEditor.view.state.selection.from - 1 + ) + ) + ) + ); + } - const copiedFragment = - editor._tiptapEditor.state.selection.content().content; + const originalSlice = editor._tiptapEditor.view.state.selection.content(); - const internalHTML = serializer.serializeProseMirrorFragment( - copiedFragment, - {} - ); - const externalHTML = exporter.exportProseMirrorFragment(copiedFragment, {}); + // Uses default ProseMirror clipboard serialization. + const internalHTML: string = pmView.__serializeForClipboard( + editor._tiptapEditor.view, + originalSlice + ).dom.innerHTML; expect(internalHTML).toMatchFileSnapshot( - `./__snapshots_selection_html__/internal/${testName}.html` + `./__snapshots_selection_html__/${testName}.html` ); - expect(externalHTML).toMatchFileSnapshot( - `./__snapshots_selection_html__/external/${testName}.html` + + const recreatedSlice = pmView.__parseFromClipboard( + editor._tiptapEditor.view, + "", + internalHTML, + false, + editor._tiptapEditor.state.selection.$from ); - // TODO: Add a snapshot comparison for document JSON after paste. + expect(recreatedSlice).toStrictEqual(originalSlice); } const testCases: { testName: string; startPos: number; endPos: number }[] = [ diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index 1e45066cf..3329449a8 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -48,11 +48,16 @@ const TableParagraph = Node.create({ return [ { tag: "p", + priority: 1000, getAttrs: (element) => { if (typeof element === "string" || !element.textContent) { return false; } + if (element.hasAttribute("data-table-cell")) { + return {}; + } + const parent = element.parentElement; if (parent === null) { @@ -72,7 +77,10 @@ const TableParagraph = Node.create({ renderHTML({ HTMLAttributes }) { return [ "p", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + // Ensures correct parsing even without a parent table cell context. + "data-table-cell": "", + }), 0, ]; }, diff --git a/packages/core/src/schema/blocks/createSpec.ts b/packages/core/src/schema/blocks/createSpec.ts index 1dac32dd6..b68b90c65 100644 --- a/packages/core/src/schema/blocks/createSpec.ts +++ b/packages/core/src/schema/blocks/createSpec.ts @@ -162,15 +162,24 @@ export function createBlockSpec< return getParseRules(blockConfig, blockImplementation.parse); }, - renderHTML() { - // renderHTML is not really used, as we always use a nodeView, and we use toExternalHTML / toInternalHTML for serialization - // There's an edge case when this gets called nevertheless; before the nodeviews have been mounted - // this is why we implement it with a temporary placeholder + renderHTML({ HTMLAttributes }) { + // renderHTML is used for copy/pasting content from the editor back into + // the editor, so we need to make sure the `blockContent` element is + // structured correctly as this is what's used for parsing blocks. We + // just render a placeholder div inside as the `blockContent` element + // already has all the information needed for proper parsing. const div = document.createElement("div"); div.setAttribute("data-tmp-placeholder", "true"); - return { - dom: div, - }; + return wrapInBlockStructure( + { + dom: div, + }, + blockConfig.type, + {}, + blockConfig.propSchema, + blockConfig.isFileBlock, + HTMLAttributes + ); }, addNodeView() { diff --git a/packages/react/src/schema/ReactBlockSpec.tsx b/packages/react/src/schema/ReactBlockSpec.tsx index 687f93afe..1122527a0 100644 --- a/packages/react/src/schema/ReactBlockSpec.tsx +++ b/packages/react/src/schema/ReactBlockSpec.tsx @@ -17,6 +17,7 @@ import { PropSchema, propsToAttributes, StyleSchema, + wrapInBlockStructure, } from "@blocknote/core"; import { NodeView, @@ -130,15 +131,24 @@ export function createReactBlockSpec< return getParseRules(blockConfig, blockImplementation.parse); }, - renderHTML() { - // renderHTML is not really used, as we always use a nodeView, and we use toExternalHTML / toInternalHTML for serialization - // There's an edge case when this gets called nevertheless; before the nodeviews have been mounted - // this is why we implement it with a temporary placeholder + renderHTML({ HTMLAttributes }) { + // renderHTML is used for copy/pasting content from the editor back into + // the editor, so we need to make sure the `blockContent` element is + // structured correctly as this is what's used for parsing blocks. We + // just render a placeholder div inside as the `blockContent` element + // already has all the information needed for proper parsing. const div = document.createElement("div"); div.setAttribute("data-tmp-placeholder", "true"); - return { - dom: div, - }; + return wrapInBlockStructure( + { + dom: div, + }, + blockConfig.type, + {}, + blockConfig.propSchema, + blockConfig.isFileBlock, + HTMLAttributes + ); }, addNodeView() { From 8337d8242da380360c3688754a101312969c261c Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 2 Oct 2024 19:02:09 +0200 Subject: [PATCH 11/17] Implemented PR feedback --- .../__snapshots__/childToParent.html | 1 + .../__snapshots__/childrenToNextParent.html | 1 + .../childrenToNextParentsChildren.html | 1 + .../api/clipboard/__snapshots__/image.html | 1 + .../__snapshots__/multipleChildren.html | 1 + .../__snapshots__/multipleStyledText.html | 1 + .../clipboard/__snapshots__/nestedImage.html | 1 + .../__snapshots__/partialChildToParent.html | 1 + .../clipboard/__snapshots__/styledText.html | 1 + .../__snapshots__/tableAllCells.html | 1 + .../clipboard/__snapshots__/tableCell.html | 1 + .../__snapshots__/tableCellText.html | 1 + .../api/clipboard/__snapshots__/tableRow.html | 1 + .../clipboard/__snapshots__/unstyledText.html | 1 + .../core/src/api/clipboard/clipboard.test.ts | 304 +++++++++++ .../fromClipboard}/acceptedMIMETypes.ts | 0 .../fromClipboard}/fileDropExtension.ts | 4 +- .../fromClipboard}/handleFileInsertion.ts | 8 +- .../fromClipboard}/pasteExtension.ts | 6 +- .../toClipboard}/copyExtension.ts | 89 ++-- .../childToParent.html | 1 - .../childrenToNextParent.html | 1 - .../childrenToNextParentsChildren.html | 1 - .../__snapshots_selection_html__/image.html | 1 - .../multipleChildren.html | 1 - .../multipleStyledText.html | 1 - .../nestedImage.html | 1 - .../partialChildToParent.html | 1 - .../styledText.html | 1 - .../tableAllCells.html | 1 - .../tableCell.html | 1 - .../tableCellText.html | 1 - .../tableRow.html | 1 - .../unstyledText.html | 1 - .../api/exporters/html/htmlConversion.test.ts | 322 +----------- .../paste/parse-deep-nested-content.json | 240 --------- .../paste/parse-google-docs-html.json | 476 ------------------ .../src/api/parsers/html/parseHTML.test.ts | 11 +- .../TableBlockContent/TableBlockContent.ts | 10 +- .../core/src/editor/BlockNoteExtensions.ts | 6 +- 40 files changed, 384 insertions(+), 1120 deletions(-) create mode 100644 packages/core/src/api/clipboard/__snapshots__/childToParent.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/image.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/multipleChildren.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/nestedImage.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/styledText.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/tableAllCells.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/tableCell.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/tableCellText.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/tableRow.html create mode 100644 packages/core/src/api/clipboard/__snapshots__/unstyledText.html create mode 100644 packages/core/src/api/clipboard/clipboard.test.ts rename packages/core/src/api/{parsers => clipboard/fromClipboard}/acceptedMIMETypes.ts (100%) rename packages/core/src/api/{parsers => clipboard/fromClipboard}/fileDropExtension.ts (94%) rename packages/core/src/api/{parsers => clipboard/fromClipboard}/handleFileInsertion.ts (94%) rename packages/core/src/api/{parsers => clipboard/fromClipboard}/pasteExtension.ts (91%) rename packages/core/src/api/{exporters => clipboard/toClipboard}/copyExtension.ts (72%) delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html delete mode 100644 packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html delete mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json delete mode 100644 packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json diff --git a/packages/core/src/api/clipboard/__snapshots__/childToParent.html b/packages/core/src/api/clipboard/__snapshots__/childToParent.html new file mode 100644 index 000000000..a899fb60f --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/childToParent.html @@ -0,0 +1 @@ +

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html new file mode 100644 index 000000000..851b8973d --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParent.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html new file mode 100644 index 000000000..ba81a3a20 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/childrenToNextParentsChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/image.html b/packages/core/src/api/clipboard/__snapshots__/image.html new file mode 100644 index 000000000..09248e592 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/image.html @@ -0,0 +1 @@ +BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/multipleChildren.html b/packages/core/src/api/clipboard/__snapshots__/multipleChildren.html new file mode 100644 index 000000000..8bb03060e --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/multipleChildren.html @@ -0,0 +1 @@ +

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html b/packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html new file mode 100644 index 000000000..a9991463b --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/multipleStyledText.html @@ -0,0 +1 @@ +BoldItalicRegular \ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/nestedImage.html b/packages/core/src/api/clipboard/__snapshots__/nestedImage.html new file mode 100644 index 000000000..4e181d251 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/nestedImage.html @@ -0,0 +1 @@ +

BoldItalicRegular

BlockNote image

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html b/packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html new file mode 100644 index 000000000..f4d82fbae --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/partialChildToParent.html @@ -0,0 +1 @@ +

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/styledText.html b/packages/core/src/api/clipboard/__snapshots__/styledText.html new file mode 100644 index 000000000..03d564343 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/styledText.html @@ -0,0 +1 @@ +Italic \ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html b/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html new file mode 100644 index 000000000..1de956c17 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/tableAllCells.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/tableCell.html b/packages/core/src/api/clipboard/__snapshots__/tableCell.html new file mode 100644 index 000000000..b7fa75d30 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/tableCell.html @@ -0,0 +1 @@ +

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/tableCellText.html b/packages/core/src/api/clipboard/__snapshots__/tableCellText.html new file mode 100644 index 000000000..cd55158ac --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/tableCellText.html @@ -0,0 +1 @@ +Table Cell \ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/tableRow.html b/packages/core/src/api/clipboard/__snapshots__/tableRow.html new file mode 100644 index 000000000..a7d0f18df --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/tableRow.html @@ -0,0 +1 @@ +

Table Cell

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/clipboard/__snapshots__/unstyledText.html b/packages/core/src/api/clipboard/__snapshots__/unstyledText.html new file mode 100644 index 000000000..ea9503c08 --- /dev/null +++ b/packages/core/src/api/clipboard/__snapshots__/unstyledText.html @@ -0,0 +1 @@ +Regular \ 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 new file mode 100644 index 000000000..48514be63 --- /dev/null +++ b/packages/core/src/api/clipboard/clipboard.test.ts @@ -0,0 +1,304 @@ +import { Node } from "prosemirror-model"; +import { NodeSelection, Selection, TextSelection } from "prosemirror-state"; +import { CellSelection } from "prosemirror-tables"; +import * as pmView from "prosemirror-view"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +import { PartialBlock } from "../../blocks/defaultBlocks"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { initializeESMDependencies } from "../../util/esmDependencies"; +import { selectedFragmentToHTML } from "./toClipboard/copyExtension"; + +type SelectionTestCase = { + testName: string; + createSelection: (doc: Node) => Selection; +}; + +// These tests are meant to test the copying of user selections in the editor. +// The test cases used for the other HTML conversion tests are not suitable here +// as they are represented in the BlockNote API, whereas here we want to test +// ProseMirror/TipTap selections directly. +describe("Test ProseMirror selection clipboard HTML", () => { + const initialContent: PartialBlock[] = [ + { + type: "heading", + props: { + level: 2, + textColor: "red", + }, + content: "Heading 1", + children: [ + { + type: "paragraph", + content: "Nested Paragraph 1", + }, + { + type: "paragraph", + content: "Nested Paragraph 2", + }, + { + type: "paragraph", + content: "Nested Paragraph 3", + }, + ], + }, + { + type: "heading", + props: { + level: 2, + textColor: "red", + }, + content: "Heading 2", + children: [ + { + type: "paragraph", + content: "Nested Paragraph 1", + }, + { + type: "paragraph", + content: "Nested Paragraph 2", + }, + { + type: "paragraph", + content: "Nested Paragraph 3", + }, + ], + }, + { + type: "heading", + props: { + level: 2, + textColor: "red", + }, + content: [ + { + type: "text", + text: "Bold", + styles: { + bold: true, + }, + }, + { + type: "text", + text: "Italic", + styles: { + italic: true, + }, + }, + { + type: "text", + text: "Regular", + styles: {}, + }, + ], + children: [ + { + type: "image", + props: { + url: "https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg", + }, + children: [ + { + type: "paragraph", + content: "Nested Paragraph", + }, + ], + }, + ], + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell"], + }, + ], + }, + // Not needed as selections starting in table cells will get snapped to + // the table boundaries. + // children: [ + // { + // type: "table", + // content: { + // type: "tableContent", + // rows: [ + // { + // cells: ["Table Cell", "Table Cell"], + // }, + // { + // cells: ["Table Cell", "Table Cell"], + // }, + // ], + // }, + // }, + // ], + }, + ]; + + let editor: BlockNoteEditor; + const div = document.createElement("div"); + + beforeEach(() => { + editor.replaceBlocks(editor.document, initialContent); + }); + + beforeAll(async () => { + (window as any).__TEST_OPTIONS = (window as any).__TEST_OPTIONS || {}; + + editor = BlockNoteEditor.create(); + editor.mount(div); + + await initializeESMDependencies(); + }); + + afterAll(() => { + editor.mount(undefined); + editor._tiptapEditor.destroy(); + editor = undefined as any; + + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; + }); + + // Sets the editor selection to the given start and end positions, then + // exports the selected content to HTML and compares it to a snapshot. + async function testSelection(testCase: SelectionTestCase) { + editor.dispatch( + editor._tiptapEditor.state.tr.setSelection( + testCase.createSelection(editor._tiptapEditor.view.state.doc) + ) + ); + + if ( + "node" in editor._tiptapEditor.view.state.selection && + (editor._tiptapEditor.view.state.selection.node as Node).type.spec + .group === "blockContent" + ) { + editor.dispatch( + editor._tiptapEditor.state.tr.setSelection( + new NodeSelection( + editor._tiptapEditor.view.state.doc.resolve( + editor._tiptapEditor.view.state.selection.from - 1 + ) + ) + ) + ); + } + + const { clipboardHTML, externalHTML } = await selectedFragmentToHTML( + editor._tiptapEditor.view, + editor + ); + + expect(externalHTML).toMatchFileSnapshot( + `./__snapshots__/${testCase.testName}.html` + ); + + const originalDocument = editor.document; + editor._tiptapEditor.state.tr.replaceSelection( + (pmView as any).__parseFromClipboard( + editor._tiptapEditor.view, + "text", + clipboardHTML, + false, + editor._tiptapEditor.view.state.selection.$from + ) + ); + const newDocument = editor.document; + + expect(newDocument).toStrictEqual(originalDocument); + } + + const testCases: SelectionTestCase[] = [ + // TODO: Consider adding test cases for nested blocks & double nested blocks. + // TODO: Add test case for copying 2 paragraphs as this was a bug in the past. + // TODO: Add test case for copying multiple list items as this was a bug in the past. + // Selection spans all of first heading's children. + { + testName: "multipleChildren", + createSelection: (doc) => TextSelection.create(doc, 16, 78), + }, + // Selection spans from start of first heading to end of its first child. + { + testName: "childToParent", + createSelection: (doc) => TextSelection.create(doc, 3, 34), + }, + // Selection spans from middle of first heading to the middle of its first + // child. + { + testName: "partialChildToParent", + createSelection: (doc) => TextSelection.create(doc, 6, 23), + }, + // Selection spans from start of first heading's first child to end of + // second heading's content (does not include second heading's children). + { + testName: "childrenToNextParent", + createSelection: (doc) => TextSelection.create(doc, 16, 93), + }, + // Selection spans from start of first heading's first child to end of + // second heading's last child. + { + testName: "childrenToNextParentsChildren", + createSelection: (doc) => TextSelection.create(doc, 16, 159), + }, + // Selection spans "Regular" text inside third heading. + { + testName: "unstyledText", + createSelection: (doc) => TextSelection.create(doc, 175, 182), + }, + // Selection spans "Italic" text inside third heading. + { + testName: "styledText", + createSelection: (doc) => TextSelection.create(doc, 169, 175), + }, + // Selection spans third heading's content (does not include third heading's + // children). + { + testName: "multipleStyledText", + createSelection: (doc) => TextSelection.create(doc, 165, 182), + }, + // Selection spans the image block content. + { + testName: "image", + createSelection: (doc) => NodeSelection.create(doc, 185), + }, + // Selection spans from start of third heading to end of it's last + // descendant. + { + testName: "nestedImage", + createSelection: (doc) => TextSelection.create(doc, 165, 205), + }, + // Selection spans text in first cell of the table. + { + testName: "tableCellText", + createSelection: (doc) => TextSelection.create(doc, 216, 226), + }, + // Selection spans first cell of the table. + // TODO: External HTML is wrapped in unnecessary `tr` element. + { + testName: "tableCell", + createSelection: (doc) => CellSelection.create(doc, 214), + }, + // Selection spans first row of the table. + { + testName: "tableRow", + 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), + }, + ]; + + for (const testCase of testCases) { + it(`${testCase.testName}`, async () => { + await testSelection(testCase); + }); + } +}); diff --git a/packages/core/src/api/parsers/acceptedMIMETypes.ts b/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts similarity index 100% rename from packages/core/src/api/parsers/acceptedMIMETypes.ts rename to packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts diff --git a/packages/core/src/api/parsers/fileDropExtension.ts b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts similarity index 94% rename from packages/core/src/api/parsers/fileDropExtension.ts rename to packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts index 9964b1b0c..657e77527 100644 --- a/packages/core/src/api/parsers/fileDropExtension.ts +++ b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts @@ -1,8 +1,8 @@ import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema"; import { handleFileInsertion } from "./handleFileInsertion"; import { acceptedMIMETypes } from "./acceptedMIMETypes"; diff --git a/packages/core/src/api/parsers/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts similarity index 94% rename from packages/core/src/api/parsers/handleFileInsertion.ts rename to packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index b68bc4e0d..d060c0656 100644 --- a/packages/core/src/api/parsers/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -1,12 +1,12 @@ -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { PartialBlock } from "../../blocks/defaultBlocks"; +import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import { PartialBlock } from "../../../blocks/defaultBlocks"; import { BlockSchema, FileBlockConfig, InlineContentSchema, StyleSchema, -} from "../../schema"; -import { getBlockInfoFromPos } from "../getBlockInfoFromPos"; +} from "../../../schema"; +import { getBlockInfoFromPos } from "../../getBlockInfoFromPos"; import { acceptedMIMETypes } from "./acceptedMIMETypes"; function checkFileExtensionsMatch( diff --git a/packages/core/src/api/parsers/pasteExtension.ts b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts similarity index 91% rename from packages/core/src/api/parsers/pasteExtension.ts rename to packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts index 97cfd751c..4471dbe28 100644 --- a/packages/core/src/api/parsers/pasteExtension.ts +++ b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts @@ -1,10 +1,10 @@ import { Extension } from "@tiptap/core"; import { Plugin } from "prosemirror-state"; -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema"; import { handleFileInsertion } from "./handleFileInsertion"; -import { nestedListsToBlockNoteStructure } from "./html/util/nestedLists"; +import { nestedListsToBlockNoteStructure } from "../../parsers/html/util/nestedLists"; import { acceptedMIMETypes } from "./acceptedMIMETypes"; export const createPasteFromClipboardExtension = < diff --git a/packages/core/src/api/exporters/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts similarity index 72% rename from packages/core/src/api/exporters/copyExtension.ts rename to packages/core/src/api/clipboard/toClipboard/copyExtension.ts index c50f333aa..525fe174a 100644 --- a/packages/core/src/api/exporters/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -1,19 +1,17 @@ import { Extension } from "@tiptap/core"; import { Node } from "prosemirror-model"; import { NodeSelection, Plugin } from "prosemirror-state"; +import { CellSelection } from "prosemirror-tables"; +import * as pmView from "prosemirror-view"; import { EditorView } from "prosemirror-view"; -import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; -import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; -import { initializeESMDependencies } from "../../util/esmDependencies"; -import { createExternalHTMLExporter } from "./html/externalHTMLExporter"; -import { cleanHTMLToMarkdown } from "./markdown/markdownExporter"; +import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import { BlockSchema, InlineContentSchema, StyleSchema } from "../../../schema"; +import { initializeESMDependencies } from "../../../util/esmDependencies"; +import { createExternalHTMLExporter } from "../../exporters/html/externalHTMLExporter"; +import { cleanHTMLToMarkdown } from "../../exporters/markdown/markdownExporter"; -// use a dynamic import because we want to access -// __serializeForClipboard which is not exposed in types -let pmView: any; - -async function selectedFragmentToHTML< +export async function selectedFragmentToHTML< BSchema extends BlockSchema, I extends InlineContentSchema, S extends StyleSchema @@ -21,43 +19,45 @@ async function selectedFragmentToHTML< view: EditorView, editor: BlockNoteEditor ): Promise<{ - internalHTML: string; + clipboardHTML: string; externalHTML: string; - plainText: string; + markdown: string; }> { // Uses default ProseMirror clipboard serialization. - const internalHTML: string = pmView.__serializeForClipboard( + 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 selectedFragment = view.state.doc.slice( - view.state.selection.from, - view.state.selection.to, - false - ).content; - - const children = []; - for (let i = 0; i < selectedFragment.childCount; i++) { - children.push(selectedFragment.child(i)); - } - - const isWithinBlockContent = - children.find( - (child) => - child.type.name === "blockContainer" || - child.type.name === "blockGroup" || - child.type.spec.group === "blockContent" - ) === undefined; - if (!isWithinBlockContent) { - selectedFragment = view.state.doc.slice( + let isWithinBlockContent = false; + const isWithinTable = view.state.selection instanceof CellSelection; + if (!isWithinTable) { + const fragmentWithoutParents = view.state.doc.slice( view.state.selection.from, view.state.selection.to, - true + false ).content; + + const children = []; + for (let i = 0; i < fragmentWithoutParents.childCount; i++) { + children.push(fragmentWithoutParents.child(i)); + } + + isWithinBlockContent = + children.find( + (child) => + child.type.name === "blockContainer" || + child.type.name === "blockGroup" || + child.type.spec.group === "blockContent" + ) === undefined; + if (isWithinBlockContent) { + selectedFragment = fragmentWithoutParents; + } } await initializeESMDependencies(); @@ -67,12 +67,12 @@ async function selectedFragmentToHTML< ); const externalHTML = externalHTMLExporter.exportProseMirrorFragment( selectedFragment, - { simplifyBlocks: !isWithinBlockContent } + { simplifyBlocks: !isWithinBlockContent && !isWithinTable } ); - const plainText = cleanHTMLToMarkdown(externalHTML); + const markdown = cleanHTMLToMarkdown(externalHTML); - return { internalHTML, externalHTML, plainText }; + return { clipboardHTML, externalHTML, markdown }; } const copyToClipboard = < @@ -104,14 +104,14 @@ const copyToClipboard = < } (async () => { - const { internalHTML, externalHTML, plainText } = + const { clipboardHTML, externalHTML, markdown } = await selectedFragmentToHTML(view, editor); // TODO: Writing to other MIME types not working in Safari for // some reason. - event.clipboardData!.setData("blocknote/html", internalHTML); + event.clipboardData!.setData("blocknote/html", clipboardHTML); event.clipboardData!.setData("text/html", externalHTML); - event.clipboardData!.setData("text/plain", plainText); + event.clipboardData!.setData("text/plain", markdown); })(); }; @@ -124,9 +124,6 @@ export const createCopyToClipboardExtension = < ) => Extension.create<{ editor: BlockNoteEditor }, undefined>({ name: "copyToClipboard", - onCreate: async () => { - pmView = await import("prosemirror-view"); - }, addProseMirrorPlugins() { return [ new Plugin({ @@ -174,14 +171,14 @@ export const createCopyToClipboardExtension = < event.dataTransfer!.clearData(); (async () => { - const { internalHTML, externalHTML, plainText } = + const { clipboardHTML, externalHTML, markdown } = await selectedFragmentToHTML(view, editor); // TODO: Writing to other MIME types not working in Safari for // some reason. - event.dataTransfer!.setData("blocknote/html", internalHTML); + event.dataTransfer!.setData("blocknote/html", clipboardHTML); event.dataTransfer!.setData("text/html", externalHTML); - event.dataTransfer!.setData("text/plain", plainText); + 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_selection_html__/childToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html deleted file mode 100644 index 4ede3b121..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childToParent.html +++ /dev/null @@ -1 +0,0 @@ -

Heading 1

Nested Paragraph 1

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html deleted file mode 100644 index 82b08dd73..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParent.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html deleted file mode 100644 index 529b1be46..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/childrenToNextParentsChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

Heading 2

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html deleted file mode 100644 index 4fb5a3a5e..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/image.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html deleted file mode 100644 index 299369835..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleChildren.html +++ /dev/null @@ -1 +0,0 @@ -

Nested Paragraph 1

Nested Paragraph 2

Nested Paragraph 3

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html deleted file mode 100644 index b57986e6a..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/multipleStyledText.html +++ /dev/null @@ -1 +0,0 @@ -

BoldItalicRegular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html deleted file mode 100644 index 10fec96f9..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/nestedImage.html +++ /dev/null @@ -1 +0,0 @@ -

BoldItalicRegular

Nested Paragraph

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html deleted file mode 100644 index c099c9291..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/partialChildToParent.html +++ /dev/null @@ -1 +0,0 @@ -

ding 1

Nested

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html deleted file mode 100644 index a70005fea..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/styledText.html +++ /dev/null @@ -1 +0,0 @@ -

Italic

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html deleted file mode 100644 index 9a1354555..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableAllCells.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html deleted file mode 100644 index 9a1354555..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCell.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html deleted file mode 100644 index b2a08a378..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableCellText.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html deleted file mode 100644 index 9a1354555..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/tableRow.html +++ /dev/null @@ -1 +0,0 @@ -

Table Cell

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html b/packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html deleted file mode 100644 index a0398fd0a..000000000 --- a/packages/core/src/api/exporters/html/__snapshots_selection_html__/unstyledText.html +++ /dev/null @@ -1 +0,0 @@ -

Regular

\ No newline at end of file diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts index 5f1c6dbec..850158902 100644 --- a/packages/core/src/api/exporters/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -1,20 +1,11 @@ -import { NodeSelection, TextSelection } from "prosemirror-state"; -import { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, -} from "vitest"; -import { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { addIdsToBlocks, partialBlocksToBlocksForTesting } from "../../.."; import { PartialBlock } from "../../../blocks/defaultBlocks"; -import { BlockSchema } from "../../../schema/blocks/types"; -import { InlineContentSchema } from "../../../schema/inlineContent/types"; -import { StyleSchema } from "../../../schema/styles/types"; +import { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; +import { BlockSchema } from "../../../schema"; +import { InlineContentSchema } from "../../../schema"; +import { StyleSchema } from "../../../schema"; import { initializeESMDependencies } from "../../../util/esmDependencies"; import { customBlocksTestCases } from "../../testUtil/cases/customBlocks"; import { customInlineContentTestCases } from "../../testUtil/cases/customInlineContent"; @@ -22,7 +13,6 @@ import { customStylesTestCases } from "../../testUtil/cases/customStyles"; import { defaultSchemaTestCases } from "../../testUtil/cases/defaultSchema"; import { createExternalHTMLExporter } from "./externalHTMLExporter"; import { createInternalHTMLSerializer } from "./internalHTMLSerializer"; -import { Node } from "prosemirror-model"; async function convertToHTMLAndCompareSnapshots< B extends BlockSchema, @@ -115,305 +105,3 @@ describe("Test HTML conversion", () => { }); } }); - -// These tests are meant to test the copying of user selections in the editor. -// The test cases used for the other HTML conversion tests are not suitable here -// as they are represented in the BlockNote API, whereas here we want to test -// ProseMirror/TipTap selections directly. -describe("Test ProseMirror selection clipboard HTML", () => { - const initialContent: PartialBlock[] = [ - { - type: "heading", - props: { - level: 2, - textColor: "red", - }, - content: "Heading 1", - children: [ - { - type: "paragraph", - content: "Nested Paragraph 1", - }, - { - type: "paragraph", - content: "Nested Paragraph 2", - }, - { - type: "paragraph", - content: "Nested Paragraph 3", - }, - ], - }, - { - type: "heading", - props: { - level: 2, - textColor: "red", - }, - content: "Heading 2", - children: [ - { - type: "paragraph", - content: "Nested Paragraph 1", - }, - { - type: "paragraph", - content: "Nested Paragraph 2", - }, - { - type: "paragraph", - content: "Nested Paragraph 3", - }, - ], - }, - { - type: "heading", - props: { - level: 2, - textColor: "red", - }, - content: [ - { - type: "text", - text: "Bold", - styles: { - bold: true, - }, - }, - { - type: "text", - text: "Italic", - styles: { - italic: true, - }, - }, - { - type: "text", - text: "Regular", - styles: {}, - }, - ], - children: [ - { - type: "image", - props: { - url: "https://ralfvanveen.com/wp-content/uploads/2021/06/Placeholder-_-Glossary.svg", - }, - children: [ - { - type: "paragraph", - content: "Nested Paragraph", - }, - ], - }, - ], - }, - { - type: "table", - content: { - type: "tableContent", - rows: [ - { - cells: ["Table Cell", "Table Cell"], - }, - { - cells: ["Table Cell", "Table Cell"], - }, - ], - }, - // Not needed as selections starting in table cells will get snapped to - // the table boundaries. - // children: [ - // { - // type: "table", - // content: { - // type: "tableContent", - // rows: [ - // { - // cells: ["Table Cell", "Table Cell"], - // }, - // { - // cells: ["Table Cell", "Table Cell"], - // }, - // ], - // }, - // }, - // ], - }, - ]; - - let pmView: any; - let editor: BlockNoteEditor; - const div = document.createElement("div"); - document.body.append(div); - - beforeEach(() => { - editor.replaceBlocks(editor.document, initialContent); - }); - - beforeAll(async () => { - pmView = await import("prosemirror-view"); - (window as any).__TEST_OPTIONS = (window as any).__TEST_OPTIONS || {}; - - editor = BlockNoteEditor.create(); - editor.mount(div); - - await initializeESMDependencies(); - }); - - afterAll(() => { - editor.mount(undefined); - editor._tiptapEditor.destroy(); - editor = undefined as any; - - delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; - }); - - // Sets the editor selection to the given start and end positions, then - // exports the selected content to HTML and compares it to a snapshot. - function testSelection(testName: string, startPos: number, endPos: number) { - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - TextSelection.create(editor._tiptapEditor.state.doc, startPos, endPos) - ) - ); - if ( - "node" in editor._tiptapEditor.view.state.selection && - (editor._tiptapEditor.view.state.selection.node as Node).type.spec - .group === "blockContent" - ) { - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - new NodeSelection( - editor._tiptapEditor.view.state.doc.resolve( - editor._tiptapEditor.view.state.selection.from - 1 - ) - ) - ) - ); - } - - const originalSlice = editor._tiptapEditor.view.state.selection.content(); - - // Uses default ProseMirror clipboard serialization. - const internalHTML: string = pmView.__serializeForClipboard( - editor._tiptapEditor.view, - originalSlice - ).dom.innerHTML; - - expect(internalHTML).toMatchFileSnapshot( - `./__snapshots_selection_html__/${testName}.html` - ); - - const recreatedSlice = pmView.__parseFromClipboard( - editor._tiptapEditor.view, - "", - internalHTML, - false, - editor._tiptapEditor.state.selection.$from - ); - - expect(recreatedSlice).toStrictEqual(originalSlice); - } - - const testCases: { testName: string; startPos: number; endPos: number }[] = [ - // TODO: Consider adding test cases for nested blocks & double nested blocks. - // TODO: Add test case for copying 2 paragraphs as this was a bug in the past. - // TODO: Add test case for copying multiple list items as this was a bug in the past. - // Selection spans all of first heading's children. - { - testName: "multipleChildren", - startPos: 16, - endPos: 78, - }, - // Selection spans from start of first heading to end of its first child. - { - testName: "childToParent", - startPos: 3, - endPos: 34, - }, - // Selection spans from middle of first heading to the middle of its first - // child. - { - testName: "partialChildToParent", - startPos: 6, - endPos: 23, - }, - // Selection spans from start of first heading's first child to end of - // second heading's content (does not include second heading's children). - { - testName: "childrenToNextParent", - startPos: 16, - endPos: 93, - }, - // Selection spans from start of first heading's first child to end of - // second heading's last child. - { - testName: "childrenToNextParentsChildren", - startPos: 16, - endPos: 159, - }, - // Selection spans "Regular" text inside third heading. - { - testName: "unstyledText", - startPos: 175, - endPos: 182, - }, - // Selection spans "Italic" text inside third heading. - { - testName: "styledText", - startPos: 169, - endPos: 175, - }, - // Selection spans third heading's content (does not include third heading's - // children). - { - testName: "multipleStyledText", - startPos: 165, - endPos: 182, - }, - // Selection spans the image block content. - { - testName: "image", - startPos: 185, - endPos: 186, - }, - // Selection spans from start of third heading to end of it's last - // descendant. - { - testName: "nestedImage", - startPos: 165, - endPos: 205, - }, - // Selection spans text in first cell of the table. - { - testName: "tableCellText", - startPos: 216, - endPos: 226, - }, - // Selection spans first cell of the table. - { - testName: "tableCell", - startPos: 215, - endPos: 227, - }, - // Selection spans first row of the table. - { - testName: "tableRow", - startPos: 229, - endPos: 241, - }, - // Selection spans all cells of the table. - { - testName: "tableAllCells", - startPos: 259, - endPos: 271, - }, - ]; - - for (const testCase of testCases) { - it(`${testCase.testName}`, () => { - testSelection(testCase.testName, testCase.startPos, testCase.endPos); - }); - } -}); diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json deleted file mode 100644 index ae11e36cb..000000000 --- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-deep-nested-content.json +++ /dev/null @@ -1,240 +0,0 @@ -[ - { - "id": "1", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Outer 1 Div Before", - "styles": {} - } - ], - "children": [] - }, - { - "id": "2", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": " Outer 2 Div Before", - "styles": {} - } - ], - "children": [] - }, - { - "id": "3", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": " Outer 3 Div Before", - "styles": {} - } - ], - "children": [] - }, - { - "id": "4", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": " Outer 4 Div Before", - "styles": {} - } - ], - "children": [] - }, - { - "id": "5", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 1 - }, - "content": [ - { - "type": "text", - "text": "Heading 1", - "styles": {} - } - ], - "children": [] - }, - { - "id": "6", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 2 - }, - "content": [ - { - "type": "text", - "text": "Heading 2", - "styles": {} - } - ], - "children": [] - }, - { - "id": "7", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 3 - }, - "content": [ - { - "type": "text", - "text": "Heading 3", - "styles": {} - } - ], - "children": [] - }, - { - "id": "8", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph", - "styles": {} - } - ], - "children": [] - }, - { - "id": "9", - "type": "image", - "props": { - "backgroundColor": "default", - "textAlignment": "left", - "url": "exampleURL", - "caption": "Image Caption", - "width": 512 - }, - "children": [] - }, - { - "id": "10", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Bold", - "styles": { - "bold": true - } - }, - { - "type": "text", - "text": " ", - "styles": {} - }, - { - "type": "text", - "text": "Italic", - "styles": { - "italic": true - } - }, - { - "type": "text", - "text": " ", - "styles": {} - }, - { - "type": "text", - "text": "Underline", - "styles": { - "underline": true - } - }, - { - "type": "text", - "text": " ", - "styles": {} - }, - { - "type": "text", - "text": "Strikethrough", - "styles": { - "strike": true - } - }, - { - "type": "text", - "text": " ", - "styles": {} - }, - { - "type": "text", - "text": "All", - "styles": { - "bold": true, - "italic": true, - "underline": true, - "strike": true - } - } - ], - "children": [] - }, - { - "id": "11", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": " Outer 4 Div After Outer 3 Div After Outer 2 Div After Outer 1 Div After", - "styles": {} - } - ], - "children": [] - } -] \ No newline at end of file diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json b/packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json deleted file mode 100644 index c45e54ef9..000000000 --- a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-google-docs-html.json +++ /dev/null @@ -1,476 +0,0 @@ -[ - { - "id": "1", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 1 - }, - "content": [ - { - "type": "text", - "text": "Heading 1", - "styles": { - "bold": true - } - } - ], - "children": [] - }, - { - "id": "2", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 2 - }, - "content": [ - { - "type": "text", - "text": "Heading 2", - "styles": { - "bold": true - } - } - ], - "children": [] - }, - { - "id": "3", - "type": "heading", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left", - "level": 3 - }, - "content": [ - { - "type": "text", - "text": "Heading 3", - "styles": { - "bold": true - } - } - ], - "children": [] - }, - { - "id": "4", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph 1", - "styles": {} - } - ], - "children": [] - }, - { - "id": "5", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph 2", - "styles": {} - } - ], - "children": [] - }, - { - "id": "6", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph 3", - "styles": {} - } - ], - "children": [] - }, - { - "id": "7", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph With \nHard Break", - "styles": {} - } - ], - "children": [] - }, - { - "id": "8", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Bold", - "styles": { - "bold": true - } - }, - { - "type": "text", - "text": " ", - "styles": {} - }, - { - "type": "text", - "text": "Italic", - "styles": { - "italic": true - } - }, - { - "type": "text", - "text": " Underline ", - "styles": {} - }, - { - "type": "text", - "text": "Strikethrough", - "styles": { - "strike": true - } - }, - { - "type": "text", - "text": " ", - "styles": {} - }, - { - "type": "text", - "text": "All", - "styles": { - "bold": true, - "italic": true, - "strike": true - } - } - ], - "children": [] - }, - { - "id": "9", - "type": "bulletListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Bullet List Item 1", - "styles": {} - } - ], - "children": [ - { - "id": "10", - "type": "bulletListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Nested Bullet List Item 1", - "styles": {} - } - ], - "children": [ - { - "id": "11", - "type": "numberedListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Nested Numbered List Item 1", - "styles": {} - } - ], - "children": [] - }, - { - "id": "12", - "type": "numberedListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Nested Numbered List Item 2", - "styles": {} - } - ], - "children": [] - } - ] - }, - { - "id": "13", - "type": "bulletListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Nested Bullet List Item 2", - "styles": {} - } - ], - "children": [] - } - ] - }, - { - "id": "14", - "type": "bulletListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Bullet List Item 2", - "styles": {} - } - ], - "children": [] - }, - { - "id": "15", - "type": "numberedListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Numbered List Item 1", - "styles": {} - } - ], - "children": [] - }, - { - "id": "16", - "type": "numberedListItem", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Numbered List Item 2", - "styles": {} - } - ], - "children": [] - }, - { - "id": "17", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [], - "children": [] - }, - { - "id": "18", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "\n", - "styles": {} - } - ], - "children": [] - }, - { - "id": "19", - "type": "table", - "props": { - "textColor": "default", - "backgroundColor": "default" - }, - "content": { - "type": "tableContent", - "rows": [ - { - "cells": [ - [ - { - "type": "text", - "text": "Cell 1", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Cell 2", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Cell 3", - "styles": {} - } - ] - ] - }, - { - "cells": [ - [ - { - "type": "text", - "text": "Cell 4", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Cell 5", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Cell 6", - "styles": {} - } - ] - ] - }, - { - "cells": [ - [ - { - "type": "text", - "text": "Cell 7", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Cell 8", - "styles": {} - } - ], - [ - { - "type": "text", - "text": "Cell 9", - "styles": {} - } - ] - ] - } - ] - }, - "children": [] - }, - { - "id": "20", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "Paragraph", - "styles": {} - } - ], - "children": [] - }, - { - "id": "21", - "type": "paragraph", - "props": { - "textColor": "default", - "backgroundColor": "default", - "textAlignment": "left" - }, - "content": [ - { - "type": "text", - "text": "\n", - "styles": {} - } - ], - "children": [] - } -] \ No newline at end of file diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index 06ff826ac..d31986d35 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -1,3 +1,4 @@ +import * as pmView from "prosemirror-view"; import { describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../.."; import { nestedListsToBlockNoteStructure } from "./util/nestedLists"; @@ -6,20 +7,18 @@ async function parseHTMLAndCompareSnapshots( html: string, snapshotName: string ) { - // use a dynamic import because we want to access - // __parseFromClipboard which is not exposed in types - const view: any = await import("prosemirror-view"); - const editor = BlockNoteEditor.create(); const div = document.createElement("div"); editor.mount(div); const blocks = await editor.tryParseHTMLToBlocks(html); - const snapshotPath = "./__snapshots__/paste/" + snapshotName + ".json"; + const snapshotPath = "./__snapshots__/" + snapshotName + ".json"; expect(JSON.stringify(blocks, undefined, 2)).toMatchFileSnapshot( snapshotPath ); + // TODO: I don't think is really related to parsing. It's paste behaviour + // which is already tested in the clipboard tests. // Now, we also want to test actually pasting in the editor, and not just calling // tryParseHTMLToBlocks directly. // The reason is that the prosemirror logic for pasting can be a bit different, because @@ -34,7 +33,7 @@ async function parseHTMLAndCompareSnapshots( (window as any).__TEST_OPTIONS.mockID = 0; // reset id counter const htmlNode = nestedListsToBlockNoteStructure(html); - const slice = view.__parseFromClipboard( + const slice = (pmView as any).__parseFromClipboard( editor.prosemirrorView, "", htmlNode.innerHTML, diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index 3329449a8..1e45066cf 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -48,16 +48,11 @@ const TableParagraph = Node.create({ return [ { tag: "p", - priority: 1000, getAttrs: (element) => { if (typeof element === "string" || !element.textContent) { return false; } - if (element.hasAttribute("data-table-cell")) { - return {}; - } - const parent = element.parentElement; if (parent === null) { @@ -77,10 +72,7 @@ const TableParagraph = Node.create({ renderHTML({ HTMLAttributes }) { return [ "p", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - // Ensures correct parsing even without a parent table cell context. - "data-table-cell": "", - }), + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0, ]; }, diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts index fca81fb5a..e03163e53 100644 --- a/packages/core/src/editor/BlockNoteExtensions.ts +++ b/packages/core/src/editor/BlockNoteExtensions.ts @@ -11,9 +11,9 @@ import { History } from "@tiptap/extension-history"; import { Link } from "@tiptap/extension-link"; import { Text } from "@tiptap/extension-text"; import * as Y from "yjs"; -import { createCopyToClipboardExtension } from "../api/exporters/copyExtension"; -import { createDropFileExtension } from "../api/parsers/fileDropExtension"; -import { createPasteFromClipboardExtension } from "../api/parsers/pasteExtension"; +import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension"; +import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension"; +import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension"; import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension"; import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension"; import { TextColorExtension } from "../extensions/TextColor/TextColorExtension"; From ae043e1dfe8acf2ac372c30923a83f9635419607 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 2 Oct 2024 19:02:36 +0200 Subject: [PATCH 12/17] Implemented PR feedback --- packages/core/package.json | 2 +- .../api/parsers/html/__snapshots__/{paste => }/list-test.json | 0 .../__snapshots__/{paste => }/parse-basic-block-types.json | 0 .../{paste => }/parse-div-with-inline-content.json | 0 .../parsers/html/__snapshots__/{paste => }/parse-divs.json | 0 .../__snapshots__/{paste => }/parse-fake-image-caption.json | 0 .../__snapshots__/{paste => }/parse-image-in-paragraph.json | 0 .../__snapshots__/{paste => }/parse-mixed-nested-lists.json | 0 .../{paste => }/parse-nested-lists-with-paragraphs.json | 0 .../html/__snapshots__/{paste => }/parse-nested-lists.json | 0 .../html/__snapshots__/{paste => }/parse-notion-html.json | 0 .../html/__snapshots__/{paste => }/parse-two-divs.json | 0 tests/package.json | 4 ++-- 13 files changed, 3 insertions(+), 3 deletions(-) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/list-test.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-basic-block-types.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-div-with-inline-content.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-divs.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-fake-image-caption.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-image-in-paragraph.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-mixed-nested-lists.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-nested-lists-with-paragraphs.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-nested-lists.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-notion-html.json (100%) rename packages/core/src/api/parsers/html/__snapshots__/{paste => }/parse-two-divs.json (100%) diff --git a/packages/core/package.json b/packages/core/package.json index 7117fa3ee..77ae9b3cb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,7 +49,7 @@ "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release", "preview": "vite preview", "lint": "eslint src --max-warnings 0", - "test": "vitest --run", + "test": "vitest -u", "test-watch": "vitest watch", "clean": "rimraf dist && rimraf types" }, diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json b/packages/core/src/api/parsers/html/__snapshots__/list-test.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/list-test.json rename to packages/core/src/api/parsers/html/__snapshots__/list-test.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json b/packages/core/src/api/parsers/html/__snapshots__/parse-basic-block-types.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-basic-block-types.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-basic-block-types.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json b/packages/core/src/api/parsers/html/__snapshots__/parse-div-with-inline-content.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-div-with-inline-content.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-div-with-inline-content.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json b/packages/core/src/api/parsers/html/__snapshots__/parse-divs.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-divs.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-divs.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json b/packages/core/src/api/parsers/html/__snapshots__/parse-fake-image-caption.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-fake-image-caption.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-fake-image-caption.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-image-in-paragraph.json b/packages/core/src/api/parsers/html/__snapshots__/parse-image-in-paragraph.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-image-in-paragraph.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-image-in-paragraph.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/parse-mixed-nested-lists.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-mixed-nested-lists.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-mixed-nested-lists.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json b/packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists-with-paragraphs.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists-with-paragraphs.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists-with-paragraphs.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json b/packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-nested-lists.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-nested-lists.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-notion-html.json b/packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-notion-html.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-notion-html.json diff --git a/packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json b/packages/core/src/api/parsers/html/__snapshots__/parse-two-divs.json similarity index 100% rename from packages/core/src/api/parsers/html/__snapshots__/paste/parse-two-divs.json rename to packages/core/src/api/parsers/html/__snapshots__/parse-two-divs.json diff --git a/tests/package.json b/tests/package.json index f78c5857e..92b709880 100644 --- a/tests/package.json +++ b/tests/package.json @@ -5,9 +5,9 @@ "scripts": { "build": "tsc", "lint": "eslint src --max-warnings 0", - "playwright": "npx playwright test", + "playwright": "npx playwright test copypaste --headed", "playwright:ui": "npx playwright test --ui", - "test:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.44.1-focal npx playwright test", + "test:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.44.1-focal npx playwright test copypaste", "test-ct": "playwright test -c playwright-ct.config.ts --headed", "test-ct:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.35.1-focal npm install && playwright test -c playwright-ct.config.ts -u", "clean": "rimraf dist" From fc788f22fb0660b9cf48811a09f48af33b51c7b4 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 2 Oct 2024 19:03:12 +0200 Subject: [PATCH 13/17] Small fix --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 77ae9b3cb..7117fa3ee 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,7 +49,7 @@ "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release", "preview": "vite preview", "lint": "eslint src --max-warnings 0", - "test": "vitest -u", + "test": "vitest --run", "test-watch": "vitest watch", "clean": "rimraf dist && rimraf types" }, From e7bf31a6001e688dc2e48abae493f2fa300ff780 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 2 Oct 2024 19:07:33 +0200 Subject: [PATCH 14/17] Small fix --- tests/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/package.json b/tests/package.json index 92b709880..f78c5857e 100644 --- a/tests/package.json +++ b/tests/package.json @@ -5,9 +5,9 @@ "scripts": { "build": "tsc", "lint": "eslint src --max-warnings 0", - "playwright": "npx playwright test copypaste --headed", + "playwright": "npx playwright test", "playwright:ui": "npx playwright test --ui", - "test:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.44.1-focal npx playwright test copypaste", + "test:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.44.1-focal npx playwright test", "test-ct": "playwright test -c playwright-ct.config.ts --headed", "test-ct:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.35.1-focal npm install && playwright test -c playwright-ct.config.ts -u", "clean": "rimraf dist" From 4d545ef80bea8ffcb62590116c54077e53c84670 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Wed, 2 Oct 2024 19:18:25 +0200 Subject: [PATCH 15/17] Removed TODOs --- packages/core/src/api/clipboard/clipboard.test.ts | 2 -- packages/core/src/api/parsers/html/parseHTML.test.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/core/src/api/clipboard/clipboard.test.ts b/packages/core/src/api/clipboard/clipboard.test.ts index 48514be63..cd9413a7f 100644 --- a/packages/core/src/api/clipboard/clipboard.test.ts +++ b/packages/core/src/api/clipboard/clipboard.test.ts @@ -215,8 +215,6 @@ describe("Test ProseMirror selection clipboard HTML", () => { const testCases: SelectionTestCase[] = [ // TODO: Consider adding test cases for nested blocks & double nested blocks. - // TODO: Add test case for copying 2 paragraphs as this was a bug in the past. - // TODO: Add test case for copying multiple list items as this was a bug in the past. // Selection spans all of first heading's children. { testName: "multipleChildren", diff --git a/packages/core/src/api/parsers/html/parseHTML.test.ts b/packages/core/src/api/parsers/html/parseHTML.test.ts index d31986d35..389d1b745 100644 --- a/packages/core/src/api/parsers/html/parseHTML.test.ts +++ b/packages/core/src/api/parsers/html/parseHTML.test.ts @@ -17,8 +17,6 @@ async function parseHTMLAndCompareSnapshots( snapshotPath ); - // TODO: I don't think is really related to parsing. It's paste behaviour - // which is already tested in the clipboard tests. // Now, we also want to test actually pasting in the editor, and not just calling // tryParseHTMLToBlocks directly. // The reason is that the prosemirror logic for pasting can be a bit different, because From afadc58581e268ab1f05dea0562ba5dfaf52eb4f Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 3 Oct 2024 05:49:01 +0200 Subject: [PATCH 16/17] fix paste --- packages/core/src/api/parsers/pasteExtension.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/src/api/parsers/pasteExtension.ts b/packages/core/src/api/parsers/pasteExtension.ts index 97cfd751c..6fb255e2f 100644 --- a/packages/core/src/api/parsers/pasteExtension.ts +++ b/packages/core/src/api/parsers/pasteExtension.ts @@ -3,9 +3,9 @@ import { Plugin } from "prosemirror-state"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import { acceptedMIMETypes } from "./acceptedMIMETypes"; import { handleFileInsertion } from "./handleFileInsertion"; import { nestedListsToBlockNoteStructure } from "./html/util/nestedLists"; -import { acceptedMIMETypes } from "./acceptedMIMETypes"; export const createPasteFromClipboardExtension = < BSchema extends BlockSchema, @@ -28,14 +28,14 @@ export const createPasteFromClipboardExtension = < return; } - let format: (typeof acceptedMIMETypes)[number] | null = null; + let format: (typeof acceptedMIMETypes)[number] | undefined; for (const mimeType of acceptedMIMETypes) { if (event.clipboardData!.types.includes(mimeType)) { format = mimeType; break; } } - if (format === null) { + if (!format) { return true; } @@ -46,12 +46,19 @@ export const createPasteFromClipboardExtension = < let data = event.clipboardData!.getData(format); + if (format === "blocknote/html") { + editor._tiptapEditor.view.pasteHTML(data); + return true; + } + if (format === "text/html") { const htmlNode = nestedListsToBlockNoteStructure(data.trim()); data = htmlNode.innerHTML; + editor._tiptapEditor.view.pasteHTML(data); + return true; } - editor._tiptapEditor.view.pasteHTML(data); + editor._tiptapEditor.view.pasteText(data); return true; }, From 817e3aa667632cc57192d99d9e8c027f4e5e932b Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Thu, 3 Oct 2024 11:54:06 +0200 Subject: [PATCH 17/17] Implemented PR feedback --- .../core/src/api/clipboard/clipboard.test.ts | 32 +- .../clipboard/toClipboard/copyExtension.ts | 30 +- .../__snapshots__/pasted/complex.json | 319 ++++++++++++++++++ .../__snapshots__/pasted/issue-226-1.json | 81 +++++ .../__snapshots__/pasted/issue-226-2.json | 165 +++++++++ .../markdown/__snapshots__/pasted/nested.json | 81 +++++ .../__snapshots__/pasted/non-nested.json | 81 +++++ .../markdown/__snapshots__/pasted/styled.json | 61 ++++ .../parsers/markdown/parseMarkdown.test.ts | 15 + packages/core/src/api/testUtil/paste.ts | 46 +++ .../src/extensions/SideMenu/SideMenuPlugin.ts | 13 +- 11 files changed, 877 insertions(+), 47 deletions(-) create mode 100644 packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json create mode 100644 packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json create mode 100644 packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json create mode 100644 packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json create mode 100644 packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json create mode 100644 packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json create mode 100644 packages/core/src/api/testUtil/paste.ts diff --git a/packages/core/src/api/clipboard/clipboard.test.ts b/packages/core/src/api/clipboard/clipboard.test.ts index cd9413a7f..58cf9e892 100644 --- a/packages/core/src/api/clipboard/clipboard.test.ts +++ b/packages/core/src/api/clipboard/clipboard.test.ts @@ -1,11 +1,11 @@ import { Node } from "prosemirror-model"; import { NodeSelection, Selection, TextSelection } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; -import * as pmView from "prosemirror-view"; 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 { selectedFragmentToHTML } from "./toClipboard/copyExtension"; @@ -173,22 +173,6 @@ describe("Test ProseMirror selection clipboard HTML", () => { ) ); - if ( - "node" in editor._tiptapEditor.view.state.selection && - (editor._tiptapEditor.view.state.selection.node as Node).type.spec - .group === "blockContent" - ) { - editor.dispatch( - editor._tiptapEditor.state.tr.setSelection( - new NodeSelection( - editor._tiptapEditor.view.state.doc.resolve( - editor._tiptapEditor.view.state.selection.from - 1 - ) - ) - ) - ); - } - const { clipboardHTML, externalHTML } = await selectedFragmentToHTML( editor._tiptapEditor.view, editor @@ -199,14 +183,12 @@ describe("Test ProseMirror selection clipboard HTML", () => { ); const originalDocument = editor.document; - editor._tiptapEditor.state.tr.replaceSelection( - (pmView as any).__parseFromClipboard( - editor._tiptapEditor.view, - "text", - clipboardHTML, - false, - editor._tiptapEditor.view.state.selection.$from - ) + doPaste( + editor._tiptapEditor.view, + "text", + clipboardHTML, + false, + new ClipboardEvent("paste") ); const newDocument = editor.document; diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 525fe174a..ed3446ac3 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -23,6 +23,21 @@ export async function selectedFragmentToHTML< 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, @@ -88,21 +103,6 @@ const copyToClipboard = < event.preventDefault(); event.clipboardData!.clearData(); - // 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)) - ) - ); - } - (async () => { const { clipboardHTML, externalHTML, markdown } = await selectedFragmentToHTML(view, editor); diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json new file mode 100644 index 000000000..e0d619a9e --- /dev/null +++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/complex.json @@ -0,0 +1,319 @@ +[ + { + "id": "19", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "# Heading 1", + "styles": {} + } + ], + "children": [] + }, + { + "id": "20", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "## Heading 2", + "styles": {} + } + ], + "children": [] + }, + { + "id": "21", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "### Heading 3", + "styles": {} + } + ], + "children": [] + }, + { + "id": "22", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "23", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "P**ara***grap*h", + "styles": {} + } + ], + "children": [] + }, + { + "id": "24", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "P*ara*~~grap~~h", + "styles": {} + } + ], + "children": [] + }, + { + "id": "25", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "26", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "27", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " * Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "28", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " * Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "29", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "30", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " 1. Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "31", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " 2. Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "32", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " 3. Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "33", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " 1. Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "34", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " * Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "35", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " * Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "36", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "37", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json new file mode 100644 index 000000000..3c17433c1 --- /dev/null +++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-1.json @@ -0,0 +1,81 @@ +[ + { + "id": "5", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "- 📝 item1", + "styles": {} + } + ], + "children": [] + }, + { + "id": "6", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "- ⚙️ item2", + "styles": {} + } + ], + "children": [] + }, + { + "id": "7", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "- 🔗 item3", + "styles": {} + } + ], + "children": [] + }, + { + "id": "8", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "# h1", + "styles": {} + } + ], + "children": [] + }, + { + "id": "9", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json new file mode 100644 index 000000000..3ee19cf3e --- /dev/null +++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/issue-226-2.json @@ -0,0 +1,165 @@ +[ + { + "id": "9", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* a", + "styles": {} + } + ], + "children": [] + }, + { + "id": "10", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* b", + "styles": {} + } + ], + "children": [] + }, + { + "id": "11", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* c", + "styles": {} + } + ], + "children": [] + }, + { + "id": "12", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* d", + "styles": {} + } + ], + "children": [] + }, + { + "id": "13", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "anything", + "styles": {} + } + ], + "children": [] + }, + { + "id": "14", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "[a link](", + "styles": {} + }, + { + "type": "link", + "href": "http://example.com", + "content": [ + { + "type": "text", + "text": "http://example.com", + "styles": {} + } + ] + }, + { + "type": "text", + "text": ")", + "styles": {} + } + ], + "children": [] + }, + { + "id": "15", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* another", + "styles": {} + } + ], + "children": [] + }, + { + "id": "16", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* list", + "styles": {} + } + ], + "children": [] + }, + { + "id": "17", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json new file mode 100644 index 000000000..84e352628 --- /dev/null +++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/nested.json @@ -0,0 +1,81 @@ +[ + { + "id": "5", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "# Heading", + "styles": {} + } + ], + "children": [] + }, + { + "id": "6", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "7", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "8", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": " 1. Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "9", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json new file mode 100644 index 000000000..f72ab28fa --- /dev/null +++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/non-nested.json @@ -0,0 +1,81 @@ +[ + { + "id": "5", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "# Heading", + "styles": {} + } + ], + "children": [] + }, + { + "id": "6", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Paragraph", + "styles": {} + } + ], + "children": [] + }, + { + "id": "7", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "* Bullet List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "8", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "1. Numbered List Item", + "styles": {} + } + ], + "children": [] + }, + { + "id": "9", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json new file mode 100644 index 000000000..5d3c67820 --- /dev/null +++ b/packages/core/src/api/parsers/markdown/__snapshots__/pasted/styled.json @@ -0,0 +1,61 @@ +[ + { + "id": "2", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [ + { + "type": "text", + "text": "Bold", + "styles": { + "bold": true + } + }, + { + "type": "text", + "text": " ", + "styles": {} + }, + { + "type": "text", + "text": "Italic", + "styles": { + "italic": true + } + }, + { + "type": "text", + "text": " ", + "styles": {} + }, + { + "type": "text", + "text": "Strikethrough", + "styles": { + "strike": true + } + }, + { + "type": "text", + "text": " ***Multiple***", + "styles": {} + } + ], + "children": [] + }, + { + "id": "3", + "type": "paragraph", + "props": { + "textColor": "default", + "backgroundColor": "default", + "textAlignment": "left" + }, + "content": [], + "children": [] + } +] \ No newline at end of file diff --git a/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts b/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts index b8ef953af..16e12b0dc 100644 --- a/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts +++ b/packages/core/src/api/parsers/markdown/parseMarkdown.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { BlockNoteEditor } from "../../.."; +import { doPaste } from "../../testUtil/paste"; async function parseMarkdownAndCompareSnapshots( md: string, @@ -14,6 +15,20 @@ async function parseMarkdownAndCompareSnapshots( expect(JSON.stringify(blocks, undefined, 2)).toMatchFileSnapshot( snapshotPath ); + + doPaste( + editor._tiptapEditor.view, + md, + null, + true, + new ClipboardEvent("paste") + ); + + const pastedSnapshotPath = "./__snapshots__/pasted/" + snapshotName + ".json"; + expect(JSON.stringify(editor.document, undefined, 2)).toMatchFileSnapshot( + pastedSnapshotPath + ); + editor.mount(undefined); } diff --git a/packages/core/src/api/testUtil/paste.ts b/packages/core/src/api/testUtil/paste.ts new file mode 100644 index 000000000..a25874313 --- /dev/null +++ b/packages/core/src/api/testUtil/paste.ts @@ -0,0 +1,46 @@ +import { Slice } from "@tiptap/pm/model"; +import { EditorView } from "@tiptap/pm/view"; +import * as pmView from "@tiptap/pm/view"; + +function sliceSingleNode(slice: Slice) { + return slice.openStart === 0 && + slice.openEnd === 0 && + slice.content.childCount === 1 + ? slice.content.firstChild + : null; +} + +// This function is a copy of the `doPaste` function from `@tiptap/pm/view`, +// but made to work in a JSDOM environment. To do this, the `tr.scrollIntoView` +// call has been removed. +// https://github.com/ProseMirror/prosemirror-view/blob/17b508f618c944c54776f8ddac45edcb49970796/src/input.ts#L624 +export function doPaste( + view: EditorView, + text: string, + html: string | null, + preferPlain: boolean, + event: ClipboardEvent +) { + const slice = (pmView as any).__parseFromClipboard( + view, + text, + html, + preferPlain, + view.state.selection.$from + ); + if ( + view.someProp("handlePaste", (f) => f(view, event, slice || Slice.empty)) + ) { + return true; + } + if (!slice) { + return false; + } + + const singleNode = sliceSingleNode(slice); + const tr = singleNode + ? view.state.tr.replaceSelectionWith(singleNode, preferPlain) + : view.state.tr.replaceSelection(slice); + view.dispatch(tr.setMeta("paste", true).setMeta("uiEvent", "paste")); + return true; +} diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 6bc5cc7cd..bc7b310b5 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -2,9 +2,9 @@ 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 { createExternalHTMLExporter } from "../../api/exporters/html/externalHTMLExporter"; -import { createInternalHTMLSerializer } from "../../api/exporters/html/internalHTMLSerializer"; import { cleanHTMLToMarkdown } from "../../api/exporters/markdown/markdownExporter"; import { getBlockInfoFromPos } from "../../api/getBlockInfoFromPos"; import { Block } from "../../blocks/defaultBlocks"; @@ -227,11 +227,10 @@ function dragStart< const selectedSlice = view.state.selection.content(); const schema = editor.pmSchema; - const internalHTMLSerializer = createInternalHTMLSerializer(schema, editor); - const internalHTML = internalHTMLSerializer.serializeProseMirrorFragment( - selectedSlice.content, - {} - ); + const clipboardHML = (pmView as any).__serializeForClipboard( + view, + selectedSlice + ).dom.innerHTML; const externalHTMLExporter = createExternalHTMLExporter(schema, editor); const externalHTML = externalHTMLExporter.exportProseMirrorFragment( @@ -242,7 +241,7 @@ function dragStart< const plainText = cleanHTMLToMarkdown(externalHTML); e.dataTransfer.clearData(); - e.dataTransfer.setData("blocknote/html", internalHTML); + e.dataTransfer.setData("blocknote/html", clipboardHML); e.dataTransfer.setData("text/html", externalHTML); e.dataTransfer.setData("text/plain", plainText); e.dataTransfer.effectAllowed = "move";