@prosekit/core 0.8.3 → 0.8.5

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 (170) hide show
  1. package/dist/editor-KZlceNQ1.d.ts +722 -0
  2. package/dist/editor-KZlceNQ1.d.ts.map +1 -0
  3. package/dist/{editor-DlGlYOp-.js → editor-TvRTsFdO.js} +102 -196
  4. package/dist/editor-TvRTsFdO.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 +5 -8
  8. package/dist/prosekit-core-test.js.map +1 -0
  9. package/dist/prosekit-core.d.ts +797 -792
  10. package/dist/prosekit-core.d.ts.map +1 -0
  11. package/dist/prosekit-core.js +42 -79
  12. package/dist/prosekit-core.js.map +1 -0
  13. package/package.json +14 -12
  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 +125 -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.spec.ts +153 -0
  68. package/src/extensions/plugin.ts +81 -0
  69. package/src/extensions/text.ts +34 -0
  70. package/src/facets/base-extension.ts +54 -0
  71. package/src/facets/command.ts +21 -0
  72. package/src/facets/facet-extension.spec.ts +173 -0
  73. package/src/facets/facet-extension.ts +53 -0
  74. package/src/facets/facet-node.spec.ts +265 -0
  75. package/src/facets/facet-node.ts +185 -0
  76. package/src/facets/facet-types.ts +9 -0
  77. package/src/facets/facet.spec.ts +76 -0
  78. package/src/facets/facet.ts +84 -0
  79. package/src/facets/root.ts +44 -0
  80. package/src/facets/schema-spec.ts +30 -0
  81. package/src/facets/schema.ts +26 -0
  82. package/src/facets/state.spec.ts +53 -0
  83. package/src/facets/state.ts +85 -0
  84. package/src/facets/union-extension.ts +41 -0
  85. package/src/index.ts +302 -0
  86. package/src/test/index.ts +4 -0
  87. package/src/test/test-builder.ts +68 -0
  88. package/src/test/test-editor.spec.ts +104 -0
  89. package/src/test/test-editor.ts +113 -0
  90. package/src/testing/index.ts +283 -0
  91. package/src/testing/keyboard.ts +5 -0
  92. package/src/types/any-function.ts +4 -0
  93. package/src/types/assert-type-equal.ts +8 -0
  94. package/src/types/attrs.ts +32 -0
  95. package/src/types/base-node-view-options.ts +33 -0
  96. package/src/types/dom-node.ts +1 -0
  97. package/src/types/extension-command.ts +52 -0
  98. package/src/types/extension-mark.ts +15 -0
  99. package/src/types/extension-node.ts +15 -0
  100. package/src/types/extension.spec.ts +56 -0
  101. package/src/types/extension.ts +168 -0
  102. package/src/types/model.ts +54 -0
  103. package/src/types/object-entries.ts +13 -0
  104. package/src/types/pick-string-literal.spec.ts +10 -0
  105. package/src/types/pick-string-literal.ts +6 -0
  106. package/src/types/pick-sub-type.spec.ts +20 -0
  107. package/src/types/pick-sub-type.ts +6 -0
  108. package/src/types/priority.ts +12 -0
  109. package/src/types/setter.ts +4 -0
  110. package/src/types/simplify-deeper.spec.ts +40 -0
  111. package/src/types/simplify-deeper.ts +6 -0
  112. package/src/types/simplify-union.spec.ts +21 -0
  113. package/src/types/simplify-union.ts +11 -0
  114. package/src/utils/array-grouping.spec.ts +29 -0
  115. package/src/utils/array-grouping.ts +25 -0
  116. package/src/utils/array.ts +21 -0
  117. package/src/utils/assert.ts +13 -0
  118. package/src/utils/attrs-match.ts +20 -0
  119. package/src/utils/can-use-regex-lookbehind.ts +12 -0
  120. package/src/utils/clsx.spec.ts +14 -0
  121. package/src/utils/clsx.ts +14 -0
  122. package/src/utils/collect-children.ts +21 -0
  123. package/src/utils/collect-nodes.ts +37 -0
  124. package/src/utils/combine-event-handlers.spec.ts +27 -0
  125. package/src/utils/combine-event-handlers.ts +27 -0
  126. package/src/utils/contains-inline-node.ts +17 -0
  127. package/src/utils/deep-equals.spec.ts +26 -0
  128. package/src/utils/deep-equals.ts +29 -0
  129. package/src/utils/default-block-at.ts +15 -0
  130. package/src/utils/editor-content.spec.ts +47 -0
  131. package/src/utils/editor-content.ts +77 -0
  132. package/src/utils/env.ts +6 -0
  133. package/src/utils/find-parent-node-of-type.ts +29 -0
  134. package/src/utils/find-parent-node.spec.ts +68 -0
  135. package/src/utils/find-parent-node.ts +55 -0
  136. package/src/utils/get-custom-selection.ts +19 -0
  137. package/src/utils/get-dom-api.ts +56 -0
  138. package/src/utils/get-id.spec.ts +14 -0
  139. package/src/utils/get-id.ts +13 -0
  140. package/src/utils/get-mark-type.ts +20 -0
  141. package/src/utils/get-node-type.ts +20 -0
  142. package/src/utils/get-node-types.ts +19 -0
  143. package/src/utils/includes-mark.ts +18 -0
  144. package/src/utils/is-at-block-start.ts +26 -0
  145. package/src/utils/is-in-code-block.ts +18 -0
  146. package/src/utils/is-mark-absent.spec.ts +53 -0
  147. package/src/utils/is-mark-absent.ts +42 -0
  148. package/src/utils/is-mark-active.ts +27 -0
  149. package/src/utils/is-node-active.ts +25 -0
  150. package/src/utils/is-subset.spec.ts +12 -0
  151. package/src/utils/is-subset.ts +11 -0
  152. package/src/utils/maybe-run.spec.ts +39 -0
  153. package/src/utils/maybe-run.ts +11 -0
  154. package/src/utils/merge-objects.spec.ts +30 -0
  155. package/src/utils/merge-objects.ts +11 -0
  156. package/src/utils/merge-specs.ts +35 -0
  157. package/src/utils/object-equal.spec.ts +26 -0
  158. package/src/utils/object-equal.ts +28 -0
  159. package/src/utils/output-spec.test.ts +95 -0
  160. package/src/utils/output-spec.ts +130 -0
  161. package/src/utils/parse.spec.ts +46 -0
  162. package/src/utils/parse.ts +321 -0
  163. package/src/utils/remove-undefined-values.spec.ts +15 -0
  164. package/src/utils/remove-undefined-values.ts +9 -0
  165. package/src/utils/set-selection-around.ts +11 -0
  166. package/src/utils/type-assertion.ts +91 -0
  167. package/src/utils/unicode.spec.ts +10 -0
  168. package/src/utils/unicode.ts +4 -0
  169. package/src/utils/with-skip-code-block.ts +15 -0
  170. package/dist/editor-OUH5V8BA.d.ts +0 -754
@@ -0,0 +1,84 @@
1
+ import {
2
+ Fragment,
3
+ Slice,
4
+ } from '@prosekit/pm/model'
5
+ import type {
6
+ Command,
7
+ Transaction,
8
+ } from '@prosekit/pm/state'
9
+ import { ReplaceAroundStep } from '@prosekit/pm/transform'
10
+
11
+ import type { CommandCreator } from '../types/extension-command'
12
+
13
+ /**
14
+ * @public
15
+ */
16
+ export interface UnsetBlockTypeOptions {
17
+ /**
18
+ * The start position of the document. By default it will be the start position of current selection.
19
+ */
20
+ from?: number
21
+
22
+ /**
23
+ * The end position of the document. By default it will be the end position of current selection.
24
+ */
25
+ to?: number
26
+ }
27
+
28
+ /**
29
+ * Returns a command that set the type of all textblocks between the given range
30
+ * to the default type (usually `paragraph`).
31
+ *
32
+ * @public
33
+ */
34
+ export function unsetBlockType(options?: UnsetBlockTypeOptions): Command {
35
+ return (state, dispatch) => {
36
+ const from = options?.from ?? state.selection.from
37
+ const to = options?.to ?? state.selection.to
38
+ if (from > to) return false
39
+
40
+ const tr = state.tr
41
+ if (unsetTextBlockType(tr, from, to)) {
42
+ dispatch?.(tr)
43
+ return true
44
+ }
45
+ return false
46
+ }
47
+ }
48
+
49
+ function unsetTextBlockType(
50
+ tr: Transaction,
51
+ from: number,
52
+ to: number,
53
+ ): boolean {
54
+ const mapFrom = tr.steps.length
55
+ tr.doc.nodesBetween(from, to, (node, pos, parent, index): boolean => {
56
+ if (!parent || !node.isTextblock) return true
57
+
58
+ const defaultType = parent.contentMatchAt(index).defaultType
59
+ if (
60
+ defaultType
61
+ && defaultType.isTextblock
62
+ && node.type !== defaultType
63
+ && defaultType.validContent(node.content)
64
+ ) {
65
+ const mapping = tr.mapping.slice(mapFrom)
66
+ const start = mapping.map(pos, 1)
67
+ const end = mapping.map(pos + node.nodeSize, 1)
68
+ const step = new ReplaceAroundStep(
69
+ start,
70
+ end,
71
+ start + 1,
72
+ end - 1,
73
+ new Slice(Fragment.from(defaultType.create()), 0, 0),
74
+ 1,
75
+ true,
76
+ )
77
+ tr.step(step)
78
+ }
79
+ return false
80
+ })
81
+ return tr.steps.length > mapFrom
82
+ }
83
+
84
+ unsetBlockType satisfies CommandCreator
@@ -0,0 +1,35 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from 'vitest'
6
+
7
+ import { setupTest } from '../testing'
8
+
9
+ describe('unsetMark', () => {
10
+ it('can unset marks', () => {
11
+ const { editor, m, n } = setupTest()
12
+
13
+ const doc1 = n.doc(
14
+ n.paragraph(
15
+ '<a>',
16
+ '1',
17
+ m.bold('2'),
18
+ m.italic('3'),
19
+ '<b>',
20
+ m.italic('4'),
21
+ m.italic('5'),
22
+ ),
23
+ )
24
+
25
+ const doc2 = n.doc(
26
+ n.paragraph('<a>', '123', '<b>', m.italic('4'), m.italic('5')),
27
+ )
28
+
29
+ editor.set(doc1)
30
+ expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
31
+
32
+ editor.commands.unsetMark()
33
+ expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
34
+ })
35
+ })
@@ -0,0 +1,38 @@
1
+ import type { Command } from '@prosekit/pm/state'
2
+
3
+ import type { CommandCreator } from '../types/extension-command'
4
+
5
+ /**
6
+ * @public
7
+ *
8
+ * Options for {@link unsetMark}.
9
+ */
10
+ export interface UnsetMarkOptions {
11
+ /**
12
+ * The start position of the document. By default it will be the start position of current selection.
13
+ */
14
+ from?: number
15
+
16
+ /**
17
+ * The end position of the document. By default it will be the end position of current selection.
18
+ */
19
+ to?: number
20
+ }
21
+
22
+ /**
23
+ * Returns a command that removes all marks.
24
+ *
25
+ * @public
26
+ */
27
+ export function unsetMark(options?: UnsetMarkOptions): Command {
28
+ return (state, dispatch) => {
29
+ const from = options?.from ?? state.selection.from
30
+ const to = options?.to ?? state.selection.to
31
+ if (from > to) return false
32
+
33
+ dispatch?.(state.tr.removeMark(from, to))
34
+ return true
35
+ }
36
+ }
37
+
38
+ unsetMark satisfies CommandCreator
@@ -0,0 +1,50 @@
1
+ import type {
2
+ Attrs,
3
+ NodeType,
4
+ } from '@prosekit/pm/model'
5
+ import type { Command } from '@prosekit/pm/state'
6
+ import { findWrapping } from '@prosekit/pm/transform'
7
+
8
+ import { getNodeType } from '../utils/get-node-type'
9
+
10
+ /**
11
+ * @public
12
+ */
13
+ export interface WrapOptions {
14
+ /**
15
+ * The node type to wrap the selected textblock with.
16
+ */
17
+ type: NodeType | string
18
+
19
+ /**
20
+ * @deprecated Use `nodeSpec` instead.
21
+ */
22
+ nodeType?: NodeType
23
+
24
+ /**
25
+ * Optional attributes to apply to the node.
26
+ */
27
+ attrs?: Attrs | null
28
+ }
29
+
30
+ /**
31
+ * Returns a command that wraps the selected textblock with the given node type.
32
+ *
33
+ * @param options
34
+ *
35
+ * @public
36
+ */
37
+ export function wrap(options: WrapOptions): Command {
38
+ return (state, dispatch) => {
39
+ const { $from, $to } = state.selection
40
+ const range = $from.blockRange($to)
41
+ if (!range) return false
42
+
43
+ const nodeType = getNodeType(state.schema, options.nodeType || options.type)
44
+ const wrapping = findWrapping(range, nodeType, options.attrs)
45
+ if (!wrapping) return false
46
+
47
+ dispatch?.(state.tr.wrap(range, wrapping))
48
+ return true
49
+ }
50
+ }
@@ -0,0 +1,143 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from 'vitest'
6
+
7
+ import { setupTest } from '../testing'
8
+ import { jsonFromNode } from '../utils/parse'
9
+
10
+ import type { NodeChild } from './action'
11
+
12
+ describe('NodeAction', () => {
13
+ const { editor, n } = setupTest()
14
+
15
+ it('can apply node', () => {
16
+ expect(n.heading('foo').toJSON()).toEqual({
17
+ type: 'heading',
18
+ content: [{ text: 'foo', type: 'text' }],
19
+ })
20
+ })
21
+
22
+ it('can apply node with attrs', () => {
23
+ expect(n.codeBlock({ language: 'javascript' }, 'foo').toJSON()).toEqual({
24
+ type: 'codeBlock',
25
+ attrs: { language: 'javascript' },
26
+ content: [{ text: 'foo', type: 'text' }],
27
+ })
28
+ })
29
+
30
+ it('can check node activity', () => {
31
+ editor.set(n.doc(n.heading('<a>foo<b>')))
32
+ expect(editor.nodes.heading.isActive()).toBe(true)
33
+ expect(editor.nodes.paragraph.isActive()).toBe(false)
34
+ })
35
+ })
36
+
37
+ describe('MarkAction', () => {
38
+ const { editor, m, n } = setupTest()
39
+
40
+ it('can apply mark', () => {
41
+ expect(n.p(m.bold('foo')).toJSON()).toEqual({
42
+ type: 'paragraph',
43
+ content: [{ marks: [{ type: 'bold' }], text: 'foo', type: 'text' }],
44
+ })
45
+ })
46
+
47
+ it('can apply mark with attrs', () => {
48
+ expect(
49
+ n.p(m.link({ href: 'https://example.com', target: '_blank', rel: 'noopener' }, 'foo')).toJSON(),
50
+ ).toEqual({
51
+ type: 'paragraph',
52
+ content: [
53
+ {
54
+ marks: [{ type: 'link', attrs: { href: 'https://example.com', target: '_blank', rel: 'noopener' } }],
55
+ text: 'foo',
56
+ type: 'text',
57
+ },
58
+ ],
59
+ })
60
+ })
61
+
62
+ it('can apply multiple marks', () => {
63
+ const json = jsonFromNode(n.p(m.bold(m.italic('foo'))))
64
+ const marks = json.content?.[0].marks?.map((mark) => mark.type)
65
+ expect(marks?.sort()).toEqual(['bold', 'italic'])
66
+ })
67
+
68
+ it('can apply the same mark multiple times', () => {
69
+ const href1 = 'https://example.com/1'
70
+ const href2 = 'https://example.com/2'
71
+ const link1 = (node: NodeChild) => m.link({ href: href1 }, node)
72
+ const link2 = (node: NodeChild) => m.link({ href: href2, target: '_blank', rel: 'noopener' }, node)
73
+
74
+ expect(n.p(link1('foo')).toJSON()).toEqual({
75
+ type: 'paragraph',
76
+ content: [
77
+ {
78
+ type: 'text',
79
+ text: 'foo',
80
+ marks: [{ attrs: { href: href1, target: null, rel: null }, type: 'link' }],
81
+ },
82
+ ],
83
+ })
84
+
85
+ expect(n.p(link2(link1('foo'))).toJSON()).toEqual({
86
+ type: 'paragraph',
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: 'foo',
91
+ marks: [{ attrs: { href: href2, target: '_blank', rel: 'noopener' }, type: 'link' }],
92
+ },
93
+ ],
94
+ })
95
+
96
+ expect(n.p(link2(link2(link2('foo')))).toJSON()).toEqual({
97
+ type: 'paragraph',
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: 'foo',
102
+ marks: [{ attrs: { href: href2, target: '_blank', rel: 'noopener' }, type: 'link' }],
103
+ },
104
+ ],
105
+ })
106
+ })
107
+
108
+ it('can check mark activity', () => {
109
+ editor.set(n.doc(n.p('<a>foo<b>')))
110
+ expect(editor.marks.bold.isActive()).toBe(false)
111
+ expect(editor.marks.italic.isActive()).toBe(false)
112
+
113
+ editor.set(n.doc(n.p('<a>', m.bold('foo'), '<b>')))
114
+ expect(editor.marks.bold.isActive()).toBe(true)
115
+ expect(editor.marks.italic.isActive()).toBe(false)
116
+ })
117
+
118
+ it('can check mark activity for cross-paragraph selection', () => {
119
+ editor.set(n.doc(n.p('<a>', m.bold('foo')), n.p(m.bold('foo'), '<b>')))
120
+
121
+ expect(editor.marks.bold.isActive()).toBe(true)
122
+ expect(editor.marks.italic.isActive()).toBe(false)
123
+ })
124
+
125
+ it('should not set isActive to true when only part of the text is marked', () => {
126
+ editor.set(n.doc(n.p('<a>', 'foo', m.bold('bar'), 'baz', '<b>')))
127
+ expect(editor.marks.bold.isActive()).toBe(false)
128
+
129
+ editor.set(n.doc(n.p('<a>', m.bold('foo'), 'bar', '<b>')))
130
+ expect(editor.marks.bold.isActive()).toBe(false)
131
+
132
+ editor.set(n.doc(n.p('<a>', 'foo', m.bold('bar'), '<b>')))
133
+ expect(editor.marks.bold.isActive()).toBe(false)
134
+
135
+ editor.set(n.doc(n.p('<a>', m.bold('foo', 'bar'), '<b>')))
136
+ expect(editor.marks.bold.isActive()).toBe(true)
137
+ })
138
+
139
+ it('should not set isActive to true when multiple empty paragraphs are selected', () => {
140
+ editor.set(n.doc(n.p('<a>'), n.p(''), n.p('<b>')))
141
+ expect(editor.marks.bold.isActive()).toBe(false)
142
+ })
143
+ })
@@ -0,0 +1,248 @@
1
+ import type {
2
+ Attrs,
3
+ Mark,
4
+ MarkType,
5
+ NodeType,
6
+ ProseMirrorNode,
7
+ Schema,
8
+ } from '@prosekit/pm/model'
9
+ import type { EditorState } from '@prosekit/pm/state'
10
+ import mapValues from 'just-map-values'
11
+
12
+ import { ProseKitError } from '../error'
13
+ import type { AnyAttrs } from '../types/attrs'
14
+ import { assert } from '../utils/assert'
15
+ import { isMarkActive } from '../utils/is-mark-active'
16
+ import { isNodeActive } from '../utils/is-node-active'
17
+ import { isProseMirrorNode } from '../utils/type-assertion'
18
+
19
+ /**
20
+ * Available children parameters for {@link NodeAction} and {@link MarkAction}.
21
+ *
22
+ * @public
23
+ */
24
+ export type NodeChild = ProseMirrorNode | string | NodeChild[]
25
+
26
+ /**
27
+ * A function for creating a node with optional attributes and any number of
28
+ * children.
29
+ *
30
+ * It also has a `isActive` method for checking if the node is active in the
31
+ * current editor selection.
32
+ *
33
+ * @public
34
+ */
35
+ export interface NodeAction<Attrs extends AnyAttrs = AnyAttrs> {
36
+ /**
37
+ * Creates a node with attributes and any number of children.
38
+ */
39
+ (attrs: Attrs | null, ...children: NodeChild[]): ProseMirrorNode
40
+
41
+ /**
42
+ * Creates a node with any number of children.
43
+ */
44
+ (...children: NodeChild[]): ProseMirrorNode
45
+
46
+ /**
47
+ * Checks if the node is active in the current editor selection. If the
48
+ * optional `attrs` parameter is provided, it will check if the node is active
49
+ * with the given attributes.
50
+ */
51
+ isActive: (attrs?: Attrs) => boolean
52
+ }
53
+
54
+ /**
55
+ * A function for applying a mark with optional attributes and any number of
56
+ * children.
57
+ *
58
+ * It also has a `isActive` method for checking if the mark is active in the
59
+ * current editor selection.
60
+ *
61
+ * @public
62
+ */
63
+ export interface MarkAction<Attrs extends AnyAttrs = AnyAttrs> {
64
+ /**
65
+ * Applies a mark with attributes and any number of children.
66
+ */
67
+ (attrs: Attrs | null, ...children: NodeChild[]): ProseMirrorNode[]
68
+
69
+ /**
70
+ * Applies a mark with any number of children.
71
+ */
72
+ (...children: NodeChild[]): ProseMirrorNode[]
73
+
74
+ /**
75
+ * Checks if the mark is active in the current editor selection. If the
76
+ * optional `attrs` parameter is provided, it will check if the mark is active
77
+ * with the given attributes.
78
+ */
79
+ isActive: (attrs?: Attrs) => boolean
80
+ }
81
+
82
+ /**
83
+ * @deprecated Use type {@link NodeAction} instead.
84
+ */
85
+ export type NodeBuilder = NodeAction
86
+
87
+ /**
88
+ * @deprecated Use type {@link MarkAction} instead.
89
+ */
90
+ export type MarkBuilder = MarkAction
91
+
92
+ /**
93
+ * @internal
94
+ */
95
+ export function createNodeActions(
96
+ schema: Schema,
97
+ getState: GetStateFunction,
98
+ createNode: CreateNodeFunction = defaultCreateNode,
99
+ ): Record<string, NodeAction> {
100
+ return mapValues(schema.nodes, (type) => createNodeAction(type, getState, createNode))
101
+ }
102
+
103
+ function createNodeAction(
104
+ type: NodeType,
105
+ getState: GetStateFunction,
106
+ createNode: CreateNodeFunction,
107
+ ): NodeAction {
108
+ const action = (
109
+ ...args: [Attrs | NodeChild | null | undefined, ...NodeChild[]]
110
+ ) => buildNode(type, args, createNode)
111
+ action.isActive = (attrs?: Attrs) => {
112
+ const state = getState()
113
+ return state ? isNodeActive(state, type, attrs) : false
114
+ }
115
+ return action
116
+ }
117
+
118
+ /**
119
+ * @internal
120
+ */
121
+ export function createMarkActions(
122
+ schema: Schema,
123
+ getState: GetStateFunction,
124
+ applyMark: ApplyMarkFunction = defaultApplyMark,
125
+ ): Record<string, MarkAction> {
126
+ return mapValues(schema.marks, (type) => createMarkAction(type, getState, applyMark))
127
+ }
128
+
129
+ function createMarkAction(
130
+ type: MarkType,
131
+ getState: GetStateFunction,
132
+ applyMark: ApplyMarkFunction,
133
+ ): MarkAction {
134
+ const action = (
135
+ ...args: [Attrs | NodeChild | null | undefined, ...NodeChild[]]
136
+ ) => buildMark(type, args, applyMark)
137
+ action.isActive = (attrs?: Attrs) => {
138
+ const state = getState()
139
+ return state ? isMarkActive(state, type, attrs) : false
140
+ }
141
+ return action
142
+ }
143
+
144
+ function buildMark(
145
+ type: MarkType,
146
+ args: [Attrs | NodeChild | null | undefined, ...NodeChild[]],
147
+ applyMark: ApplyMarkFunction,
148
+ ): ProseMirrorNode[] {
149
+ const [attrs, children] = normalizeArgs(args)
150
+ const mark = type.create(attrs)
151
+ return applyMark(mark, flattenChildren(type.schema, children))
152
+ }
153
+
154
+ /**
155
+ * @internal
156
+ */
157
+ export type ApplyMarkFunction = (
158
+ mark: Mark,
159
+ children: ProseMirrorNode[],
160
+ ) => ProseMirrorNode[]
161
+
162
+ const defaultApplyMark: ApplyMarkFunction = (
163
+ mark: Mark,
164
+ children: ProseMirrorNode[],
165
+ ): ProseMirrorNode[] => {
166
+ return children.map((node) => node.mark(mark.addToSet(node.marks)))
167
+ }
168
+
169
+ function buildNode(
170
+ type: NodeType,
171
+ args: [Attrs | NodeChild | null | undefined, ...NodeChild[]],
172
+ createNode: CreateNodeFunction,
173
+ ): ProseMirrorNode {
174
+ const [attrs, children] = normalizeArgs(args)
175
+ return createNode(type, attrs, flattenChildren(type.schema, children))
176
+ }
177
+
178
+ /**
179
+ * @internal
180
+ */
181
+ export type CreateNodeFunction = (
182
+ type: NodeType,
183
+ attrs: Attrs | null,
184
+ children: ProseMirrorNode[],
185
+ ) => ProseMirrorNode
186
+
187
+ type GetStateFunction = () => EditorState | null | undefined
188
+
189
+ const defaultCreateNode: CreateNodeFunction = (
190
+ type: NodeType,
191
+ attrs: Attrs | null,
192
+ children: ProseMirrorNode[],
193
+ ) => {
194
+ const node = type.createAndFill(attrs, children)
195
+ assert(node, `Failed to create node ${type.name}`)
196
+ return node
197
+ }
198
+
199
+ function flattenChildren(
200
+ schema: Schema,
201
+ children: NodeChild[],
202
+ ): Array<ProseMirrorNode> {
203
+ const nodes: Array<ProseMirrorNode> = []
204
+
205
+ for (const child of children) {
206
+ if (typeof child === 'string') {
207
+ if (child) {
208
+ nodes.push(schema.text(child, null))
209
+ }
210
+ } else if (Array.isArray(child)) {
211
+ nodes.push(...flattenChildren(schema, child))
212
+ } else if (isProseMirrorNode(child)) {
213
+ nodes.push(child)
214
+ } else {
215
+ throw new ProseKitError(`Invalid node child: ${typeof child}`)
216
+ }
217
+ }
218
+
219
+ return nodes
220
+ }
221
+
222
+ function normalizeArgs(
223
+ args: [Attrs | NodeChild | null | undefined, ...NodeChild[]],
224
+ ): [Attrs | null, NodeChild[]] {
225
+ const [attrs, ...children] = args
226
+ if (isNodeChild(attrs)) {
227
+ children.unshift(attrs)
228
+ return [null, children]
229
+ } else if (typeof attrs === 'object') {
230
+ return [attrs, children]
231
+ } else {
232
+ return [null, children]
233
+ }
234
+ }
235
+
236
+ function isNodeChild(
237
+ value: Attrs | NodeChild | null | undefined,
238
+ ): value is NodeChild {
239
+ if (!value) {
240
+ return false
241
+ }
242
+
243
+ return (
244
+ typeof value === 'string'
245
+ || Array.isArray(value)
246
+ || isProseMirrorNode(value)
247
+ )
248
+ }