@blocknote/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocknote.js +12508 -1276
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +50 -1
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +16 -5
- package/src/BlockNoteEditor.test.ts +12 -0
- package/src/BlockNoteEditor.ts +38 -15
- package/src/BlockNoteExtensions.ts +25 -21
- package/src/api/Editor.ts +142 -0
- package/src/api/blockManipulation/blockManipulation.ts +114 -0
- package/src/api/formatConversions/formatConversions.ts +86 -0
- package/src/api/formatConversions/removeUnderlinesRehypePlugin.ts +39 -0
- package/src/api/formatConversions/simplifyBlocksRehypePlugin.ts +125 -0
- package/src/api/nodeConversions/nodeConversions.ts +170 -0
- package/src/editor.module.css +7 -1
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +7 -1
- package/src/extensions/Blocks/api/blockTypes.ts +85 -0
- package/src/extensions/Blocks/api/cursorPositionTypes.ts +5 -0
- package/src/extensions/Blocks/api/inlineContentTypes.ts +44 -0
- package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +4 -4
- package/src/extensions/Blocks/nodes/BlockContainer.ts +75 -25
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +23 -5
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +28 -6
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +2 -2
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +3 -3
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +7 -12
- package/src/extensions/SlashMenu/SlashMenuItem.ts +4 -1
- package/src/extensions/SlashMenu/{defaultCommands.tsx → defaultSlashCommands.tsx} +34 -17
- package/src/extensions/SlashMenu/index.ts +7 -4
- package/src/extensions/UniqueID/UniqueID.ts +1 -1
- package/src/index.ts +4 -2
- package/types/src/BlockNoteEditor.d.ts +13 -4
- package/types/src/BlockNoteEditor.test.d.ts +1 -0
- package/types/src/BlockNoteExtensions.d.ts +7 -3
- package/types/src/api/Editor.d.ts +73 -0
- package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -0
- package/types/src/api/formatConversions/formatConversions.d.ts +6 -0
- package/types/src/api/formatConversions/removeUnderlinesRehypePlugin.d.ts +6 -0
- package/types/src/api/formatConversions/simplifyBlocksRehypePlugin.d.ts +16 -0
- package/types/src/api/nodeConversions/nodeConversions.d.ts +8 -0
- package/types/src/api/removeUnderlinesRehypePlugin.d.ts +6 -0
- package/types/src/api/simplifyBlocksRehypePlugin.d.ts +16 -0
- package/types/src/extensions/Blocks/api/apiTypes.d.ts +18 -0
- package/types/src/extensions/Blocks/api/blockTypes.d.ts +36 -0
- package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +4 -0
- package/types/src/extensions/Blocks/api/inlineContentTypes.d.ts +23 -0
- package/types/src/extensions/Blocks/api/styleTypes.d.ts +22 -0
- package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +3 -3
- package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -2
- package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +1 -3
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -1
- package/types/src/extensions/SlashMenu/index.d.ts +3 -3
- package/types/src/index.d.ts +4 -2
- package/src/extensions/Blocks/apiTypes.ts +0 -48
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://github.com/yousefed/blocknote",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.4.0",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
9
|
"types",
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"build": "tsc && vite build",
|
|
44
44
|
"build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release",
|
|
45
45
|
"preview": "vite preview",
|
|
46
|
-
"lint": "eslint src --max-warnings 0"
|
|
46
|
+
"lint": "eslint src --max-warnings 0",
|
|
47
|
+
"test": "vitest",
|
|
48
|
+
"test-watch": "vitest watch"
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"@emotion/cache": "^11.10.5",
|
|
@@ -66,21 +68,30 @@
|
|
|
66
68
|
"@tiptap/extension-text": "2.0.0-beta.217",
|
|
67
69
|
"@tiptap/extension-underline": "2.0.0-beta.217",
|
|
68
70
|
"@tiptap/pm": "2.0.0-beta.217",
|
|
71
|
+
"hast-util-from-dom": "^4.2.0",
|
|
69
72
|
"lodash": "^4.17.21",
|
|
73
|
+
"rehype": "^12.0.1",
|
|
74
|
+
"rehype-remark": "^9.1.2",
|
|
75
|
+
"remark": "^14.0.2",
|
|
76
|
+
"remark-gfm": "^3.0.1",
|
|
77
|
+
"remark-rehype": "^10.1.0",
|
|
70
78
|
"uuid": "^8.3.2",
|
|
71
79
|
"y-prosemirror": "1.0.20",
|
|
72
80
|
"y-protocols": "1.0.5",
|
|
73
81
|
"yjs": "13.5.44"
|
|
74
82
|
},
|
|
75
83
|
"devDependencies": {
|
|
84
|
+
"@types/hast": "^2.3.4",
|
|
76
85
|
"@types/lodash": "^4.14.179",
|
|
77
86
|
"@types/uuid": "^8.3.4",
|
|
78
87
|
"eslint": "^8.10.0",
|
|
79
88
|
"eslint-config-react-app": "^7.0.0",
|
|
89
|
+
"jsdom": "^21.1.0",
|
|
80
90
|
"prettier": "^2.7.1",
|
|
81
91
|
"typescript": "^4.5.4",
|
|
82
|
-
"vite": "^
|
|
83
|
-
"vite-plugin-eslint": "^1.
|
|
92
|
+
"vite": "^4.1.2",
|
|
93
|
+
"vite-plugin-eslint": "^1.8.1",
|
|
94
|
+
"vitest": "^0.28.5"
|
|
84
95
|
},
|
|
85
96
|
"eslintConfig": {
|
|
86
97
|
"extends": [
|
|
@@ -95,5 +106,5 @@
|
|
|
95
106
|
"access": "public",
|
|
96
107
|
"registry": "https://registry.npmjs.org/"
|
|
97
108
|
},
|
|
98
|
-
"gitHead": "
|
|
109
|
+
"gitHead": "aa88219581e5427fd86c759b827ffc2b2f3af99b"
|
|
99
110
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { BlockNoteEditor } from "./BlockNoteEditor";
|
|
3
|
+
import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @vitest-environment jsdom
|
|
7
|
+
*/
|
|
8
|
+
it("creates an editor", () => {
|
|
9
|
+
const editor = new BlockNoteEditor({});
|
|
10
|
+
const blockInfo = getBlockInfoFromPos(editor._tiptapEditor.state.doc, 2);
|
|
11
|
+
expect(blockInfo?.contentNode.type.name).toEqual("paragraph");
|
|
12
|
+
});
|
package/src/BlockNoteEditor.ts
CHANGED
|
@@ -1,54 +1,77 @@
|
|
|
1
1
|
import { Editor, EditorOptions } from "@tiptap/core";
|
|
2
2
|
|
|
3
3
|
// import "./blocknote.css";
|
|
4
|
+
import { Editor as EditorAPI } from "./api/Editor";
|
|
4
5
|
import { getBlockNoteExtensions, UiFactories } from "./BlockNoteExtensions";
|
|
5
6
|
import styles from "./editor.module.css";
|
|
7
|
+
import { defaultSlashCommands, SlashCommand } from "./extensions/SlashMenu";
|
|
6
8
|
|
|
7
|
-
export type BlockNoteEditorOptions =
|
|
9
|
+
export type BlockNoteEditorOptions = {
|
|
8
10
|
enableBlockNoteExtensions: boolean;
|
|
9
11
|
disableHistoryExtension: boolean;
|
|
10
12
|
uiFactories: UiFactories;
|
|
13
|
+
slashCommands: SlashCommand[];
|
|
14
|
+
parentElement: HTMLElement;
|
|
15
|
+
editorDOMAttributes: Record<string, string>;
|
|
16
|
+
onUpdate: () => void;
|
|
17
|
+
onCreate: () => void;
|
|
18
|
+
|
|
19
|
+
// tiptap options, undocumented
|
|
20
|
+
_tiptapOptions: any;
|
|
11
21
|
};
|
|
12
22
|
|
|
13
|
-
const
|
|
23
|
+
const blockNoteTipTapOptions = {
|
|
14
24
|
enableInputRules: true,
|
|
15
25
|
enablePasteRules: true,
|
|
16
26
|
enableCoreExtensions: false,
|
|
17
27
|
};
|
|
18
28
|
|
|
19
|
-
export class BlockNoteEditor {
|
|
20
|
-
public readonly
|
|
29
|
+
export class BlockNoteEditor extends EditorAPI {
|
|
30
|
+
public readonly _tiptapEditor: Editor & { contentComponent: any };
|
|
31
|
+
|
|
32
|
+
public get domElement() {
|
|
33
|
+
return this._tiptapEditor.view.dom as HTMLDivElement;
|
|
34
|
+
}
|
|
21
35
|
|
|
22
36
|
constructor(options: Partial<BlockNoteEditorOptions> = {}) {
|
|
23
|
-
const blockNoteExtensions = getBlockNoteExtensions(
|
|
24
|
-
options.uiFactories || {}
|
|
25
|
-
|
|
37
|
+
const blockNoteExtensions = getBlockNoteExtensions({
|
|
38
|
+
uiFactories: options.uiFactories || {},
|
|
39
|
+
slashCommands: options.slashCommands || defaultSlashCommands,
|
|
40
|
+
});
|
|
26
41
|
|
|
27
42
|
let extensions = options.disableHistoryExtension
|
|
28
43
|
? blockNoteExtensions.filter((e) => e.name !== "history")
|
|
29
44
|
: blockNoteExtensions;
|
|
30
45
|
|
|
31
|
-
const tiptapOptions = {
|
|
32
|
-
...
|
|
33
|
-
...options,
|
|
46
|
+
const tiptapOptions: EditorOptions = {
|
|
47
|
+
...blockNoteTipTapOptions,
|
|
48
|
+
...options._tiptapOptions,
|
|
49
|
+
onUpdate: () => {
|
|
50
|
+
options.onUpdate?.();
|
|
51
|
+
},
|
|
52
|
+
onCreate: () => {
|
|
53
|
+
options.onCreate?.();
|
|
54
|
+
},
|
|
34
55
|
extensions:
|
|
35
56
|
options.enableBlockNoteExtensions === false
|
|
36
|
-
? options.extensions
|
|
37
|
-
: [...(options.extensions || []), ...extensions],
|
|
57
|
+
? options._tiptapOptions?.extensions
|
|
58
|
+
: [...(options._tiptapOptions?.extensions || []), ...extensions],
|
|
38
59
|
editorProps: {
|
|
39
60
|
attributes: {
|
|
40
|
-
...(options.
|
|
61
|
+
...(options.editorDOMAttributes || {}),
|
|
41
62
|
class: [
|
|
42
63
|
styles.bnEditor,
|
|
43
64
|
styles.bnRoot,
|
|
44
|
-
|
|
65
|
+
options.editorDOMAttributes?.class || "",
|
|
45
66
|
].join(" "),
|
|
46
67
|
},
|
|
47
68
|
},
|
|
48
69
|
};
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
const _tiptapEditor = new Editor(tiptapOptions) as Editor & {
|
|
51
72
|
contentComponent: any;
|
|
52
73
|
};
|
|
74
|
+
super(_tiptapEditor);
|
|
75
|
+
this._tiptapEditor = _tiptapEditor;
|
|
53
76
|
}
|
|
54
77
|
}
|
|
@@ -7,29 +7,29 @@ import GapCursor from "@tiptap/extension-gapcursor";
|
|
|
7
7
|
import HardBreak from "@tiptap/extension-hard-break";
|
|
8
8
|
import { History } from "@tiptap/extension-history";
|
|
9
9
|
import Italic from "@tiptap/extension-italic";
|
|
10
|
+
import { Link } from "@tiptap/extension-link";
|
|
10
11
|
import Strike from "@tiptap/extension-strike";
|
|
11
12
|
import Text from "@tiptap/extension-text";
|
|
12
13
|
import Underline from "@tiptap/extension-underline";
|
|
14
|
+
import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension";
|
|
15
|
+
import { BackgroundColorMark } from "./extensions/BackgroundColor/BackgroundColorMark";
|
|
13
16
|
import { blocks } from "./extensions/Blocks";
|
|
14
17
|
import blockStyles from "./extensions/Blocks/nodes/Block.module.css";
|
|
15
|
-
import {
|
|
18
|
+
import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes";
|
|
16
19
|
import { DraggableBlocksExtension } from "./extensions/DraggableBlocks/DraggableBlocksExtension";
|
|
20
|
+
import { FormattingToolbarExtension } from "./extensions/FormattingToolbar/FormattingToolbarExtension";
|
|
21
|
+
import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes";
|
|
17
22
|
import HyperlinkMark from "./extensions/HyperlinkToolbar/HyperlinkMark";
|
|
23
|
+
import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes";
|
|
18
24
|
import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension";
|
|
19
|
-
import SlashMenuExtension from "./extensions/SlashMenu";
|
|
25
|
+
import { SlashCommand, SlashMenuExtension } from "./extensions/SlashMenu";
|
|
26
|
+
import { SlashMenuItem } from "./extensions/SlashMenu/SlashMenuItem";
|
|
27
|
+
import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension";
|
|
28
|
+
import { TextColorExtension } from "./extensions/TextColor/TextColorExtension";
|
|
29
|
+
import { TextColorMark } from "./extensions/TextColor/TextColorMark";
|
|
20
30
|
import { TrailingNode } from "./extensions/TrailingNode/TrailingNodeExtension";
|
|
21
31
|
import UniqueID from "./extensions/UniqueID/UniqueID";
|
|
22
|
-
import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes";
|
|
23
|
-
import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes";
|
|
24
32
|
import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes";
|
|
25
|
-
import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes";
|
|
26
|
-
import { Link } from "@tiptap/extension-link";
|
|
27
|
-
import { SlashMenuItem } from "./extensions/SlashMenu/SlashMenuItem";
|
|
28
|
-
import { BackgroundColorMark } from "./extensions/BackgroundColor/BackgroundColorMark";
|
|
29
|
-
import { TextColorMark } from "./extensions/TextColor/TextColorMark";
|
|
30
|
-
import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension";
|
|
31
|
-
import { TextColorExtension } from "./extensions/TextColor/TextColorExtension";
|
|
32
|
-
import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension";
|
|
33
33
|
|
|
34
34
|
export type UiFactories = Partial<{
|
|
35
35
|
formattingToolbarFactory: FormattingToolbarFactory;
|
|
@@ -41,7 +41,10 @@ export type UiFactories = Partial<{
|
|
|
41
41
|
/**
|
|
42
42
|
* Get all the Tiptap extensions BlockNote is configured with by default
|
|
43
43
|
*/
|
|
44
|
-
export const getBlockNoteExtensions = (
|
|
44
|
+
export const getBlockNoteExtensions = (opts: {
|
|
45
|
+
uiFactories: UiFactories;
|
|
46
|
+
slashCommands: SlashCommand[];
|
|
47
|
+
}) => {
|
|
45
48
|
const ret: Extensions = [
|
|
46
49
|
extensions.ClipboardTextSerializer,
|
|
47
50
|
extensions.Commands,
|
|
@@ -91,36 +94,37 @@ export const getBlockNoteExtensions = (uiFactories: UiFactories) => {
|
|
|
91
94
|
TrailingNode,
|
|
92
95
|
];
|
|
93
96
|
|
|
94
|
-
if (uiFactories.blockSideMenuFactory) {
|
|
97
|
+
if (opts.uiFactories.blockSideMenuFactory) {
|
|
95
98
|
ret.push(
|
|
96
99
|
DraggableBlocksExtension.configure({
|
|
97
|
-
blockSideMenuFactory: uiFactories.blockSideMenuFactory,
|
|
100
|
+
blockSideMenuFactory: opts.uiFactories.blockSideMenuFactory,
|
|
98
101
|
})
|
|
99
102
|
);
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
if (uiFactories.formattingToolbarFactory) {
|
|
105
|
+
if (opts.uiFactories.formattingToolbarFactory) {
|
|
103
106
|
ret.push(
|
|
104
107
|
FormattingToolbarExtension.configure({
|
|
105
|
-
formattingToolbarFactory: uiFactories.formattingToolbarFactory,
|
|
108
|
+
formattingToolbarFactory: opts.uiFactories.formattingToolbarFactory,
|
|
106
109
|
})
|
|
107
110
|
);
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
if (uiFactories.hyperlinkToolbarFactory) {
|
|
113
|
+
if (opts.uiFactories.hyperlinkToolbarFactory) {
|
|
111
114
|
ret.push(
|
|
112
115
|
HyperlinkMark.configure({
|
|
113
|
-
hyperlinkToolbarFactory: uiFactories.hyperlinkToolbarFactory,
|
|
116
|
+
hyperlinkToolbarFactory: opts.uiFactories.hyperlinkToolbarFactory,
|
|
114
117
|
})
|
|
115
118
|
);
|
|
116
119
|
} else {
|
|
117
120
|
ret.push(Link);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
if (uiFactories.slashMenuFactory) {
|
|
123
|
+
if (opts.uiFactories.slashMenuFactory) {
|
|
121
124
|
ret.push(
|
|
122
125
|
SlashMenuExtension.configure({
|
|
123
|
-
|
|
126
|
+
commands: opts.slashCommands,
|
|
127
|
+
slashMenuFactory: opts.uiFactories.slashMenuFactory,
|
|
124
128
|
})
|
|
125
129
|
);
|
|
126
130
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Editor as TiptapEditor } from "@tiptap/core";
|
|
2
|
+
import { Node } from "prosemirror-model";
|
|
3
|
+
import { getBlockInfoFromPos } from "../extensions/Blocks/helpers/getBlockInfoFromPos";
|
|
4
|
+
import { Block, PartialBlock } from "../extensions/Blocks/api/blockTypes";
|
|
5
|
+
import { TextCursorPosition } from "../extensions/Blocks/api/cursorPositionTypes";
|
|
6
|
+
import { nodeToBlock } from "./nodeConversions/nodeConversions";
|
|
7
|
+
import {
|
|
8
|
+
insertBlocks,
|
|
9
|
+
removeBlocks,
|
|
10
|
+
replaceBlocks,
|
|
11
|
+
updateBlock,
|
|
12
|
+
} from "./blockManipulation/blockManipulation";
|
|
13
|
+
import {
|
|
14
|
+
blocksToHTML,
|
|
15
|
+
blocksToMarkdown,
|
|
16
|
+
HTMLToBlocks,
|
|
17
|
+
markdownToBlocks,
|
|
18
|
+
} from "./formatConversions/formatConversions";
|
|
19
|
+
|
|
20
|
+
export class Editor {
|
|
21
|
+
constructor(
|
|
22
|
+
private tiptapEditor: TiptapEditor,
|
|
23
|
+
private blockCache = new WeakMap<Node, Block>()
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets a list of all top-level blocks that are in the editor.
|
|
28
|
+
*/
|
|
29
|
+
public get allBlocks(): Block[] {
|
|
30
|
+
const blocks: Block[] = [];
|
|
31
|
+
|
|
32
|
+
this.tiptapEditor.state.doc.firstChild!.descendants((node) => {
|
|
33
|
+
blocks.push(nodeToBlock(node, this.blockCache));
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return blocks;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets information regarding the position of the text cursor in the editor.
|
|
43
|
+
*/
|
|
44
|
+
public get textCursorPosition(): TextCursorPosition {
|
|
45
|
+
const { node } = getBlockInfoFromPos(
|
|
46
|
+
this.tiptapEditor.state.doc,
|
|
47
|
+
this.tiptapEditor.state.selection.from
|
|
48
|
+
)!;
|
|
49
|
+
|
|
50
|
+
return { block: nodeToBlock(node, this.blockCache) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Inserts multiple blocks before, after, or nested inside an existing block in the editor.
|
|
55
|
+
* @param blocksToInsert An array of blocks to insert.
|
|
56
|
+
* @param blockToInsertAt An existing block, marking where the new blocks should be inserted at.
|
|
57
|
+
* @param placement Determines whether the blocks should be inserted just before, just after, or nested inside the
|
|
58
|
+
* existing block.
|
|
59
|
+
*/
|
|
60
|
+
public insertBlocks(
|
|
61
|
+
blocksToInsert: PartialBlock[],
|
|
62
|
+
blockToInsertAt: Block,
|
|
63
|
+
placement: "before" | "after" | "nested" = "before"
|
|
64
|
+
): void {
|
|
65
|
+
insertBlocks(blocksToInsert, blockToInsertAt, placement, this.tiptapEditor);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Updates a block in the editor to the given specification.
|
|
70
|
+
* @param blockToUpdate The block that should be updated.
|
|
71
|
+
* @param updatedBlock The specification that the block should be updated to.
|
|
72
|
+
*/
|
|
73
|
+
public updateBlock(blockToUpdate: Block, updatedBlock: PartialBlock) {
|
|
74
|
+
updateBlock(blockToUpdate, updatedBlock, this.tiptapEditor);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Removes multiple blocks from the editor. Throws an error if any of the blocks could not be found.
|
|
79
|
+
* @param blocksToRemove An array of blocks that should be removed.
|
|
80
|
+
*/
|
|
81
|
+
public removeBlocks(blocksToRemove: Block[]) {
|
|
82
|
+
removeBlocks(blocksToRemove, this.tiptapEditor);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Replaces multiple blocks in the editor with several other blocks. If the provided blocks to remove are not adjacent
|
|
87
|
+
* to each other, the new blocks are inserted at the position of the first block in the array. Throws an error if any
|
|
88
|
+
* of the blocks could not be found.
|
|
89
|
+
* @param blocksToRemove An array of blocks that should be replaced.
|
|
90
|
+
* @param blocksToInsert An array of blocks to replace the old ones with.
|
|
91
|
+
*/
|
|
92
|
+
public replaceBlocks(
|
|
93
|
+
blocksToRemove: Block[],
|
|
94
|
+
blocksToInsert: PartialBlock[]
|
|
95
|
+
) {
|
|
96
|
+
replaceBlocks(blocksToRemove, blocksToInsert, this.tiptapEditor);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Executes a callback function whenever the editor's content changes.
|
|
101
|
+
* @param callback The callback function to execute.
|
|
102
|
+
*/
|
|
103
|
+
public onContentChange(callback: () => void) {
|
|
104
|
+
this.tiptapEditor.on("update", callback);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Serializes a list of blocks into an HTML string. The output is not the same as what's rendered by the editor, and
|
|
109
|
+
* is simplified in order to better conform to HTML standards. Block structuring elements are removed, children of
|
|
110
|
+
* blocks which aren't list items are lifted out of them, and list items blocks are wrapped in `ul`/`ol` tags.
|
|
111
|
+
* @param blocks The list of blocks to serialize into HTML.
|
|
112
|
+
*/
|
|
113
|
+
public async blocksToHTML(blocks: Block[]): Promise<string> {
|
|
114
|
+
return blocksToHTML(blocks, this.tiptapEditor.schema);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a list of blocks from an HTML string.
|
|
119
|
+
* @param htmlString The HTML string to create a list of blocks from.
|
|
120
|
+
*/
|
|
121
|
+
public async HTMLToBlocks(htmlString: string): Promise<Block[]> {
|
|
122
|
+
return HTMLToBlocks(htmlString, this.tiptapEditor.schema);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Serializes a list of blocks into a Markdown string. The output is simplified as Markdown does not support all
|
|
127
|
+
* features of BlockNote. Block structuring elements are removed, children of blocks which aren't list items are
|
|
128
|
+
* lifted out of them, and certain styles are removed.
|
|
129
|
+
* @param blocks The list of blocks to serialize into Markdown.
|
|
130
|
+
*/
|
|
131
|
+
public async blocksToMarkdown(blocks: Block[]): Promise<string> {
|
|
132
|
+
return blocksToMarkdown(blocks, this.tiptapEditor.schema);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a list of blocks from a Markdown string.
|
|
137
|
+
* @param markdownString The Markdown string to create a list of blocks from.
|
|
138
|
+
*/
|
|
139
|
+
public async markdownToBlocks(markdownString: string): Promise<Block[]> {
|
|
140
|
+
return markdownToBlocks(markdownString, this.tiptapEditor.schema);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Editor } from "@tiptap/core";
|
|
2
|
+
import { Node } from "prosemirror-model";
|
|
3
|
+
import { Block, PartialBlock } from "../../extensions/Blocks/api/blockTypes";
|
|
4
|
+
import { blockToNode, getNodeById } from "../nodeConversions/nodeConversions";
|
|
5
|
+
|
|
6
|
+
export function insertBlocks(
|
|
7
|
+
blocksToInsert: PartialBlock[],
|
|
8
|
+
blockToInsertAt: Block,
|
|
9
|
+
placement: "before" | "after" | "nested" = "before",
|
|
10
|
+
editor: Editor
|
|
11
|
+
): void {
|
|
12
|
+
const nodesToInsert: Node[] = [];
|
|
13
|
+
for (const blockSpec of blocksToInsert) {
|
|
14
|
+
nodesToInsert.push(blockToNode(blockSpec, editor.schema));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let insertionPos = -1;
|
|
18
|
+
|
|
19
|
+
const { node, posBeforeNode } = getNodeById(
|
|
20
|
+
blockToInsertAt.id,
|
|
21
|
+
editor.state.doc
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (placement === "before") {
|
|
25
|
+
insertionPos = posBeforeNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (placement === "after") {
|
|
29
|
+
insertionPos = posBeforeNode + node.nodeSize;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (placement === "nested") {
|
|
33
|
+
// Case if block doesn't already have children.
|
|
34
|
+
if (node.childCount < 2) {
|
|
35
|
+
insertionPos = posBeforeNode + node.firstChild!.nodeSize + 1;
|
|
36
|
+
|
|
37
|
+
const blockGroupNode = editor.state.schema.nodes["blockGroup"].create(
|
|
38
|
+
{},
|
|
39
|
+
nodesToInsert
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
editor.view.dispatch(
|
|
43
|
+
editor.state.tr.insert(insertionPos, blockGroupNode)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
insertionPos = posBeforeNode + node.firstChild!.nodeSize + 2;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
editor.view.dispatch(editor.state.tr.insert(insertionPos, nodesToInsert));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function updateBlock(
|
|
56
|
+
blockToUpdate: Block,
|
|
57
|
+
updatedBlock: PartialBlock,
|
|
58
|
+
editor: Editor
|
|
59
|
+
) {
|
|
60
|
+
const { posBeforeNode } = getNodeById(blockToUpdate.id, editor.state.doc);
|
|
61
|
+
|
|
62
|
+
editor.commands.BNUpdateBlock(posBeforeNode + 1, updatedBlock);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function removeBlocks(blocksToRemove: Block[], editor: Editor) {
|
|
66
|
+
const idsOfBlocksToRemove = new Set<string>(
|
|
67
|
+
blocksToRemove.map((block) => block.id)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let removedSize = 0;
|
|
71
|
+
|
|
72
|
+
editor.state.doc.descendants((node, pos) => {
|
|
73
|
+
// Skips traversing nodes after all target blocks have been removed.
|
|
74
|
+
if (idsOfBlocksToRemove.size === 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Keeps traversing nodes if block with target ID has not been found.
|
|
79
|
+
if (
|
|
80
|
+
node.type.name !== "blockContainer" ||
|
|
81
|
+
!idsOfBlocksToRemove.has(node.attrs.id)
|
|
82
|
+
) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
idsOfBlocksToRemove.delete(node.attrs.id);
|
|
87
|
+
const oldDocSize = editor.state.doc.nodeSize;
|
|
88
|
+
|
|
89
|
+
editor.commands.BNDeleteBlock(pos - removedSize + 1);
|
|
90
|
+
|
|
91
|
+
const newDocSize = editor.state.doc.nodeSize;
|
|
92
|
+
removedSize += oldDocSize - newDocSize;
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (idsOfBlocksToRemove.size > 0) {
|
|
98
|
+
let notFoundIds = [...idsOfBlocksToRemove].join("\n");
|
|
99
|
+
|
|
100
|
+
throw Error(
|
|
101
|
+
"Blocks with the following IDs could not be found in the editor: " +
|
|
102
|
+
notFoundIds
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function replaceBlocks(
|
|
108
|
+
blocksToRemove: Block[],
|
|
109
|
+
blocksToInsert: PartialBlock[],
|
|
110
|
+
editor: Editor
|
|
111
|
+
) {
|
|
112
|
+
insertBlocks(blocksToInsert, blocksToRemove[0], "before", editor);
|
|
113
|
+
removeBlocks(blocksToRemove, editor);
|
|
114
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DOMParser, DOMSerializer, Schema } from "prosemirror-model";
|
|
2
|
+
import { unified } from "unified";
|
|
3
|
+
import rehypeParse from "rehype-parse";
|
|
4
|
+
import rehypeStringify from "rehype-stringify";
|
|
5
|
+
import rehypeRemark from "rehype-remark";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import remarkStringify from "remark-stringify";
|
|
8
|
+
import remarkParse from "remark-parse";
|
|
9
|
+
import remarkRehype from "remark-rehype";
|
|
10
|
+
import { Block } from "../../extensions/Blocks/api/blockTypes";
|
|
11
|
+
import { blockToNode, nodeToBlock } from "../nodeConversions/nodeConversions";
|
|
12
|
+
import { simplifyBlocks } from "./simplifyBlocksRehypePlugin";
|
|
13
|
+
import { removeUnderlines } from "./removeUnderlinesRehypePlugin";
|
|
14
|
+
|
|
15
|
+
export async function blocksToHTML(
|
|
16
|
+
blocks: Block[],
|
|
17
|
+
schema: Schema
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
const htmlParentElement = document.createElement("div");
|
|
20
|
+
const serializer = DOMSerializer.fromSchema(schema);
|
|
21
|
+
|
|
22
|
+
for (const block of blocks) {
|
|
23
|
+
const node = blockToNode(block, schema);
|
|
24
|
+
const htmlNode = serializer.serializeNode(node);
|
|
25
|
+
htmlParentElement.appendChild(htmlNode);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const htmlString = await unified()
|
|
29
|
+
.use(rehypeParse, { fragment: true })
|
|
30
|
+
.use(simplifyBlocks, {
|
|
31
|
+
orderedListItemBlockTypes: new Set<string>(["numberedListItem"]),
|
|
32
|
+
unorderedListItemBlockTypes: new Set<string>(["bulletListItem"]),
|
|
33
|
+
})
|
|
34
|
+
.use(rehypeStringify)
|
|
35
|
+
.process(htmlParentElement.innerHTML);
|
|
36
|
+
|
|
37
|
+
return htmlString.value as string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function HTMLToBlocks(
|
|
41
|
+
htmlString: string,
|
|
42
|
+
schema: Schema
|
|
43
|
+
): Promise<Block[]> {
|
|
44
|
+
const htmlNode = document.createElement("div");
|
|
45
|
+
htmlNode.innerHTML = htmlString.trim();
|
|
46
|
+
|
|
47
|
+
const parser = DOMParser.fromSchema(schema);
|
|
48
|
+
const parentNode = parser.parse(htmlNode);
|
|
49
|
+
|
|
50
|
+
const blocks: Block[] = [];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < parentNode.firstChild!.childCount; i++) {
|
|
53
|
+
blocks.push(nodeToBlock(parentNode.firstChild!.child(i)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return blocks;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function blocksToMarkdown(
|
|
60
|
+
blocks: Block[],
|
|
61
|
+
schema: Schema
|
|
62
|
+
): Promise<string> {
|
|
63
|
+
const markdownString = await unified()
|
|
64
|
+
.use(rehypeParse, { fragment: true })
|
|
65
|
+
.use(removeUnderlines)
|
|
66
|
+
.use(rehypeRemark)
|
|
67
|
+
.use(remarkGfm)
|
|
68
|
+
.use(remarkStringify)
|
|
69
|
+
.process(await blocksToHTML(blocks, schema));
|
|
70
|
+
|
|
71
|
+
return markdownString.value as string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function markdownToBlocks(
|
|
75
|
+
markdownString: string,
|
|
76
|
+
schema: Schema
|
|
77
|
+
): Promise<Block[]> {
|
|
78
|
+
const htmlString = await unified()
|
|
79
|
+
.use(remarkParse)
|
|
80
|
+
.use(remarkGfm)
|
|
81
|
+
.use(remarkRehype)
|
|
82
|
+
.use(rehypeStringify)
|
|
83
|
+
.process(markdownString);
|
|
84
|
+
|
|
85
|
+
return HTMLToBlocks(htmlString.value as string, schema);
|
|
86
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Element as HASTElement, Parent as HASTParent } from "hast";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rehype plugin which removes <u> tags. Used to remove underlines before converting HTML to markdown, as Markdown
|
|
5
|
+
* doesn't support underlines.
|
|
6
|
+
*/
|
|
7
|
+
export function removeUnderlines() {
|
|
8
|
+
const removeUnderlinesHelper = (tree: HASTParent) => {
|
|
9
|
+
let numChildElements = tree.children.length;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < numChildElements; i++) {
|
|
12
|
+
const node = tree.children[i];
|
|
13
|
+
|
|
14
|
+
if (node.type === "element") {
|
|
15
|
+
// Recursively removes underlines from child elements.
|
|
16
|
+
removeUnderlinesHelper(node);
|
|
17
|
+
|
|
18
|
+
if ((node as HASTElement).tagName === "u") {
|
|
19
|
+
// Lifts child nodes outside underline element, deletes the underline element, and updates current index &
|
|
20
|
+
// the number of child elements.
|
|
21
|
+
if (node.children.length > 0) {
|
|
22
|
+
tree.children.splice(i, 1, ...node.children);
|
|
23
|
+
|
|
24
|
+
const numElementsAdded = node.children.length - 1;
|
|
25
|
+
numChildElements += numElementsAdded;
|
|
26
|
+
i += numElementsAdded;
|
|
27
|
+
} else {
|
|
28
|
+
tree.children.splice(i, 1);
|
|
29
|
+
|
|
30
|
+
numChildElements--;
|
|
31
|
+
i--;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return removeUnderlinesHelper;
|
|
39
|
+
}
|