Skip to content

feat: HTML paste handling #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"rehype-parse": "^8.0.4",
"rehype-remark": "^9.1.2",
"rehype-stringify": "^9.0.3",
"rehype-format":"^5.0.0",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
Expand Down
159 changes: 116 additions & 43 deletions packages/core/src/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Editor, EditorOptions, Extension } from "@tiptap/core";
import { Node } from "prosemirror-model";
import { Fragment, Node, Slice } from "prosemirror-model";
// import "./blocknote.css";
import { Editor as TiptapEditor } from "@tiptap/core/dist/packages/core/src/Editor";
import * as Y from "yjs";
Expand All @@ -24,7 +24,6 @@ import {
BlockSpecs,
PartialBlock,
} from "./extensions/Blocks/api/blocks/types";
import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes";
import {
DefaultBlockSchema,
DefaultInlineContentSchema,
Expand All @@ -44,8 +43,14 @@ import {
import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos";

import "prosemirror-tables/style/tables.css";

import { createExternalHTMLExporter } from "./api/exporters/html/externalHTMLExporter";
import { blocksToMarkdown } from "./api/exporters/markdown/markdownExporter";
import { HTMLToBlocks } from "./api/parsers/html/parseHTML";
import { markdownToBlocks } from "./api/parsers/markdown/parseMarkdown";
import "./editor.css";
import { getBlockSchemaFromSpecs } from "./extensions/Blocks/api/blocks/internal";
import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes";
import { getInlineContentSchemaFromSpecs } from "./extensions/Blocks/api/inlineContent/internal";
import {
InlineContentSchema,
Expand Down Expand Up @@ -409,6 +414,50 @@ export class BlockNoteEditor<
newOptions.domAttributes?.editor?.class || ""
),
},
transformPasted(slice, view) {
// helper function
function removeChild(node: Fragment, n: number) {
const children: any[] = [];
node.forEach((child, _, i) => {
if (i !== n) {
children.push(child);
}
});
return Fragment.from(children);
}

// fix for https://github.com/ProseMirror/prosemirror/issues/1430#issuecomment-1822570821
let f = Fragment.from(slice.content);
for (let i = 0; i < f.childCount; i++) {
if (f.child(i).type.spec.group === "blockContent") {
const content = [f.child(i)];
if (i + 1 < f.childCount) {
// when there is a blockGroup, it should be nested in the new blockcontainer
if (f.child(i + 1).type.spec.group === "blockGroup") {
const nestedChild = f
.child(i + 1)
.child(0)
.child(0);

if (
nestedChild.type.name === "bulletListItem" ||
nestedChild.type.name === "numberedListItem"
) {
content.push(f.child(i + 1));
f = removeChild(f, i + 1);
}
}
}
const container = view.state.schema.nodes.blockContainer.create(
undefined,
content
);
f = f.replaceChild(i, container);
}
}

return new Slice(f, slice.openStart, slice.openEnd);
},
},
};

Expand Down Expand Up @@ -942,47 +991,71 @@ export class BlockNoteEditor<
}

// TODO: Fix when implementing HTML/Markdown import & export
// /**
// * Serializes blocks into an HTML string. To better conform to HTML standards, children of blocks which aren't list
// * items are un-nested in the output HTML.
// * @param blocks An array of blocks that should be serialized into HTML.
// * @returns The blocks, serialized as an HTML string.
// */
// public async blocksToHTML(blocks: Block<BSchema>[]): Promise<string> {
// return blocksToHTML(blocks, this._tiptapEditor.schema, this);
// }
//
// /**
// * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and
// * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote
// * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text.
// * @param html The HTML string to parse blocks from.
// * @returns The blocks parsed from the HTML string.
// */
// public async HTMLToBlocks(html: string): Promise<Block<BSchema>[]> {
// return HTMLToBlocks(html, this.schema, this._tiptapEditor.schema);
// }
//
// /**
// * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of
// * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed.
// * @param blocks An array of blocks that should be serialized into Markdown.
// * @returns The blocks, serialized as a Markdown string.
// */
// public async blocksToMarkdown(blocks: Block<BSchema>[]): Promise<string> {
// return blocksToMarkdown(blocks, this._tiptapEditor.schema, this);
// }
//
// /**
// * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on
// * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it
// * as text.
// * @param markdown The Markdown string to parse blocks from.
// * @returns The blocks parsed from the Markdown string.
// */
// public async markdownToBlocks(markdown: string): Promise<Block<BSchema>[]> {
// return markdownToBlocks(markdown, this.schema, this._tiptapEditor.schema);
// }
/**
* Serializes blocks into an HTML string. To better conform to HTML standards, children of blocks which aren't list
* items are un-nested in the output HTML.
* @param blocks An array of blocks that should be serialized into HTML.
* @returns The blocks, serialized as an HTML string.
*/
public async blocksToHTMLLossy(
blocks = this.topLevelBlocks
): Promise<string> {
const exporter = createExternalHTMLExporter(
this._tiptapEditor.schema,
this
);
return exporter.exportBlocks(blocks);
}

/**
* Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and
* `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote
* doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text.
* @param html The HTML string to parse blocks from.
* @returns The blocks parsed from the HTML string.
*/
public async tryParseHTMLToBlocks(
html: string
): Promise<Block<BSchema, ISchema, SSchema>[]> {
return HTMLToBlocks(
html,
this.blockSchema,
this.inlineContentSchema,
this.styleSchema,
this._tiptapEditor.schema
);
}

/**
* Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of
* BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed.
* @param blocks An array of blocks that should be serialized into Markdown.
* @returns The blocks, serialized as a Markdown string.
*/
public async blocksToMarkdownLossy(
blocks = this.topLevelBlocks
): Promise<string> {
return blocksToMarkdown(blocks, this._tiptapEditor.schema, this);
}

/**
* Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on
* Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it
* as text.
* @param markdown The Markdown string to parse blocks from.
* @returns The blocks parsed from the Markdown string.
*/
public async tryParseMarkdownToBlocks(
markdown: string
): Promise<Block<BSchema, ISchema, SSchema>[]> {
return markdownToBlocks(
markdown,
this.blockSchema,
this.inlineContentSchema,
this.styleSchema,
this._tiptapEditor.schema
);
}

/**
* Updates the user info for the current user that's shown to other collaborators.
Expand Down
Loading