@prosekit/core 0.8.2 → 0.8.4

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 (168) hide show
  1. package/dist/editor-CfkZ4TNU.d.ts +748 -0
  2. package/dist/editor-CfkZ4TNU.d.ts.map +1 -0
  3. package/dist/{editor-DbMrpnmL.js → editor-CizSwUN8.js} +102 -192
  4. package/dist/editor-CizSwUN8.js.map +1 -0
  5. package/dist/prosekit-core-test.d.ts +20 -19
  6. package/dist/prosekit-core-test.d.ts.map +1 -0
  7. package/dist/prosekit-core-test.js +4 -5
  8. package/dist/prosekit-core-test.js.map +1 -0
  9. package/dist/prosekit-core.d.ts +782 -757
  10. package/dist/prosekit-core.d.ts.map +1 -0
  11. package/dist/prosekit-core.js +30 -45
  12. package/dist/prosekit-core.js.map +1 -0
  13. package/package.json +14 -11
  14. package/src/commands/add-mark.ts +53 -0
  15. package/src/commands/expand-mark.ts +96 -0
  16. package/src/commands/insert-default-block.spec.ts +102 -0
  17. package/src/commands/insert-default-block.ts +49 -0
  18. package/src/commands/insert-node.ts +71 -0
  19. package/src/commands/insert-text.ts +24 -0
  20. package/src/commands/remove-mark.ts +54 -0
  21. package/src/commands/remove-node.ts +43 -0
  22. package/src/commands/select-all.ts +16 -0
  23. package/src/commands/set-block-type.ts +64 -0
  24. package/src/commands/set-node-attrs.ts +68 -0
  25. package/src/commands/toggle-mark.ts +65 -0
  26. package/src/commands/toggle-node.ts +47 -0
  27. package/src/commands/toggle-wrap.spec.ts +35 -0
  28. package/src/commands/toggle-wrap.ts +42 -0
  29. package/src/commands/unset-block-type.spec.ts +49 -0
  30. package/src/commands/unset-block-type.ts +84 -0
  31. package/src/commands/unset-mark.spec.ts +35 -0
  32. package/src/commands/unset-mark.ts +38 -0
  33. package/src/commands/wrap.ts +50 -0
  34. package/src/editor/action.spec.ts +143 -0
  35. package/src/editor/action.ts +248 -0
  36. package/src/editor/editor.spec.ts +186 -0
  37. package/src/editor/editor.ts +563 -0
  38. package/src/editor/union.spec.ts +108 -0
  39. package/src/editor/union.ts +47 -0
  40. package/src/editor/with-priority.ts +25 -0
  41. package/src/error.ts +28 -0
  42. package/src/extensions/clipboard-serializer.ts +107 -0
  43. package/src/extensions/command.ts +121 -0
  44. package/src/extensions/default-state.spec.ts +60 -0
  45. package/src/extensions/default-state.ts +76 -0
  46. package/src/extensions/doc.ts +31 -0
  47. package/src/extensions/events/doc-change.ts +34 -0
  48. package/src/extensions/events/dom-event.spec.ts +70 -0
  49. package/src/extensions/events/dom-event.ts +117 -0
  50. package/src/extensions/events/editor-event.ts +293 -0
  51. package/src/extensions/events/focus.spec.ts +50 -0
  52. package/src/extensions/events/focus.ts +28 -0
  53. package/src/extensions/events/plugin-view.ts +132 -0
  54. package/src/extensions/history.ts +81 -0
  55. package/src/extensions/keymap-base.ts +60 -0
  56. package/src/extensions/keymap.spec.ts +89 -0
  57. package/src/extensions/keymap.ts +96 -0
  58. package/src/extensions/mark-spec.spec.ts +177 -0
  59. package/src/extensions/mark-spec.ts +181 -0
  60. package/src/extensions/mark-view-effect.ts +85 -0
  61. package/src/extensions/mark-view.ts +43 -0
  62. package/src/extensions/node-spec.spec.ts +224 -0
  63. package/src/extensions/node-spec.ts +199 -0
  64. package/src/extensions/node-view-effect.ts +85 -0
  65. package/src/extensions/node-view.ts +43 -0
  66. package/src/extensions/paragraph.ts +61 -0
  67. package/src/extensions/plugin.ts +91 -0
  68. package/src/extensions/text.ts +34 -0
  69. package/src/facets/base-extension.ts +54 -0
  70. package/src/facets/command.ts +21 -0
  71. package/src/facets/facet-extension.spec.ts +173 -0
  72. package/src/facets/facet-extension.ts +53 -0
  73. package/src/facets/facet-node.spec.ts +265 -0
  74. package/src/facets/facet-node.ts +185 -0
  75. package/src/facets/facet-types.ts +9 -0
  76. package/src/facets/facet.spec.ts +76 -0
  77. package/src/facets/facet.ts +84 -0
  78. package/src/facets/root.ts +44 -0
  79. package/src/facets/schema-spec.ts +30 -0
  80. package/src/facets/schema.ts +26 -0
  81. package/src/facets/state.ts +57 -0
  82. package/src/facets/union-extension.ts +41 -0
  83. package/src/index.ts +302 -0
  84. package/src/test/index.ts +4 -0
  85. package/src/test/test-builder.ts +68 -0
  86. package/src/test/test-editor.spec.ts +104 -0
  87. package/src/test/test-editor.ts +113 -0
  88. package/src/testing/index.ts +283 -0
  89. package/src/testing/keyboard.ts +5 -0
  90. package/src/types/any-function.ts +4 -0
  91. package/src/types/assert-type-equal.ts +8 -0
  92. package/src/types/attrs.ts +32 -0
  93. package/src/types/base-node-view-options.ts +33 -0
  94. package/src/types/dom-node.ts +1 -0
  95. package/src/types/extension-command.ts +52 -0
  96. package/src/types/extension-mark.ts +15 -0
  97. package/src/types/extension-node.ts +15 -0
  98. package/src/types/extension.spec.ts +56 -0
  99. package/src/types/extension.ts +168 -0
  100. package/src/types/model.ts +54 -0
  101. package/src/types/object-entries.ts +13 -0
  102. package/src/types/pick-string-literal.spec.ts +10 -0
  103. package/src/types/pick-string-literal.ts +6 -0
  104. package/src/types/pick-sub-type.spec.ts +20 -0
  105. package/src/types/pick-sub-type.ts +6 -0
  106. package/src/types/priority.ts +12 -0
  107. package/src/types/setter.ts +4 -0
  108. package/src/types/simplify-deeper.spec.ts +40 -0
  109. package/src/types/simplify-deeper.ts +6 -0
  110. package/src/types/simplify-union.spec.ts +21 -0
  111. package/src/types/simplify-union.ts +11 -0
  112. package/src/utils/array-grouping.spec.ts +29 -0
  113. package/src/utils/array-grouping.ts +25 -0
  114. package/src/utils/array.ts +21 -0
  115. package/src/utils/assert.ts +13 -0
  116. package/src/utils/attrs-match.ts +20 -0
  117. package/src/utils/can-use-regex-lookbehind.ts +12 -0
  118. package/src/utils/clsx.spec.ts +14 -0
  119. package/src/utils/clsx.ts +12 -0
  120. package/src/utils/collect-children.ts +21 -0
  121. package/src/utils/collect-nodes.ts +37 -0
  122. package/src/utils/combine-event-handlers.spec.ts +27 -0
  123. package/src/utils/combine-event-handlers.ts +27 -0
  124. package/src/utils/contains-inline-node.ts +17 -0
  125. package/src/utils/deep-equals.spec.ts +26 -0
  126. package/src/utils/deep-equals.ts +29 -0
  127. package/src/utils/default-block-at.ts +15 -0
  128. package/src/utils/editor-content.spec.ts +47 -0
  129. package/src/utils/editor-content.ts +77 -0
  130. package/src/utils/env.ts +6 -0
  131. package/src/utils/find-parent-node-of-type.ts +29 -0
  132. package/src/utils/find-parent-node.spec.ts +68 -0
  133. package/src/utils/find-parent-node.ts +55 -0
  134. package/src/utils/get-custom-selection.ts +19 -0
  135. package/src/utils/get-dom-api.ts +56 -0
  136. package/src/utils/get-id.spec.ts +14 -0
  137. package/src/utils/get-id.ts +13 -0
  138. package/src/utils/get-mark-type.ts +20 -0
  139. package/src/utils/get-node-type.ts +20 -0
  140. package/src/utils/get-node-types.ts +19 -0
  141. package/src/utils/includes-mark.ts +18 -0
  142. package/src/utils/is-at-block-start.ts +26 -0
  143. package/src/utils/is-in-code-block.ts +18 -0
  144. package/src/utils/is-mark-absent.spec.ts +53 -0
  145. package/src/utils/is-mark-absent.ts +42 -0
  146. package/src/utils/is-mark-active.ts +27 -0
  147. package/src/utils/is-node-active.ts +25 -0
  148. package/src/utils/is-subset.spec.ts +12 -0
  149. package/src/utils/is-subset.ts +11 -0
  150. package/src/utils/maybe-run.spec.ts +39 -0
  151. package/src/utils/maybe-run.ts +11 -0
  152. package/src/utils/merge-objects.spec.ts +30 -0
  153. package/src/utils/merge-objects.ts +11 -0
  154. package/src/utils/merge-specs.ts +35 -0
  155. package/src/utils/object-equal.spec.ts +26 -0
  156. package/src/utils/object-equal.ts +28 -0
  157. package/src/utils/output-spec.test.ts +95 -0
  158. package/src/utils/output-spec.ts +130 -0
  159. package/src/utils/parse.spec.ts +46 -0
  160. package/src/utils/parse.ts +321 -0
  161. package/src/utils/remove-undefined-values.spec.ts +15 -0
  162. package/src/utils/remove-undefined-values.ts +9 -0
  163. package/src/utils/set-selection-around.ts +11 -0
  164. package/src/utils/type-assertion.ts +91 -0
  165. package/src/utils/unicode.spec.ts +10 -0
  166. package/src/utils/unicode.ts +4 -0
  167. package/src/utils/with-skip-code-block.ts +15 -0
  168. package/dist/editor-CjVyjJqw.d.ts +0 -739
@@ -0,0 +1,563 @@
1
+ import type {
2
+ ProseMirrorNode,
3
+ Schema,
4
+ } from '@prosekit/pm/model'
5
+ import {
6
+ EditorState,
7
+ type Command,
8
+ type Plugin,
9
+ type Selection,
10
+ type Transaction,
11
+ } from '@prosekit/pm/state'
12
+ import {
13
+ EditorView,
14
+ type DirectEditorProps,
15
+ type EditorProps,
16
+ } from '@prosekit/pm/view'
17
+
18
+ import { ProseKitError } from '../error'
19
+ import { defineDefaultState } from '../extensions/default-state'
20
+ import type { BaseExtension } from '../facets/base-extension'
21
+ import {
22
+ subtractFacetNode,
23
+ unionFacetNode,
24
+ type FacetNode,
25
+ } from '../facets/facet-node'
26
+ import type {
27
+ Extension,
28
+ ExtractCommandActions,
29
+ ExtractMarkActions,
30
+ ExtractMarkNames,
31
+ ExtractNodeActions,
32
+ ExtractNodeNames,
33
+ } from '../types/extension'
34
+ import type {
35
+ CommandAction,
36
+ CommandCreator,
37
+ } from '../types/extension-command'
38
+ import type {
39
+ NodeJSON,
40
+ SelectionJSON,
41
+ } from '../types/model'
42
+ import { assert } from '../utils/assert'
43
+ import { deepEquals } from '../utils/deep-equals'
44
+ import {
45
+ getEditorContentDoc,
46
+ getEditorSelection,
47
+ } from '../utils/editor-content'
48
+ import {
49
+ htmlFromNode,
50
+ jsonFromNode,
51
+ type DOMDocumentOptions,
52
+ } from '../utils/parse'
53
+
54
+ import {
55
+ createMarkActions,
56
+ createNodeActions,
57
+ type MarkAction,
58
+ type NodeAction,
59
+ } from './action'
60
+ import { union } from './union'
61
+
62
+ /**
63
+ * @public
64
+ */
65
+ export interface EditorOptions<E extends Extension> {
66
+ /**
67
+ * The extension to use when creating the editor.
68
+ */
69
+ extension: E
70
+
71
+ /**
72
+ * The starting document to use when creating the editor. It can be a
73
+ * ProseMirror node JSON object, a HTML string, or a HTML element instance.
74
+ */
75
+ defaultContent?: NodeJSON | string | HTMLElement
76
+
77
+ /**
78
+ * A JSON object representing the starting document to use when creating the
79
+ * editor.
80
+ *
81
+ * @deprecated Use `defaultContent` instead.
82
+ */
83
+ defaultDoc?: NodeJSON
84
+
85
+ /**
86
+ * A HTML element or a HTML string representing the starting document to use
87
+ * when creating the editor.
88
+ *
89
+ * @deprecated Use `defaultContent` instead.
90
+ */
91
+ defaultHTML?: string | HTMLElement
92
+
93
+ /**
94
+ * A JSON object representing the starting selection to use when creating the
95
+ * editor. It's only used when `defaultContent` is also provided.
96
+ */
97
+ defaultSelection?: SelectionJSON
98
+ }
99
+
100
+ /**
101
+ * @public
102
+ */
103
+ export interface getDocHTMLOptions extends DOMDocumentOptions {}
104
+
105
+ /**
106
+ * @internal
107
+ */
108
+ export function setupEditorExtension<E extends Extension>(
109
+ options: EditorOptions<E>,
110
+ ): E {
111
+ if (options.defaultContent || options.defaultDoc || options.defaultHTML) {
112
+ return union(
113
+ options.extension,
114
+ defineDefaultState(options),
115
+ ) as Extension as E
116
+ }
117
+ return options.extension
118
+ }
119
+
120
+ /**
121
+ * @public
122
+ */
123
+ export function createEditor<E extends Extension>(
124
+ options: EditorOptions<E>,
125
+ ): Editor<E> {
126
+ const extension = setupEditorExtension(options)
127
+ const instance = new EditorInstance(extension)
128
+ return new Editor(instance)
129
+ }
130
+
131
+ /**
132
+ * An internal class to make TypeScript generic type easier to use.
133
+ *
134
+ * @internal
135
+ */
136
+ export class EditorInstance {
137
+ view: EditorView | null = null
138
+ schema: Schema
139
+ nodes: Record<string, NodeAction>
140
+ marks: Record<string, MarkAction>
141
+ commands: Record<string, CommandAction> = {}
142
+
143
+ private tree: FacetNode
144
+ private directEditorProps: DirectEditorProps
145
+ private afterMounted: Array<VoidFunction> = []
146
+
147
+ constructor(extension: Extension) {
148
+ this.tree = (extension as BaseExtension).getTree()
149
+
150
+ const payload = this.tree.getRootOutput()
151
+ const schema = payload.schema
152
+ const stateConfig = payload.state
153
+
154
+ assert(schema && stateConfig, 'Schema must be defined')
155
+
156
+ const state = EditorState.create(stateConfig)
157
+
158
+ if (payload.commands) {
159
+ for (const [name, commandCreator] of Object.entries(payload.commands)) {
160
+ this.defineCommand(name, commandCreator)
161
+ }
162
+ }
163
+
164
+ this.nodes = createNodeActions(state.schema, this.getState)
165
+ this.marks = createMarkActions(state.schema, this.getState)
166
+
167
+ this.schema = state.schema
168
+ this.directEditorProps = { state, ...payload.view }
169
+ }
170
+
171
+ public getState = (): EditorState => {
172
+ return this.view?.state || this.directEditorProps.state
173
+ }
174
+
175
+ private getDoc(): ProseMirrorNode {
176
+ return this.getState().doc
177
+ }
178
+
179
+ private getProp<PropName extends keyof EditorProps>(propName: PropName): EditorProps[PropName] | undefined {
180
+ return this.view?.someProp(propName) ?? this.directEditorProps[propName]
181
+ }
182
+
183
+ public updateState(state: EditorState): void {
184
+ if (this.view) {
185
+ this.view.updateState(state)
186
+ } else {
187
+ this.directEditorProps.state = state
188
+ }
189
+ }
190
+
191
+ private dispatch = (tr: Transaction): void => {
192
+ if (this.view) {
193
+ this.view.dispatch(tr)
194
+ } else {
195
+ this.directEditorProps.state = this.directEditorProps.state.apply(tr)
196
+ }
197
+ }
198
+
199
+ public setContent(
200
+ content: NodeJSON | string | HTMLElement | ProseMirrorNode,
201
+ selection?: SelectionJSON | Selection | 'start' | 'end',
202
+ ): void {
203
+ const doc = getEditorContentDoc(this.schema, content)
204
+ doc.check()
205
+ const sel = getEditorSelection(doc, selection || 'start')
206
+
207
+ const oldState = this.getState()
208
+ if (doc.eq(oldState.doc) && (!selection || sel.eq(oldState.selection))) {
209
+ return
210
+ }
211
+
212
+ const newState = EditorState.create({
213
+ doc,
214
+ selection: sel,
215
+ plugins: oldState.plugins,
216
+ })
217
+ this.updateState(newState)
218
+ }
219
+
220
+ /**
221
+ * Return a JSON object representing the editor's current document.
222
+ */
223
+ public getDocJSON = (): NodeJSON => {
224
+ const state = this.getState()
225
+ return jsonFromNode(state.doc)
226
+ }
227
+
228
+ /**
229
+ * Return a HTML string representing the editor's current document.
230
+ */
231
+ public getDocHTML = (options?: getDocHTMLOptions): string => {
232
+ const serializer = this.getProp('clipboardSerializer')
233
+ const DOMSerializer = serializer ? { fromSchema: () => serializer } : undefined
234
+ const doc = this.getDoc()
235
+ return htmlFromNode(doc, { ...options, DOMSerializer })
236
+ }
237
+
238
+ private updateExtension(extension: Extension, add: boolean): void {
239
+ const view = this.view
240
+
241
+ // Don't update the extension if the editor is already unmounted
242
+ if (!view || view.isDestroyed) {
243
+ return
244
+ }
245
+
246
+ const tree = (extension as BaseExtension).getTree()
247
+ const payload = tree.getRootOutput()
248
+
249
+ if (payload?.schema) {
250
+ throw new ProseKitError('Schema cannot be changed')
251
+ }
252
+
253
+ if (payload?.view) {
254
+ throw new ProseKitError('View cannot be changed')
255
+ }
256
+
257
+ const oldPayload = this.tree.getRootOutput()
258
+ const oldPlugins = [...(view.state?.plugins ?? [])]
259
+
260
+ this.tree = add
261
+ ? unionFacetNode(this.tree, tree)
262
+ : subtractFacetNode(this.tree, tree)
263
+
264
+ const newPayload = this.tree.getRootOutput()
265
+ const newPlugins = [...(newPayload?.state?.plugins ?? [])]
266
+
267
+ if (!deepEquals(oldPlugins, newPlugins)) {
268
+ const state = view.state.reconfigure({ plugins: newPlugins })
269
+ view.updateState(state)
270
+ }
271
+
272
+ if (
273
+ newPayload?.commands
274
+ && !deepEquals(oldPayload?.commands, newPayload?.commands)
275
+ ) {
276
+ const commands = newPayload.commands
277
+ const names = Object.keys(commands)
278
+ for (const name of names) {
279
+ this.defineCommand(name, commands[name])
280
+ }
281
+ }
282
+ }
283
+
284
+ public use(extension: Extension): VoidFunction {
285
+ if (!this.mounted) {
286
+ let canceled = false
287
+ let lazyRemove: VoidFunction | null = null
288
+
289
+ const lazyCreate = () => {
290
+ if (!canceled) {
291
+ lazyRemove = this.use(extension)
292
+ }
293
+ }
294
+
295
+ this.afterMounted.push(lazyCreate)
296
+
297
+ return () => {
298
+ canceled = true
299
+ lazyRemove?.()
300
+ }
301
+ }
302
+
303
+ this.updateExtension(extension, true)
304
+ return () => this.updateExtension(extension, false)
305
+ }
306
+
307
+ public mount(place: HTMLElement): void {
308
+ if (this.view) {
309
+ throw new ProseKitError('Editor is already mounted')
310
+ }
311
+ this.view = new EditorView({ mount: place }, this.directEditorProps)
312
+ this.afterMounted.forEach((callback) => callback())
313
+ }
314
+
315
+ public unmount(): void {
316
+ // If the editor is not mounted, do nothing
317
+ if (!this.view) return
318
+
319
+ this.directEditorProps.state = this.view.state
320
+ this.view.destroy()
321
+ this.view = null
322
+ }
323
+
324
+ get mounted(): boolean {
325
+ return !!this.view && !this.view.isDestroyed
326
+ }
327
+
328
+ public get assertView(): EditorView {
329
+ if (!this.view) {
330
+ throw new ProseKitError('Editor is not mounted')
331
+ }
332
+ return this.view
333
+ }
334
+
335
+ public definePlugins(plugins: readonly Plugin[]): void {
336
+ const view = this.assertView
337
+ const state = view.state
338
+ const newPlugins = [...plugins, ...state.plugins]
339
+ const newState = state.reconfigure({ plugins: newPlugins })
340
+ view.setProps({ state: newState })
341
+ }
342
+
343
+ public removePlugins(plugins: readonly Plugin[]): void {
344
+ const view = this.view
345
+ if (!view) return
346
+
347
+ const state = view.state
348
+ const newPlugins = state.plugins.filter((p) => !plugins.includes(p))
349
+ const newState = state.reconfigure({ plugins: newPlugins })
350
+ view.setProps({ state: newState })
351
+ }
352
+
353
+ exec(command: Command): boolean {
354
+ const state = this.getState()
355
+ return command(state, this.dispatch, this.view ?? undefined)
356
+ }
357
+
358
+ canExec(command: Command): boolean {
359
+ const state = this.getState()
360
+ return command(state, undefined, this.view ?? undefined)
361
+ }
362
+
363
+ public defineCommand<Args extends any[] = any[]>(
364
+ name: string,
365
+ commandCreator: CommandCreator<Args>,
366
+ ): void {
367
+ const action: CommandAction<Args> = (...args: Args) => {
368
+ const command = commandCreator(...args)
369
+ return this.exec(command)
370
+ }
371
+
372
+ const canExec = (...args: Args) => {
373
+ const command = commandCreator(...args)
374
+ return this.canExec(command)
375
+ }
376
+
377
+ action.canApply = canExec
378
+ action.canExec = canExec
379
+
380
+ this.commands[name] = action as CommandAction
381
+ }
382
+
383
+ public removeCommand(name: string): void {
384
+ delete this.commands[name]
385
+ }
386
+ }
387
+
388
+ /**
389
+ * @public
390
+ */
391
+ export class Editor<E extends Extension = any> {
392
+ private instance: EditorInstance
393
+
394
+ /**
395
+ * @internal
396
+ */
397
+ constructor(instance: EditorInstance) {
398
+ if (!(instance instanceof EditorInstance)) {
399
+ throw new TypeError('Invalid EditorInstance')
400
+ }
401
+ this.instance = instance
402
+ }
403
+
404
+ /**
405
+ * Whether the editor is mounted.
406
+ */
407
+ get mounted(): boolean {
408
+ return this.instance.mounted
409
+ }
410
+
411
+ /**
412
+ * The editor view.
413
+ */
414
+ get view(): EditorView {
415
+ return this.instance.assertView
416
+ }
417
+
418
+ /**
419
+ * The editor schema.
420
+ */
421
+ get schema(): Schema<ExtractNodeNames<E>, ExtractMarkNames<E>> {
422
+ return this.instance.schema
423
+ }
424
+
425
+ /**
426
+ * The editor's current state.
427
+ */
428
+ get state(): EditorState {
429
+ return this.instance.getState()
430
+ }
431
+
432
+ /**
433
+ * Whether the editor is focused.
434
+ */
435
+ get focused(): boolean {
436
+ return this.instance.view?.hasFocus() ?? false
437
+ }
438
+
439
+ /**
440
+ * Mount the editor to the given HTML element.
441
+ * Pass `null` or `undefined` to unmount the editor.
442
+ */
443
+ mount = (place: HTMLElement | null | undefined): void => {
444
+ if (place) {
445
+ this.instance.mount(place)
446
+ } else {
447
+ this.instance.unmount()
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Unmount the editor. This is equivalent to `mount(null)`.
453
+ */
454
+ unmount = (): void => {
455
+ this.instance.unmount()
456
+ }
457
+
458
+ /**
459
+ * Focus the editor.
460
+ */
461
+ focus = (): void => {
462
+ this.instance.view?.focus()
463
+ }
464
+
465
+ /**
466
+ * Blur the editor.
467
+ */
468
+ blur = (): void => {
469
+ this.instance.view?.dom.blur()
470
+ }
471
+
472
+ /**
473
+ * Register an extension to the editor. Return a function to unregister the
474
+ * extension.
475
+ */
476
+ use = (extension: Extension): VoidFunction => {
477
+ return this.instance.use(extension)
478
+ }
479
+
480
+ /**
481
+ * Update the editor's state.
482
+ *
483
+ * @remarks
484
+ *
485
+ * This is an advanced method. Use it only if you have a specific reason to
486
+ * directly manipulate the editor's state.
487
+ */
488
+ updateState = (state: EditorState): void => {
489
+ this.instance.updateState(state)
490
+ }
491
+
492
+ /**
493
+ * Update the editor's document and selection.
494
+ *
495
+ * @param content - The new document to set. It can be one of the following:
496
+ * - A ProseMirror node instance
497
+ * - A ProseMirror node JSON object
498
+ * - An HTML string
499
+ * - An HTML element instance
500
+ * @param selection - Optional. Specifies the new selection. It can be one of the following:
501
+ * - A ProseMirror selection instance
502
+ * - A ProseMirror selection JSON object
503
+ * - The string "start" (to set selection at the beginning, default value)
504
+ * - The string "end" (to set selection at the end)
505
+ */
506
+ setContent = (
507
+ content: ProseMirrorNode | NodeJSON | string | HTMLElement,
508
+ selection?: SelectionJSON | Selection | 'start' | 'end',
509
+ ): void => {
510
+ return this.instance.setContent(content, selection)
511
+ }
512
+
513
+ /**
514
+ * Return a JSON object representing the editor's current document.
515
+ */
516
+ public getDocJSON = (): NodeJSON => {
517
+ return this.instance.getDocJSON()
518
+ }
519
+
520
+ /**
521
+ * Return a HTML string representing the editor's current document.
522
+ */
523
+ public getDocHTML = (options?: getDocHTMLOptions): string => {
524
+ return this.instance.getDocHTML(options)
525
+ }
526
+
527
+ /**
528
+ * Execute the given command. Return `true` if the command was successfully
529
+ * executed, otherwise `false`.
530
+ */
531
+ exec = (command: Command): boolean => {
532
+ return this.instance.exec(command)
533
+ }
534
+
535
+ /**
536
+ * Check if the given command can be executed. Return `true` if the command
537
+ * can be executed, otherwise `false`.
538
+ */
539
+ canExec = (command: Command): boolean => {
540
+ return this.instance.canExec(command)
541
+ }
542
+
543
+ /**
544
+ * All {@link CommandAction}s defined by the editor.
545
+ */
546
+ get commands(): ExtractCommandActions<E> {
547
+ return this.instance.commands as ExtractCommandActions<E>
548
+ }
549
+
550
+ /**
551
+ * All {@link NodeAction}s defined by the editor.
552
+ */
553
+ get nodes(): ExtractNodeActions<E> {
554
+ return this.instance.nodes as ExtractNodeActions<E>
555
+ }
556
+
557
+ /**
558
+ * All {@link MarkAction}s defined by the editor.
559
+ */
560
+ get marks(): ExtractMarkActions<E> {
561
+ return this.instance.marks as ExtractMarkActions<E>
562
+ }
563
+ }
@@ -0,0 +1,108 @@
1
+ import type { Attrs } from '@prosekit/pm/model'
2
+ import type { Command } from '@prosekit/pm/state'
3
+ import {
4
+ describe,
5
+ expectTypeOf,
6
+ it,
7
+ } from 'vitest'
8
+
9
+ import { defineCommands } from '../extensions/command'
10
+ import { defineMarkSpec } from '../extensions/mark-spec'
11
+ import { defineNodeSpec } from '../extensions/node-spec'
12
+ import { assertTypeEqual } from '../types/assert-type-equal'
13
+ import type { Extension } from '../types/extension'
14
+ import type { CommandCreator } from '../types/extension-command'
15
+
16
+ import { union } from './union'
17
+
18
+ describe('union', () => {
19
+ it('can merge one extension types', () => {
20
+ const input = [extension3]
21
+ const output = union(input)
22
+ type Outout = typeof output
23
+ type Expected = Extension<{
24
+ Nodes: { node3: Attrs }
25
+ Marks: never
26
+ Commands: never
27
+ }>
28
+
29
+ expectTypeOf(output).toEqualTypeOf<Expected>()
30
+ assertTypeEqual<Outout, Expected>(true)
31
+ })
32
+
33
+ it('can merge an extension array', () => {
34
+ const output = union([
35
+ extension1,
36
+ extension2,
37
+ extension3,
38
+ extension4,
39
+ extension5,
40
+ ])
41
+ type Outout = typeof output
42
+ type Expected = Extension<{
43
+ Nodes: {
44
+ node3: Attrs
45
+ node4: Attrs
46
+ }
47
+ Marks: {
48
+ mark5: { attr5: string }
49
+ }
50
+ Commands: {
51
+ command1: [{ arg1: string }]
52
+ command2: [{ arg2: number }]
53
+ command3: [number, boolean]
54
+ }
55
+ }>
56
+
57
+ expectTypeOf(output).toEqualTypeOf<Expected>()
58
+ assertTypeEqual<Outout, Expected>(true)
59
+ })
60
+
61
+ it('can merge a nested array', () => {
62
+ const e12 = union(extension1, extension2)
63
+ const e34 = union(extension3, extension4)
64
+ const e12345 = union(e12, e34, extension5)
65
+
66
+ const input = union(e12345)
67
+ const output = union(input)
68
+
69
+ type Outout = typeof output
70
+ type Expected = Extension<{
71
+ Nodes: {
72
+ node3: Attrs
73
+ node4: Attrs
74
+ }
75
+ Marks: {
76
+ mark5: { attr5: string }
77
+ }
78
+ Commands: {
79
+ command1: [{ arg1: string }]
80
+ command2: [{ arg2: number }]
81
+ command3: [number, boolean]
82
+ }
83
+ }>
84
+
85
+ expectTypeOf(output).toEqualTypeOf<Expected>()
86
+ assertTypeEqual<Outout, Expected>(true)
87
+ })
88
+ })
89
+
90
+ type Command1 = (args: { arg1: string }) => Command
91
+
92
+ const command1: Command1 = () => {
93
+ return () => true
94
+ }
95
+
96
+ function command2(_args: { arg2: number }): Command {
97
+ return () => true
98
+ }
99
+
100
+ const command3: CommandCreator<[number, boolean]> = (_num, _bool) => {
101
+ return () => true
102
+ }
103
+
104
+ const extension1 = defineCommands({ command1 })
105
+ const extension2 = defineCommands({ command2, command3 })
106
+ const extension3 = defineNodeSpec({ name: 'node3' })
107
+ const extension4 = defineNodeSpec({ name: 'node4' })
108
+ const extension5 = defineMarkSpec<'mark5', { attr5: string }>({ name: 'mark5' })
@@ -0,0 +1,47 @@
1
+ import type { BaseExtension } from '../facets/base-extension'
2
+ import { UnionExtensionImpl } from '../facets/union-extension'
3
+ import type {
4
+ Extension,
5
+ Union,
6
+ } from '../types/extension'
7
+ import { assert } from '../utils/assert'
8
+
9
+ /**
10
+ * Merges multiple extensions into one. You can pass multiple extensions as
11
+ * arguments or a single array containing multiple extensions.
12
+ *
13
+ * @throws If no extensions are provided.
14
+ *
15
+ * @example
16
+ *
17
+ * ```ts
18
+ * function defineFancyNodes() {
19
+ * return union(
20
+ * defineFancyParagraph(),
21
+ * defineFancyHeading(),
22
+ * )
23
+ * }
24
+ * ```
25
+ *
26
+ * @example
27
+ *
28
+ * ```ts
29
+ * function defineFancyNodes() {
30
+ * return union([
31
+ * defineFancyParagraph(),
32
+ * defineFancyHeading(),
33
+ * ])
34
+ * }
35
+ * ```
36
+ *
37
+ * @public
38
+ */
39
+ function union<const E extends readonly Extension[]>(...exts: E): Union<E>
40
+ function union<const E extends readonly Extension[]>(exts: E): Union<E>
41
+ function union(...exts: Array<Extension | Extension[]>): Extension {
42
+ const extensions: Extension[] = exts.flat()
43
+ assert(extensions.length > 0, 'At least one extension is required')
44
+ return new UnionExtensionImpl(extensions as BaseExtension[]) as Extension
45
+ }
46
+
47
+ export { union }