@blocknote/core 0.33.0 → 0.35.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 +9 -9
- package/dist/blocknote.cjs.map +1 -1
- package/dist/blocknote.js +1822 -1688
- package/dist/blocknote.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +3 -3
- 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/clipboard/fromClipboard/handleFileInsertion.ts +12 -1
- package/src/api/nodeUtil.test.ts +228 -1
- package/src/api/nodeUtil.ts +139 -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/QuoteBlockContent/QuoteBlockContent.ts +23 -1
- package/src/blocks/ToggleWrapper/createToggleWrapper.ts +2 -0
- package/src/blocks/defaultBlockTypeGuards.ts +30 -0
- package/src/editor/BlockNoteEditor.ts +105 -38
- 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/ForkYDocPlugin.ts +1 -1
- package/src/extensions/SideMenu/SideMenuPlugin.ts +288 -207
- package/src/schema/inlineContent/types.ts +8 -0
- package/types/src/api/nodeUtil.d.ts +19 -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 +89 -36
- 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/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,9 +106,28 @@ export type BlocksChanged<
|
|
|
120
106
|
| {
|
|
121
107
|
type: "update";
|
|
122
108
|
/**
|
|
123
|
-
* The block
|
|
109
|
+
* The previous block.
|
|
110
|
+
*/
|
|
111
|
+
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
type: "move";
|
|
115
|
+
/**
|
|
116
|
+
* The affected block.
|
|
117
|
+
*/
|
|
118
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
119
|
+
/**
|
|
120
|
+
* The block before the move.
|
|
124
121
|
*/
|
|
125
122
|
prevBlock: Block<BSchema, ISchema, SSchema>;
|
|
123
|
+
/**
|
|
124
|
+
* The previous parent block (if it existed).
|
|
125
|
+
*/
|
|
126
|
+
prevParent?: Block<BSchema, ISchema, SSchema>;
|
|
127
|
+
/**
|
|
128
|
+
* The current parent block (if it exists).
|
|
129
|
+
*/
|
|
130
|
+
currentParent?: Block<BSchema, ISchema, SSchema>;
|
|
126
131
|
}
|
|
127
132
|
)
|
|
128
133
|
>;
|
|
@@ -139,8 +144,6 @@ function areBlocksDifferentExcludingChildren<
|
|
|
139
144
|
block1: Block<BSchema, ISchema, SSchema>,
|
|
140
145
|
block2: Block<BSchema, ISchema, SSchema>,
|
|
141
146
|
): boolean {
|
|
142
|
-
// TODO use an actual diff algorithm
|
|
143
|
-
// Compare all properties except children
|
|
144
147
|
return (
|
|
145
148
|
block1.id !== block2.id ||
|
|
146
149
|
block1.type !== block2.type ||
|
|
@@ -149,11 +152,63 @@ function areBlocksDifferentExcludingChildren<
|
|
|
149
152
|
);
|
|
150
153
|
}
|
|
151
154
|
|
|
155
|
+
function determineChangeSource(transaction: Transaction): BlockChangeSource {
|
|
156
|
+
if (transaction.getMeta("paste")) {
|
|
157
|
+
return { type: "paste" };
|
|
158
|
+
}
|
|
159
|
+
if (transaction.getMeta("uiEvent") === "drop") {
|
|
160
|
+
return { type: "drop" };
|
|
161
|
+
}
|
|
162
|
+
if (transaction.getMeta("history$")) {
|
|
163
|
+
return {
|
|
164
|
+
type: transaction.getMeta("history$").redo ? "redo" : "undo",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (transaction.getMeta("y-sync$")) {
|
|
168
|
+
if (transaction.getMeta("y-sync$").isUndoRedoOperation) {
|
|
169
|
+
return { type: "undo-redo" };
|
|
170
|
+
}
|
|
171
|
+
return { type: "yjs-remote" };
|
|
172
|
+
}
|
|
173
|
+
return { type: "local" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectAllBlocks<
|
|
177
|
+
BSchema extends BlockSchema,
|
|
178
|
+
ISchema extends InlineContentSchema,
|
|
179
|
+
SSchema extends StyleSchema,
|
|
180
|
+
>(
|
|
181
|
+
doc: Node,
|
|
182
|
+
): Record<
|
|
183
|
+
string,
|
|
184
|
+
{
|
|
185
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
186
|
+
parentId: string | undefined;
|
|
187
|
+
}
|
|
188
|
+
> {
|
|
189
|
+
const blocks: Record<
|
|
190
|
+
string,
|
|
191
|
+
{
|
|
192
|
+
block: Block<BSchema, ISchema, SSchema>;
|
|
193
|
+
parentId: string | undefined;
|
|
194
|
+
}
|
|
195
|
+
> = {};
|
|
196
|
+
const pmSchema = getPmSchema(doc);
|
|
197
|
+
doc.descendants((node, pos) => {
|
|
198
|
+
if (isNodeBlock(node)) {
|
|
199
|
+
const parentId = getParentBlockId(doc, pos);
|
|
200
|
+
blocks[node.attrs.id] = {
|
|
201
|
+
block: nodeToBlock(node, pmSchema),
|
|
202
|
+
parentId,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
});
|
|
207
|
+
return blocks;
|
|
208
|
+
}
|
|
209
|
+
|
|
152
210
|
/**
|
|
153
211
|
* 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
212
|
*/
|
|
158
213
|
export function getBlocksChangedByTransaction<
|
|
159
214
|
BSchema extends BlockSchema = DefaultBlockSchema,
|
|
@@ -163,109 +218,75 @@ export function getBlocksChangedByTransaction<
|
|
|
163
218
|
transaction: Transaction,
|
|
164
219
|
appendedTransactions: Transaction[] = [],
|
|
165
220
|
): 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);
|
|
221
|
+
const source = determineChangeSource(transaction);
|
|
190
222
|
const combinedTransaction = combineTransactionSteps(transaction.before, [
|
|
191
223
|
transaction,
|
|
192
224
|
...appendedTransactions,
|
|
193
225
|
]);
|
|
194
226
|
|
|
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
|
-
}),
|
|
227
|
+
const prevBlocks = collectAllBlocks<BSchema, ISchema, SSchema>(
|
|
228
|
+
combinedTransaction.before,
|
|
220
229
|
);
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
return [block.id, block];
|
|
224
|
-
}),
|
|
230
|
+
const nextBlocks = collectAllBlocks<BSchema, ISchema, SSchema>(
|
|
231
|
+
combinedTransaction.doc,
|
|
225
232
|
);
|
|
226
233
|
|
|
227
234
|
const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];
|
|
228
235
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
// Handle inserted blocks
|
|
237
|
+
Object.keys(nextBlocks)
|
|
238
|
+
.filter((id) => !(id in prevBlocks))
|
|
239
|
+
.forEach((id) => {
|
|
232
240
|
changes.push({
|
|
233
241
|
type: "insert",
|
|
234
|
-
block,
|
|
242
|
+
block: nextBlocks[id].block,
|
|
235
243
|
source,
|
|
236
244
|
prevBlock: undefined,
|
|
237
245
|
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
246
|
+
});
|
|
240
247
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
// Handle deleted blocks
|
|
249
|
+
Object.keys(prevBlocks)
|
|
250
|
+
.filter((id) => !(id in nextBlocks))
|
|
251
|
+
.forEach((id) => {
|
|
244
252
|
changes.push({
|
|
245
253
|
type: "delete",
|
|
246
|
-
block,
|
|
254
|
+
block: prevBlocks[id].block,
|
|
247
255
|
source,
|
|
248
256
|
prevBlock: undefined,
|
|
249
257
|
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
258
|
+
});
|
|
252
259
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
260
|
+
// Handle updated, moved, indented, outdented blocks
|
|
261
|
+
Object.keys(nextBlocks)
|
|
262
|
+
.filter((id) => id in prevBlocks)
|
|
263
|
+
.forEach((id) => {
|
|
264
|
+
const prev = prevBlocks[id];
|
|
265
|
+
const next = nextBlocks[id];
|
|
266
|
+
const isParentDifferent = prev.parentId !== next.parentId;
|
|
257
267
|
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
if (isParentDifferent) {
|
|
269
|
+
changes.push({
|
|
270
|
+
type: "move",
|
|
271
|
+
block: next.block,
|
|
272
|
+
prevBlock: prev.block,
|
|
273
|
+
source,
|
|
274
|
+
prevParent: prev.parentId
|
|
275
|
+
? prevBlocks[prev.parentId]?.block
|
|
276
|
+
: undefined,
|
|
277
|
+
currentParent: next.parentId
|
|
278
|
+
? nextBlocks[next.parentId]?.block
|
|
279
|
+
: undefined,
|
|
280
|
+
});
|
|
281
|
+
} else if (areBlocksDifferentExcludingChildren(prev.block, next.block)) {
|
|
260
282
|
changes.push({
|
|
261
283
|
type: "update",
|
|
262
|
-
block,
|
|
263
|
-
prevBlock,
|
|
284
|
+
block: next.block,
|
|
285
|
+
prevBlock: prev.block,
|
|
264
286
|
source,
|
|
265
287
|
});
|
|
266
288
|
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
289
|
+
});
|
|
269
290
|
|
|
270
291
|
return changes;
|
|
271
292
|
}
|
|
@@ -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
|
}
|
|
@@ -2,11 +2,15 @@ import {
|
|
|
2
2
|
createBlockSpecFromStronglyTypedTiptapNode,
|
|
3
3
|
createStronglyTypedTiptapNode,
|
|
4
4
|
} from "../../schema/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createDefaultBlockDOMOutputSpec,
|
|
7
|
+
mergeParagraphs,
|
|
8
|
+
} from "../defaultBlockHelpers.js";
|
|
6
9
|
import { defaultProps } from "../defaultProps.js";
|
|
7
10
|
import { getBlockInfoFromSelection } from "../../api/getBlockInfoFromPos.js";
|
|
8
11
|
import { updateBlockCommand } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js";
|
|
9
12
|
import { InputRule } from "@tiptap/core";
|
|
13
|
+
import { DOMParser } from "prosemirror-model";
|
|
10
14
|
|
|
11
15
|
export const quotePropSchema = {
|
|
12
16
|
...defaultProps,
|
|
@@ -76,6 +80,24 @@ export const QuoteBlockContent = createStronglyTypedTiptapNode({
|
|
|
76
80
|
{
|
|
77
81
|
tag: "blockquote",
|
|
78
82
|
node: "quote",
|
|
83
|
+
getContent: (node, schema) => {
|
|
84
|
+
// Parse the blockquote content as inline content
|
|
85
|
+
const element = node as HTMLElement;
|
|
86
|
+
|
|
87
|
+
// Clone to avoid modifying the original
|
|
88
|
+
const clone = element.cloneNode(true) as HTMLElement;
|
|
89
|
+
|
|
90
|
+
// Merge multiple paragraphs into one with line breaks
|
|
91
|
+
mergeParagraphs(clone);
|
|
92
|
+
|
|
93
|
+
// Parse the content directly as a paragraph to extract inline content
|
|
94
|
+
const parser = DOMParser.fromSchema(schema);
|
|
95
|
+
const parsed = parser.parse(clone, {
|
|
96
|
+
topNode: schema.nodes.paragraph.create(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return parsed.content;
|
|
100
|
+
},
|
|
79
101
|
},
|
|
80
102
|
];
|
|
81
103
|
},
|
|
@@ -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();
|