@blocknote/core 0.32.0 → 0.34.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.cjs +8 -8
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1816 -1665
- package/dist/blocknote.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +2 -2
- package/src/api/__snapshots__/blocks-indented-changed.json +129 -0
- package/src/api/__snapshots__/blocks-moved-deeper-into-nesting.json +164 -0
- package/src/api/__snapshots__/blocks-moved-multiple-in-same-transaction.json +188 -0
- package/src/api/__snapshots__/blocks-moved-to-different-parent.json +78 -0
- package/src/api/__snapshots__/blocks-moved-to-root-level.json +78 -0
- package/src/api/__snapshots__/blocks-outdented-changed.json +129 -0
- package/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +58 -59
- package/src/api/nodeUtil.test.ts +228 -1
- package/src/api/nodeUtil.ts +135 -118
- package/src/api/parsers/markdown/detectMarkdown.test.ts +211 -0
- package/src/api/parsers/markdown/detectMarkdown.ts +3 -2
- package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +2 -1
- package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +2 -1
- package/src/blocks/ToggleWrapper/createToggleWrapper.ts +2 -0
- package/src/blocks/defaultBlockTypeGuards.ts +30 -0
- package/src/editor/BlockNoteEditor.ts +27 -10
- package/src/editor/BlockNoteExtensions.ts +3 -5
- package/src/exporter/Exporter.ts +2 -0
- package/src/exporter/mapping.ts +1 -0
- package/src/extensions/BlockChange/BlockChangePlugin.ts +66 -0
- package/src/extensions/Collaboration/CursorPlugin.ts +33 -2
- package/src/extensions/SideMenu/SideMenuPlugin.ts +290 -207
- package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +22 -12
- package/src/schema/inlineContent/types.ts +8 -0
- package/src/util/browser.ts +11 -1
- package/types/src/api/nodeUtil.d.ts +15 -21
- package/types/src/api/parsers/markdown/detectMarkdown.test.d.ts +1 -0
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +7 -1
- package/types/src/editor/BlockNoteEditor.d.ts +11 -8
- package/types/src/editor/BlockNoteExtensions.d.ts +0 -1
- package/types/src/exporter/Exporter.d.ts +1 -1
- package/types/src/exporter/mapping.d.ts +1 -1
- package/types/src/extensions/BlockChange/BlockChangePlugin.d.ts +15 -0
- package/types/src/extensions/Collaboration/CursorPlugin.d.ts +6 -0
- package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +50 -9
- package/types/src/schema/inlineContent/types.d.ts +4 -0
package/src/api/nodeUtil.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
combineTransactionSteps,
|
|
3
|
-
findChildrenInRange,
|
|
4
|
-
getChangedRanges,
|
|
5
|
-
} from "@tiptap/core";
|
|
1
|
+
import { combineTransactionSteps } from "@tiptap/core";
|
|
6
2
|
import type { Node } from "prosemirror-model";
|
|
7
3
|
import type { Transaction } from "prosemirror-state";
|
|
8
4
|
import {
|
|
@@ -17,6 +13,23 @@ import type { StyleSchema } from "../schema/styles/types.js";
|
|
|
17
13
|
import { nodeToBlock } from "./nodeConversions/nodeToBlock.js";
|
|
18
14
|
import { getPmSchema } from "./pmUtil.js";
|
|
19
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Gets the parent block of a node, if it has one.
|
|
18
|
+
*/
|
|
19
|
+
function getParentBlockId(doc: Node, pos: number): string | undefined {
|
|
20
|
+
if (pos === 0) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const resolvedPos = doc.resolve(pos);
|
|
24
|
+
for (let i = resolvedPos.depth; i > 0; i--) {
|
|
25
|
+
const parent = resolvedPos.node(i);
|
|
26
|
+
if (isNodeBlock(parent)) {
|
|
27
|
+
return parent.attrs.id;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
/**
|
|
21
34
|
* Get a TipTap node by id
|
|
22
35
|
*/
|
|
@@ -62,38 +75,11 @@ export function isNodeBlock(node: Node): boolean {
|
|
|
62
75
|
* This attributes the changes to a specific source.
|
|
63
76
|
*/
|
|
64
77
|
export type BlockChangeSource =
|
|
65
|
-
| {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
type: "local";
|
|
71
|
-
}
|
|
72
|
-
| {
|
|
73
|
-
/**
|
|
74
|
-
* When an event is triggered by a paste operation, the source is "paste".
|
|
75
|
-
*/
|
|
76
|
-
type: "paste";
|
|
77
|
-
}
|
|
78
|
-
| {
|
|
79
|
-
/**
|
|
80
|
-
* When an event is triggered by a drop operation, the source is "drop".
|
|
81
|
-
*/
|
|
82
|
-
type: "drop";
|
|
83
|
-
}
|
|
84
|
-
| {
|
|
85
|
-
/**
|
|
86
|
-
* When an event is triggered by an undo or redo operation, the source is "undo" or "redo".
|
|
87
|
-
* @note Y.js undo/redo are not differentiated.
|
|
88
|
-
*/
|
|
89
|
-
type: "undo" | "redo" | "undo-redo";
|
|
90
|
-
}
|
|
91
|
-
| {
|
|
92
|
-
/**
|
|
93
|
-
* When an event is triggered by a remote user, the source is "remote".
|
|
94
|
-
*/
|
|
95
|
-
type: "yjs-remote";
|
|
96
|
-
};
|
|
78
|
+
| { type: "local" }
|
|
79
|
+
| { type: "paste" }
|
|
80
|
+
| { type: "drop" }
|
|
81
|
+
| { type: "undo" | "redo" | "undo-redo" }
|
|
82
|
+
| { type: "yjs-remote" };
|
|
97
83
|
|
|
98
84
|
export type BlocksChanged<
|
|
99
85
|
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
@@ -120,10 +106,25 @@ export type BlocksChanged<
|
|
|
120
106
|
| {
|
|
121
107
|
type: "update";
|
|
122
108
|
/**
|
|
123
|
-
* The block
|
|
109
|
+
* The previous block.
|
|
124
110
|
*/
|
|
125
111
|
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
126
112
|
}
|
|
113
|
+
| {
|
|
114
|
+
type: "move";
|
|
115
|
+
/**
|
|
116
|
+
* The previous block.
|
|
117
|
+
*/
|
|
118
|
+
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
119
|
+
/**
|
|
120
|
+
* The previous parent block (if it existed).
|
|
121
|
+
*/
|
|
122
|
+
prevParent?: Block<BSchema, ISchema, SSchema>;
|
|
123
|
+
/**
|
|
124
|
+
* The current parent block (if it exists).
|
|
125
|
+
*/
|
|
126
|
+
currentParent?: Block<BSchema, ISchema, SSchema>;
|
|
127
|
+
}
|
|
127
128
|
)
|
|
128
129
|
>;
|
|
129
130
|
|
|
@@ -139,8 +140,6 @@ function areBlocksDifferentExcludingChildren<
|
|
|
139
140
|
block1: Block<BSchema, ISchema, SSchema>,
|
|
140
141
|
block2: Block<BSchema, ISchema, SSchema>,
|
|
141
142
|
): boolean {
|
|
142
|
-
// TODO use an actual diff algorithm
|
|
143
|
-
// Compare all properties except children
|
|
144
143
|
return (
|
|
145
144
|
block1.id !== block2.id ||
|
|
146
145
|
block1.type !== block2.type ||
|
|
@@ -149,11 +148,63 @@ function areBlocksDifferentExcludingChildren<
|
|
|
149
148
|
);
|
|
150
149
|
}
|
|
151
150
|
|
|
151
|
+
function determineChangeSource(transaction: Transaction): BlockChangeSource {
|
|
152
|
+
if (transaction.getMeta("paste")) {
|
|
153
|
+
return { type: "paste" };
|
|
154
|
+
}
|
|
155
|
+
if (transaction.getMeta("uiEvent") === "drop") {
|
|
156
|
+
return { type: "drop" };
|
|
157
|
+
}
|
|
158
|
+
if (transaction.getMeta("history$")) {
|
|
159
|
+
return {
|
|
160
|
+
type: transaction.getMeta("history$").redo ? "redo" : "undo",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (transaction.getMeta("y-sync$")) {
|
|
164
|
+
if (transaction.getMeta("y-sync$").isUndoRedoOperation) {
|
|
165
|
+
return { type: "undo-redo" };
|
|
166
|
+
}
|
|
167
|
+
return { type: "yjs-remote" };
|
|
168
|
+
}
|
|
169
|
+
return { type: "local" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function collectAllBlocks<
|
|
173
|
+
BSchema extends BlockSchema,
|
|
174
|
+
ISchema extends InlineContentSchema,
|
|
175
|
+
SSchema extends StyleSchema,
|
|
176
|
+
>(
|
|
177
|
+
doc: Node,
|
|
178
|
+
): Record<
|
|
179
|
+
string,
|
|
180
|
+
{
|
|
181
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
182
|
+
parentId: string | undefined;
|
|
183
|
+
}
|
|
184
|
+
> {
|
|
185
|
+
const blocks: Record<
|
|
186
|
+
string,
|
|
187
|
+
{
|
|
188
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
189
|
+
parentId: string | undefined;
|
|
190
|
+
}
|
|
191
|
+
> = {};
|
|
192
|
+
const pmSchema = getPmSchema(doc);
|
|
193
|
+
doc.descendants((node, pos) => {
|
|
194
|
+
if (isNodeBlock(node)) {
|
|
195
|
+
const parentId = getParentBlockId(doc, pos);
|
|
196
|
+
blocks[node.attrs.id] = {
|
|
197
|
+
block: nodeToBlock(node, pmSchema),
|
|
198
|
+
parentId,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
return blocks;
|
|
204
|
+
}
|
|
205
|
+
|
|
152
206
|
/**
|
|
153
207
|
* Get the blocks that were changed by a transaction.
|
|
154
|
-
* @param transaction The transaction to get the changes from.
|
|
155
|
-
* @param editor The editor to get the changes from.
|
|
156
|
-
* @returns The blocks that were changed by the transaction.
|
|
157
208
|
*/
|
|
158
209
|
export function getBlocksChangedByTransaction<
|
|
159
210
|
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
@@ -163,109 +214,75 @@ export function getBlocksChangedByTransaction<
|
|
|
163
214
|
transaction: Transaction,
|
|
164
215
|
appendedTransactions: Transaction[] = [],
|
|
165
216
|
): BlocksChanged<BSchema, ISchema, SSchema> {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (transaction.getMeta("paste")) {
|
|
169
|
-
source = { type: "paste" };
|
|
170
|
-
} else if (transaction.getMeta("uiEvent") === "drop") {
|
|
171
|
-
source = { type: "drop" };
|
|
172
|
-
} else if (transaction.getMeta("history$")) {
|
|
173
|
-
source = {
|
|
174
|
-
type: transaction.getMeta("history$").redo ? "redo" : "undo",
|
|
175
|
-
};
|
|
176
|
-
} else if (transaction.getMeta("y-sync$")) {
|
|
177
|
-
if (transaction.getMeta("y-sync$").isUndoRedoOperation) {
|
|
178
|
-
source = {
|
|
179
|
-
type: "undo-redo",
|
|
180
|
-
};
|
|
181
|
-
} else {
|
|
182
|
-
source = {
|
|
183
|
-
type: "yjs-remote",
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Get affected blocks before and after the change
|
|
189
|
-
const pmSchema = getPmSchema(transaction);
|
|
217
|
+
const source = determineChangeSource(transaction);
|
|
190
218
|
const combinedTransaction = combineTransactionSteps(transaction.before, [
|
|
191
219
|
transaction,
|
|
192
220
|
...appendedTransactions,
|
|
193
221
|
]);
|
|
194
222
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
.flatMap((range) => {
|
|
198
|
-
return findChildrenInRange(
|
|
199
|
-
combinedTransaction.before,
|
|
200
|
-
range.oldRange,
|
|
201
|
-
isNodeBlock,
|
|
202
|
-
);
|
|
203
|
-
})
|
|
204
|
-
.map(({ node }) => nodeToBlock(node, pmSchema));
|
|
205
|
-
|
|
206
|
-
const nextAffectedBlocks = changedRanges
|
|
207
|
-
.flatMap((range) => {
|
|
208
|
-
return findChildrenInRange(
|
|
209
|
-
combinedTransaction.doc,
|
|
210
|
-
range.newRange,
|
|
211
|
-
isNodeBlock,
|
|
212
|
-
);
|
|
213
|
-
})
|
|
214
|
-
.map(({ node }) => nodeToBlock(node, pmSchema));
|
|
215
|
-
|
|
216
|
-
const nextBlocks = new Map(
|
|
217
|
-
nextAffectedBlocks.map((block) => {
|
|
218
|
-
return [block.id, block];
|
|
219
|
-
}),
|
|
223
|
+
const prevBlocks = collectAllBlocks<BSchema, ISchema, SSchema>(
|
|
224
|
+
combinedTransaction.before,
|
|
220
225
|
);
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
return [block.id, block];
|
|
224
|
-
}),
|
|
226
|
+
const nextBlocks = collectAllBlocks<BSchema, ISchema, SSchema>(
|
|
227
|
+
combinedTransaction.doc,
|
|
225
228
|
);
|
|
226
229
|
|
|
227
230
|
const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];
|
|
228
231
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
// Handle inserted blocks
|
|
233
|
+
Object.keys(nextBlocks)
|
|
234
|
+
.filter((id) => !(id in prevBlocks))
|
|
235
|
+
.forEach((id) => {
|
|
232
236
|
changes.push({
|
|
233
237
|
type: "insert",
|
|
234
|
-
block,
|
|
238
|
+
block: nextBlocks[id].block,
|
|
235
239
|
source,
|
|
236
240
|
prevBlock: undefined,
|
|
237
241
|
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
242
|
+
});
|
|
240
243
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
// Handle deleted blocks
|
|
245
|
+
Object.keys(prevBlocks)
|
|
246
|
+
.filter((id) => !(id in nextBlocks))
|
|
247
|
+
.forEach((id) => {
|
|
244
248
|
changes.push({
|
|
245
249
|
type: "delete",
|
|
246
|
-
block,
|
|
250
|
+
block: prevBlocks[id].block,
|
|
247
251
|
source,
|
|
248
252
|
prevBlock: undefined,
|
|
249
253
|
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
254
|
+
});
|
|
252
255
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
// Handle updated, moved, indented, outdented blocks
|
|
257
|
+
Object.keys(nextBlocks)
|
|
258
|
+
.filter((id) => id in prevBlocks)
|
|
259
|
+
.forEach((id) => {
|
|
260
|
+
const prev = prevBlocks[id];
|
|
261
|
+
const next = nextBlocks[id];
|
|
262
|
+
const isParentDifferent = prev.parentId !== next.parentId;
|
|
257
263
|
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
if (isParentDifferent) {
|
|
265
|
+
changes.push({
|
|
266
|
+
type: "move",
|
|
267
|
+
block: next.block,
|
|
268
|
+
prevBlock: prev.block,
|
|
269
|
+
source,
|
|
270
|
+
prevParent: prev.parentId
|
|
271
|
+
? prevBlocks[prev.parentId]?.block
|
|
272
|
+
: undefined,
|
|
273
|
+
currentParent: next.parentId
|
|
274
|
+
? nextBlocks[next.parentId]?.block
|
|
275
|
+
: undefined,
|
|
276
|
+
});
|
|
277
|
+
} else if (areBlocksDifferentExcludingChildren(prev.block, next.block)) {
|
|
260
278
|
changes.push({
|
|
261
279
|
type: "update",
|
|
262
|
-
block,
|
|
263
|
-
prevBlock,
|
|
280
|
+
block: next.block,
|
|
281
|
+
prevBlock: prev.block,
|
|
264
282
|
source,
|
|
265
283
|
});
|
|
266
284
|
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
285
|
+
});
|
|
269
286
|
|
|
270
287
|
return changes;
|
|
271
288
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isMarkdown } from "./detectMarkdown.js";
|
|
3
|
+
|
|
4
|
+
describe("isMarkdown", () => {
|
|
5
|
+
describe("Headings (H1-H6)", () => {
|
|
6
|
+
it("should detect H1 headings", () => {
|
|
7
|
+
expect(isMarkdown("# Heading 1\n\nContent")).toBe(true);
|
|
8
|
+
expect(isMarkdown(" # Heading 1\n\nContent")).toBe(true);
|
|
9
|
+
expect(isMarkdown(" # Heading 1\n\nContent")).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should detect H2-H6 headings", () => {
|
|
13
|
+
expect(isMarkdown("## Heading 2\n\nContent")).toBe(true);
|
|
14
|
+
expect(isMarkdown("### Heading 3\n\nContent")).toBe(true);
|
|
15
|
+
expect(isMarkdown("#### Heading 4\n\nContent")).toBe(true);
|
|
16
|
+
expect(isMarkdown("##### Heading 5\n\nContent")).toBe(true);
|
|
17
|
+
expect(isMarkdown("###### Heading 6\n\nContent")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should not detect invalid headings", () => {
|
|
21
|
+
expect(isMarkdown("####### Heading 7\n\nContent")).toBe(false);
|
|
22
|
+
expect(isMarkdown("#Heading without space\n\nContent")).toBe(false);
|
|
23
|
+
expect(isMarkdown("# \n\nContent")).toBe(false);
|
|
24
|
+
expect(
|
|
25
|
+
isMarkdown(
|
|
26
|
+
"# Very long heading that exceeds the character limit and should not be detected as a valid markdown heading\n\nContent",
|
|
27
|
+
),
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("Bold, italic, underline, strikethrough, highlight", () => {
|
|
33
|
+
it("should detect bold text", () => {
|
|
34
|
+
expect(isMarkdown("**bold text**")).toBe(true);
|
|
35
|
+
expect(isMarkdown("__bold text__")).toBe(true);
|
|
36
|
+
expect(isMarkdown(" *bold text* ")).toBe(true);
|
|
37
|
+
expect(isMarkdown(" _bold text_ ")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should detect italic text", () => {
|
|
41
|
+
expect(isMarkdown("*italic text*")).toBe(true);
|
|
42
|
+
expect(isMarkdown("_italic text_")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should detect strikethrough text", () => {
|
|
46
|
+
expect(isMarkdown("~~strikethrough text~~")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should detect highlighted text", () => {
|
|
50
|
+
expect(isMarkdown("==highlighted text==")).toBe(true);
|
|
51
|
+
expect(isMarkdown("++highlighted text++")).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("Links", () => {
|
|
56
|
+
it("should detect basic links", () => {
|
|
57
|
+
expect(isMarkdown("[Link text](https://example.com)")).toBe(true);
|
|
58
|
+
expect(isMarkdown("[Link text](http://example.com)")).toBe(true);
|
|
59
|
+
expect(isMarkdown("[Short](https://ex.com)")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should detect image links", () => {
|
|
63
|
+
expect(isMarkdown("")).toBe(
|
|
64
|
+
true,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("Inline code", () => {
|
|
70
|
+
it("should detect inline code", () => {
|
|
71
|
+
expect(isMarkdown("`code`")).toBe(true);
|
|
72
|
+
expect(isMarkdown(" `code` ")).toBe(true);
|
|
73
|
+
expect(isMarkdown("`const x = 1;`")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should not detect invalid inline code", () => {
|
|
77
|
+
expect(isMarkdown("` code `")).toBe(false); // spaces around content
|
|
78
|
+
expect(isMarkdown("``")).toBe(false); // empty
|
|
79
|
+
expect(isMarkdown("` `")).toBe(false); // only space
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("Unordered lists", () => {
|
|
84
|
+
it("should detect unordered lists", () => {
|
|
85
|
+
expect(isMarkdown("- Item 1\n- Item 2")).toBe(true);
|
|
86
|
+
expect(isMarkdown(" - Item 1\n - Item 2")).toBe(true);
|
|
87
|
+
expect(isMarkdown(" - Item 1\n - Item 2")).toBe(true);
|
|
88
|
+
expect(isMarkdown(" - Item 1\n - Item 2")).toBe(true);
|
|
89
|
+
expect(isMarkdown(" - Item 1\n - Item 2")).toBe(true);
|
|
90
|
+
expect(isMarkdown(" - Item 1\n - Item 2")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should not detect invalid unordered lists", () => {
|
|
94
|
+
expect(isMarkdown("- Item 1")).toBe(false); // single item
|
|
95
|
+
expect(isMarkdown("-- Item 1\n-- Item 2")).toBe(false); // wrong marker
|
|
96
|
+
expect(isMarkdown("-Item 1\n-Item 2")).toBe(false); // no space after marker
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("Ordered lists", () => {
|
|
101
|
+
it("should detect ordered lists", () => {
|
|
102
|
+
expect(isMarkdown("1. Item 1\n2. Item 2")).toBe(true);
|
|
103
|
+
expect(isMarkdown(" 1. Item 1\n 2. Item 2")).toBe(true);
|
|
104
|
+
expect(isMarkdown(" 1. Item 1\n 2. Item 2")).toBe(true);
|
|
105
|
+
expect(isMarkdown(" 1. Item 1\n 2. Item 2")).toBe(true);
|
|
106
|
+
expect(isMarkdown(" 1. Item 1\n 2. Item 2")).toBe(true);
|
|
107
|
+
expect(isMarkdown(" 1. Item 1\n 2. Item 2")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should not detect invalid ordered lists", () => {
|
|
111
|
+
expect(isMarkdown("1. Item 1")).toBe(false); // single item
|
|
112
|
+
expect(isMarkdown("1 Item 1\n2 Item 2")).toBe(false); // no dot
|
|
113
|
+
expect(isMarkdown("1.Item 1\n2.Item 2")).toBe(false); // no space after dot
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("Horizontal rules", () => {
|
|
118
|
+
it("should detect horizontal rules", () => {
|
|
119
|
+
expect(isMarkdown("\n\n ---\n\n")).toBe(true);
|
|
120
|
+
expect(isMarkdown("\n\n ----\n\n")).toBe(true);
|
|
121
|
+
expect(isMarkdown("\n\n ---\n\n")).toBe(true);
|
|
122
|
+
expect(isMarkdown("\n\n ---\n\n")).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("Fenced code blocks", () => {
|
|
127
|
+
it("should detect fenced code blocks", () => {
|
|
128
|
+
expect(isMarkdown("```\ncode block\n```")).toBe(true);
|
|
129
|
+
expect(isMarkdown("~~~\ncode block\n~~~")).toBe(true);
|
|
130
|
+
expect(isMarkdown("```javascript\nconst x = 1;\n```")).toBe(true);
|
|
131
|
+
expect(isMarkdown("```js\nconst x = 1;\n```")).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("Classical underlined headings", () => {
|
|
136
|
+
it("should detect H1 with equals", () => {
|
|
137
|
+
expect(isMarkdown("Heading\n===\n\nContent")).toBe(true);
|
|
138
|
+
expect(isMarkdown("Heading\n====\n\nContent")).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should detect H2 with dashes", () => {
|
|
142
|
+
expect(isMarkdown("Heading\n---\n\nContent")).toBe(true);
|
|
143
|
+
expect(isMarkdown("Heading\n----\n\nContent")).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("Blockquotes", () => {
|
|
148
|
+
it("should detect blockquotes", () => {
|
|
149
|
+
expect(isMarkdown("> This is a blockquote\n\nContent")).toBe(true);
|
|
150
|
+
expect(isMarkdown(" > This is a blockquote\n\nContent")).toBe(true);
|
|
151
|
+
expect(isMarkdown(" > This is a blockquote\n\nContent")).toBe(true);
|
|
152
|
+
expect(isMarkdown(" > This is a blockquote\n\nContent")).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should detect multi-line blockquotes", () => {
|
|
156
|
+
expect(isMarkdown("> Line 1\n> Line 2\n\nContent")).toBe(true);
|
|
157
|
+
expect(isMarkdown("> Line 1\n> Line 2\n> Line 3\n\nContent")).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("Tables", () => {
|
|
162
|
+
it("should detect table headers", () => {
|
|
163
|
+
expect(isMarkdown("| Header 1 | Header 2 |\n")).toBe(true);
|
|
164
|
+
expect(isMarkdown("| Header 1 | Header 2 | Header 3 |\n")).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should detect table dividers", () => {
|
|
168
|
+
expect(isMarkdown("| --- | --- |\n")).toBe(true);
|
|
169
|
+
expect(isMarkdown("| :--- | ---: |\n")).toBe(true);
|
|
170
|
+
expect(isMarkdown("| :---: | --- |\n")).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should detect table rows", () => {
|
|
174
|
+
expect(isMarkdown("| Cell 1 | Cell 2 |\n")).toBe(true);
|
|
175
|
+
expect(isMarkdown("| Cell 1 | Cell 2 | Cell 3 |\n")).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should detect complete tables", () => {
|
|
179
|
+
const table =
|
|
180
|
+
"| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |\n";
|
|
181
|
+
expect(isMarkdown(table)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should not detect invalid tables", () => {
|
|
185
|
+
expect(isMarkdown("| Header 1 | Header 2\n")).toBe(false); // missing closing pipe
|
|
186
|
+
expect(isMarkdown("Header 1 | Header 2 |\n")).toBe(false); // missing opening pipe
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("Edge cases and combinations", () => {
|
|
191
|
+
it("should detect mixed markdown content", () => {
|
|
192
|
+
const mixedContent =
|
|
193
|
+
"# Heading\n\nThis is **bold** and *italic* text with a [link](https://example.com).\n\n- List item 1\n- List item 2\n\n> Blockquote\n\n```\ncode block\n```";
|
|
194
|
+
expect(isMarkdown(mixedContent)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should not detect plain text", () => {
|
|
198
|
+
expect(
|
|
199
|
+
isMarkdown("This is just plain text without any markdown formatting."),
|
|
200
|
+
).toBe(false);
|
|
201
|
+
expect(isMarkdown("")).toBe(false);
|
|
202
|
+
expect(isMarkdown(" \n \n ")).toBe(false); // only whitespace
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should handle special characters", () => {
|
|
206
|
+
expect(isMarkdown("**text with `backticks`**")).toBe(true);
|
|
207
|
+
expect(isMarkdown("**text with [brackets]**")).toBe(true);
|
|
208
|
+
expect(isMarkdown("**text with (parentheses)**")).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
const h1 = /(^|\n) {0,3}#{1,6} {1,8}[^\n]{1,64}\r?\n\r?\n\s{0,32}\S/;
|
|
3
3
|
|
|
4
4
|
// Bold, italic, underline, strikethrough, highlight.
|
|
5
|
-
const bold =
|
|
5
|
+
const bold =
|
|
6
|
+
/(_|__|\*|\*\*|~~|==|\+\+)(?!\s)(?:[^\s](?:.{0,62}[^\s])?|\S)(?=\1)/;
|
|
6
7
|
|
|
7
8
|
// Basic inline link (also captures images).
|
|
8
9
|
const link = /\[[^\]]{1,128}\]\(https?:\/\/\S{1,999}\)/;
|
|
9
10
|
|
|
10
11
|
// Inline code.
|
|
11
|
-
const code = /(?:\s|^)`(?!\s)[^`]{
|
|
12
|
+
const code = /(?:\s|^)`(?!\s)(?:[^\s`](?:[^`]{0,46}[^\s`])?|[^\s`])`([^\w]|$)/;
|
|
12
13
|
|
|
13
14
|
// Unordered list.
|
|
14
15
|
const ul = /(?:^|\n)\s{0,5}-\s{1}[^\n]+\n\s{0,15}-\s/;
|
package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts
CHANGED
|
@@ -31,7 +31,8 @@ const BulletListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
31
31
|
const blockInfo = getBlockInfoFromSelection(state);
|
|
32
32
|
if (
|
|
33
33
|
!blockInfo.isBlockContainer ||
|
|
34
|
-
blockInfo.blockContent.node.type.spec.content !== "inline*"
|
|
34
|
+
blockInfo.blockContent.node.type.spec.content !== "inline*" ||
|
|
35
|
+
blockInfo.blockNoteType === "heading"
|
|
35
36
|
) {
|
|
36
37
|
return;
|
|
37
38
|
}
|
package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts
CHANGED
|
@@ -50,7 +50,8 @@ const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
|
|
|
50
50
|
if (
|
|
51
51
|
!blockInfo.isBlockContainer ||
|
|
52
52
|
blockInfo.blockContent.node.type.spec.content !== "inline*" ||
|
|
53
|
-
blockInfo.blockNoteType === "numberedListItem"
|
|
53
|
+
blockInfo.blockNoteType === "numberedListItem" ||
|
|
54
|
+
blockInfo.blockNoteType === "heading"
|
|
54
55
|
) {
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
@@ -41,6 +41,7 @@ export const createToggleWrapper = (
|
|
|
41
41
|
|
|
42
42
|
const toggleButton = document.createElement("button");
|
|
43
43
|
toggleButton.className = "bn-toggle-button";
|
|
44
|
+
toggleButton.type = "button";
|
|
44
45
|
toggleButton.innerHTML =
|
|
45
46
|
// https://fonts.google.com/icons?selected=Material+Symbols+Rounded:chevron_right:FILL@0;wght@700;GRAD@0;opsz@24&icon.query=chevron&icon.style=Rounded&icon.size=24&icon.color=%23e8eaed
|
|
46
47
|
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="CURRENTCOLOR"><path d="M320-200v-560l440 280-440 280Z"/></svg>';
|
|
@@ -75,6 +76,7 @@ export const createToggleWrapper = (
|
|
|
75
76
|
|
|
76
77
|
const toggleAddBlockButton = document.createElement("button");
|
|
77
78
|
toggleAddBlockButton.className = "bn-toggle-add-block-button";
|
|
79
|
+
toggleAddBlockButton.type = "button";
|
|
78
80
|
toggleAddBlockButton.textContent = "Empty toggle. Click to add a block.";
|
|
79
81
|
const toggleAddBlockButtonMouseDown = (event: MouseEvent) =>
|
|
80
82
|
event.preventDefault();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { CellSelection } from "prosemirror-tables";
|
|
2
2
|
import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
|
|
3
3
|
import {
|
|
4
|
+
BlockConfig,
|
|
4
5
|
BlockFromConfig,
|
|
5
6
|
BlockSchema,
|
|
6
7
|
FileBlockConfig,
|
|
8
|
+
InlineContentConfig,
|
|
7
9
|
InlineContentSchema,
|
|
8
10
|
StyleSchema,
|
|
9
11
|
} from "../schema/index.js";
|
|
@@ -35,6 +37,20 @@ export function checkDefaultBlockTypeInSchema<
|
|
|
35
37
|
);
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export function checkBlockTypeInSchema<
|
|
41
|
+
BlockType extends string,
|
|
42
|
+
Config extends BlockConfig,
|
|
43
|
+
>(
|
|
44
|
+
blockType: BlockType,
|
|
45
|
+
blockConfig: Config,
|
|
46
|
+
editor: BlockNoteEditor<any, any, any>,
|
|
47
|
+
): editor is BlockNoteEditor<{ [T in BlockType]: Config }, any, any> {
|
|
48
|
+
return (
|
|
49
|
+
blockType in editor.schema.blockSchema &&
|
|
50
|
+
editor.schema.blockSchema[blockType] === blockConfig
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
export function checkDefaultInlineContentTypeInSchema<
|
|
39
55
|
InlineContentType extends keyof DefaultInlineContentSchema,
|
|
40
56
|
B extends BlockSchema,
|
|
@@ -54,6 +70,20 @@ export function checkDefaultInlineContentTypeInSchema<
|
|
|
54
70
|
);
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
export function checkInlineContentTypeInSchema<
|
|
74
|
+
InlineContentType extends string,
|
|
75
|
+
Config extends InlineContentConfig,
|
|
76
|
+
>(
|
|
77
|
+
inlineContentType: InlineContentType,
|
|
78
|
+
inlineContentConfig: Config,
|
|
79
|
+
editor: BlockNoteEditor<any, any, any>,
|
|
80
|
+
): editor is BlockNoteEditor<any, { [T in InlineContentType]: Config }, any> {
|
|
81
|
+
return (
|
|
82
|
+
inlineContentType in editor.schema.inlineContentSchema &&
|
|
83
|
+
editor.schema.inlineContentSchema[inlineContentType] === inlineContentConfig
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
export function checkBlockIsDefaultType<
|
|
58
88
|
BlockType extends keyof DefaultBlockSchema,
|
|
59
89
|
I extends InlineContentSchema,
|