@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.
Files changed (43) hide show
  1. package/dist/blocknote.cjs +8 -8
  2. package/dist/blocknote.cjs.map +1 -1
  3. package/dist/blocknote.js +1816 -1665
  4. package/dist/blocknote.js.map +1 -1
  5. package/dist/tsconfig.tsbuildinfo +1 -1
  6. package/dist/webpack-stats.json +1 -1
  7. package/package.json +2 -2
  8. package/src/api/__snapshots__/blocks-indented-changed.json +129 -0
  9. package/src/api/__snapshots__/blocks-moved-deeper-into-nesting.json +164 -0
  10. package/src/api/__snapshots__/blocks-moved-multiple-in-same-transaction.json +188 -0
  11. package/src/api/__snapshots__/blocks-moved-to-different-parent.json +78 -0
  12. package/src/api/__snapshots__/blocks-moved-to-root-level.json +78 -0
  13. package/src/api/__snapshots__/blocks-outdented-changed.json +129 -0
  14. package/src/api/blockManipulation/commands/nestBlock/nestBlock.ts +58 -59
  15. package/src/api/nodeUtil.test.ts +228 -1
  16. package/src/api/nodeUtil.ts +135 -118
  17. package/src/api/parsers/markdown/detectMarkdown.test.ts +211 -0
  18. package/src/api/parsers/markdown/detectMarkdown.ts +3 -2
  19. package/src/blocks/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +2 -1
  20. package/src/blocks/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +2 -1
  21. package/src/blocks/ToggleWrapper/createToggleWrapper.ts +2 -0
  22. package/src/blocks/defaultBlockTypeGuards.ts +30 -0
  23. package/src/editor/BlockNoteEditor.ts +27 -10
  24. package/src/editor/BlockNoteExtensions.ts +3 -5
  25. package/src/exporter/Exporter.ts +2 -0
  26. package/src/exporter/mapping.ts +1 -0
  27. package/src/extensions/BlockChange/BlockChangePlugin.ts +66 -0
  28. package/src/extensions/Collaboration/CursorPlugin.ts +33 -2
  29. package/src/extensions/SideMenu/SideMenuPlugin.ts +290 -207
  30. package/src/extensions/SuggestionMenu/SuggestionPlugin.ts +22 -12
  31. package/src/schema/inlineContent/types.ts +8 -0
  32. package/src/util/browser.ts +11 -1
  33. package/types/src/api/nodeUtil.d.ts +15 -21
  34. package/types/src/api/parsers/markdown/detectMarkdown.test.d.ts +1 -0
  35. package/types/src/blocks/defaultBlockTypeGuards.d.ts +7 -1
  36. package/types/src/editor/BlockNoteEditor.d.ts +11 -8
  37. package/types/src/editor/BlockNoteExtensions.d.ts +0 -1
  38. package/types/src/exporter/Exporter.d.ts +1 -1
  39. package/types/src/exporter/mapping.d.ts +1 -1
  40. package/types/src/extensions/BlockChange/BlockChangePlugin.d.ts +15 -0
  41. package/types/src/extensions/Collaboration/CursorPlugin.d.ts +6 -0
  42. package/types/src/extensions/SideMenu/SideMenuPlugin.d.ts +50 -9
  43. package/types/src/schema/inlineContent/types.d.ts +4 -0
@@ -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
- * When an event is triggered by the local user, the source is "local".
68
- * This is the default source.
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 before the update.
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
- let source: BlockChangeSource = { type: "local" };
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 changedRanges = getChangedRanges(combinedTransaction);
196
- const prevAffectedBlocks = changedRanges
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 prevBlocks = new Map(
222
- prevAffectedBlocks.map((block) => {
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
- // Inserted blocks are blocks that were not in the previous state and are in the next state
230
- for (const [id, block] of nextBlocks) {
231
- if (!prevBlocks.has(id)) {
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
- // Deleted blocks are blocks that were in the previous state but not in the next state
242
- for (const [id, block] of prevBlocks) {
243
- if (!nextBlocks.has(id)) {
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
- // Updated blocks are blocks that were in the previous state and are in the next state
254
- for (const [id, block] of nextBlocks) {
255
- if (prevBlocks.has(id)) {
256
- const prevBlock = prevBlocks.get(id)!;
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
- // Only include the update if the block itself changed (excluding children)
259
- if (areBlocksDifferentExcludingChildren(prevBlock, block)) {
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("![Alt text](https://example.com/image.jpg)")).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 = /(?:\s|^)(_|__|\*|\*\*|~~|==|\+\+)(?!\s).{1,64}(?<!\s)(?=\1)/;
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)[^`]{1,48}(?<!\s)`([^\w]|$)/;
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/;
@@ -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
  }
@@ -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,