@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,89 @@
1
+ import type { Command } from '@prosekit/pm/state'
2
+ import {
3
+ describe,
4
+ expect,
5
+ it,
6
+ vi,
7
+ } from 'vitest'
8
+
9
+ import { createEditor } from '../editor/editor'
10
+ import { defineTestExtension } from '../testing'
11
+
12
+ import {
13
+ defineKeymap,
14
+ type Keymap,
15
+ } from './keymap'
16
+
17
+ describe('keymap', () => {
18
+ it('can register and unregister keymap', () => {
19
+ const div = document.body.appendChild(document.createElement('div'))
20
+ const extension = defineTestExtension()
21
+ const editor = createEditor({ extension })
22
+ editor.mount(div)
23
+
24
+ const command1: Command = vi.fn(() => false)
25
+ const command2: Command = vi.fn(() => false)
26
+
27
+ const keymap1: Keymap = { Enter: command1 }
28
+ const keymap2: Keymap = { Enter: command2 }
29
+
30
+ const extension1 = defineKeymap(keymap1)
31
+ const extension2 = defineKeymap(keymap2)
32
+
33
+ const dispose1 = editor.use(extension1)
34
+
35
+ editor.view.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
36
+ expect(command1).toHaveBeenCalledTimes(1)
37
+ expect(command2).toHaveBeenCalledTimes(0)
38
+
39
+ const dispose2 = editor.use(extension2)
40
+
41
+ editor.view.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
42
+ expect(command1).toHaveBeenCalledTimes(2)
43
+ expect(command2).toHaveBeenCalledTimes(1)
44
+
45
+ dispose1()
46
+
47
+ editor.view.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
48
+ expect(command1).toHaveBeenCalledTimes(2)
49
+ expect(command2).toHaveBeenCalledTimes(2)
50
+
51
+ dispose2()
52
+
53
+ editor.view.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
54
+ expect(command1).toHaveBeenCalledTimes(2)
55
+ expect(command2).toHaveBeenCalledTimes(2)
56
+ })
57
+
58
+ it('can skip unnecessary plugin update', () => {
59
+ const div = document.body.appendChild(document.createElement('div'))
60
+ const extension = defineTestExtension()
61
+ const editor = createEditor({ extension })
62
+ editor.mount(div)
63
+
64
+ const command1: Command = vi.fn(() => false)
65
+ const command2: Command = vi.fn(() => false)
66
+
67
+ const keymap1: Keymap = { Enter: command1 }
68
+ const keymap2: Keymap = { Enter: command2 }
69
+
70
+ const extension1 = defineKeymap(keymap1)
71
+ const extension2 = defineKeymap(keymap2)
72
+
73
+ const dispose1 = editor.use(extension1)
74
+ const plugins1 = [...editor.view.state.plugins]
75
+
76
+ const dispose2 = editor.use(extension2)
77
+ const plugins2 = [...editor.view.state.plugins]
78
+
79
+ dispose2()
80
+ const plugins3 = [...editor.view.state.plugins]
81
+
82
+ dispose1()
83
+ const plugins4 = [...editor.view.state.plugins]
84
+
85
+ expect(plugins1).toEqual(plugins2)
86
+ expect(plugins2).toEqual(plugins3)
87
+ expect(plugins3).toEqual(plugins4)
88
+ })
89
+ })
@@ -0,0 +1,96 @@
1
+ import { chainCommands } from '@prosekit/pm/commands'
2
+ import { keydownHandler } from '@prosekit/pm/keymap'
3
+ import {
4
+ Plugin,
5
+ PluginKey,
6
+ type Command,
7
+ } from '@prosekit/pm/state'
8
+ import type { EditorView } from '@prosekit/pm/view'
9
+ import mapValues from 'just-map-values'
10
+
11
+ import {
12
+ defineFacet,
13
+ type Facet,
14
+ } from '../facets/facet'
15
+ import { defineFacetPayload } from '../facets/facet-extension'
16
+ import type { PlainExtension } from '../types/extension'
17
+ import { toReversed } from '../utils/array'
18
+
19
+ import {
20
+ pluginFacet,
21
+ type PluginPayload,
22
+ } from './plugin'
23
+
24
+ /**
25
+ * @public
26
+ */
27
+ export interface Keymap {
28
+ [key: string]: Command
29
+ }
30
+
31
+ /**
32
+ * @public
33
+ */
34
+ export function defineKeymap(keymap: Keymap): PlainExtension {
35
+ return defineFacetPayload(keymapFacet, [keymap]) as PlainExtension
36
+ }
37
+
38
+ /**
39
+ * @internal
40
+ */
41
+ export type KeymapPayload = Keymap
42
+
43
+ /**
44
+ * @internal
45
+ */
46
+ export const keymapFacet: Facet<KeymapPayload, PluginPayload> = defineFacet<
47
+ KeymapPayload,
48
+ PluginPayload
49
+ >({
50
+ reduce: () => {
51
+ type Handler = (view: EditorView, event: KeyboardEvent) => boolean
52
+
53
+ let handler: Handler | undefined
54
+
55
+ const handlerWrapper: Handler = (view, event) => {
56
+ if (handler) return handler(view, event)
57
+ return false
58
+ }
59
+
60
+ const plugin = new Plugin({
61
+ key: keymapPluginKey,
62
+ props: { handleKeyDown: handlerWrapper },
63
+ })
64
+
65
+ return (keymaps: Keymap[]) => {
66
+ handler = keydownHandler(
67
+ mergeKeymaps(
68
+ // The keymap at the end have a higher priority.
69
+ toReversed(keymaps),
70
+ ),
71
+ )
72
+ return plugin
73
+ }
74
+ },
75
+ parent: pluginFacet,
76
+ singleton: true,
77
+ })
78
+
79
+ function mergeKeymaps(keymaps: Keymap[]): Keymap {
80
+ const bindings: Record<string, Command[]> = {}
81
+
82
+ for (const keymap of keymaps) {
83
+ for (const [key, command] of Object.entries(keymap)) {
84
+ const commands = bindings[key] || (bindings[key] = [])
85
+ commands.push(command)
86
+ }
87
+ }
88
+
89
+ return mapValues(bindings, mergeCommands)
90
+ }
91
+
92
+ function mergeCommands(commands: Command[]): Command {
93
+ return chainCommands(...commands)
94
+ }
95
+
96
+ const keymapPluginKey = new PluginKey('prosekit-keymap')
@@ -0,0 +1,177 @@
1
+ import type {
2
+ DOMOutputSpec,
3
+ TagParseRule,
4
+ } from '@prosekit/pm/model'
5
+ import formatHTML from 'diffable-html'
6
+ import {
7
+ describe,
8
+ expect,
9
+ it,
10
+ } from 'vitest'
11
+
12
+ import { union } from '../editor/union'
13
+ import {
14
+ defineDoc,
15
+ defineParagraph,
16
+ defineText,
17
+ setupTestFromExtension,
18
+ } from '../testing'
19
+
20
+ import {
21
+ defineMarkAttr,
22
+ defineMarkSpec,
23
+ } from './mark-spec'
24
+
25
+ describe('defineMarkSpec', () => {
26
+ it('can merge mark specs', () => {
27
+ const toDOM1 = (): DOMOutputSpec => ['strong', { 'data-ext1': '' }]
28
+ const toDOM2 = (): DOMOutputSpec => ['strong', { 'data-ext2': '' }]
29
+ const parseDOM1: TagParseRule = { tag: 'strong[data-ext1]' }
30
+ const parseDOM2: TagParseRule = { tag: 'strong[data-ext2]' }
31
+ const toDebugString2 = () => 'ext2'
32
+
33
+ const ext1 = defineMarkSpec({
34
+ name: 'bold',
35
+ parseDOM: [parseDOM1],
36
+ toDOM: toDOM1,
37
+ attrs: {
38
+ foo: { default: undefined },
39
+ bar: { default: 'bar' },
40
+ baz: { default: 'baz:1 ' },
41
+ },
42
+ })
43
+ const ext2 = defineMarkSpec({
44
+ name: 'bold',
45
+ parseDOM: [parseDOM2],
46
+ toDOM: toDOM2,
47
+ toDebugString: toDebugString2,
48
+ attrs: {
49
+ foo: { default: 'foo' },
50
+ bar: { default: undefined },
51
+ baz: { default: 'baz:2' },
52
+ },
53
+ })
54
+
55
+ const extension = union(
56
+ ext1,
57
+ ext2,
58
+ defineDoc(),
59
+ defineText(),
60
+ defineParagraph(),
61
+ )
62
+ const schema = extension.schema
63
+ expect(schema).toBeTruthy()
64
+ expect(schema?.spec.marks.get('bold')).toEqual({
65
+ parseDOM: [parseDOM1, parseDOM2],
66
+ toDOM: toDOM2,
67
+ toDebugString: toDebugString2,
68
+ attrs: {
69
+ foo: { default: 'foo' },
70
+ bar: { default: 'bar' },
71
+ baz: { default: 'baz:2' },
72
+ },
73
+ })
74
+ })
75
+ })
76
+
77
+ describe('defineMarkAttr', () => {
78
+ it('can add a new attribute', () => {
79
+ const boldExt = defineMarkSpec({
80
+ name: 'bold',
81
+ parseDOM: [{ tag: 'strong' }],
82
+ toDOM() {
83
+ return ['strong', 0]
84
+ },
85
+ })
86
+ const textColorExt = defineMarkAttr({
87
+ type: 'bold',
88
+ attr: 'textColor',
89
+ default: 'black',
90
+ toDOM: (value) => ['style', `color: ${value}`],
91
+ parseDOM: (node: HTMLElement) => node.style.color,
92
+ })
93
+ const backgroundColorExt = defineMarkAttr({
94
+ type: 'bold',
95
+ attr: 'backgroundColor',
96
+ default: 'white',
97
+ toDOM: (value) => ['style', `background-color: ${value}`],
98
+ parseDOM: (node: HTMLElement) => node.style.backgroundColor,
99
+ })
100
+ const nodeIdExt = defineMarkAttr<'bold', 'markId', string | null>({
101
+ type: 'bold',
102
+ attr: 'markId',
103
+ default: null,
104
+ toDOM: (value) => (value ? ['data-mark-id', value] : null),
105
+ parseDOM: (node: HTMLElement) => node.dataset.markId || null,
106
+ })
107
+ const extension = union(
108
+ defineDoc(),
109
+ defineText(),
110
+ defineParagraph(),
111
+ textColorExt,
112
+ backgroundColorExt,
113
+ nodeIdExt,
114
+ boldExt,
115
+ )
116
+ const { editor } = setupTestFromExtension(extension)
117
+
118
+ expect(Object.keys(editor.schema.marks.bold.spec.attrs || {}))
119
+ .toMatchInlineSnapshot(`
120
+ [
121
+ "textColor",
122
+ "backgroundColor",
123
+ "markId",
124
+ ]
125
+ `)
126
+
127
+ editor.setContent(
128
+ '<p><strong data-mark-id="123" style="background-color:blue;color:red;">Hello</strong></p>',
129
+ )
130
+
131
+ const json1 = editor.getDocJSON()
132
+ const html1 = editor.getDocHTML()
133
+ expect(json1).toMatchInlineSnapshot(`
134
+ {
135
+ "content": [
136
+ {
137
+ "content": [
138
+ {
139
+ "marks": [
140
+ {
141
+ "attrs": {
142
+ "backgroundColor": "blue",
143
+ "markId": "123",
144
+ "textColor": "red",
145
+ },
146
+ "type": "bold",
147
+ },
148
+ ],
149
+ "text": "Hello",
150
+ "type": "text",
151
+ },
152
+ ],
153
+ "type": "paragraph",
154
+ },
155
+ ],
156
+ "type": "doc",
157
+ }
158
+ `)
159
+ expect(formatHTML(html1)).toMatchInlineSnapshot(`
160
+ "
161
+ <div>
162
+ <p>
163
+ <strong
164
+ data-mark-id="123"
165
+ style="background-color: blue; color: red;"
166
+ >
167
+ Hello
168
+ </strong>
169
+ </p>
170
+ </div>
171
+ "
172
+ `)
173
+ editor.setContent(html1)
174
+ const json2 = editor.getDocJSON()
175
+ expect(json2).toEqual(json1)
176
+ })
177
+ })
@@ -0,0 +1,181 @@
1
+ import type {
2
+ MarkSpec,
3
+ ParseRule,
4
+ SchemaSpec,
5
+ } from '@prosekit/pm/model'
6
+ import clone from 'just-clone'
7
+ import OrderedMap from 'orderedmap'
8
+
9
+ import { defineFacet } from '../facets/facet'
10
+ import { defineFacetPayload } from '../facets/facet-extension'
11
+ import { schemaSpecFacet } from '../facets/schema-spec'
12
+ import type {
13
+ AnyAttrs,
14
+ AttrSpec,
15
+ } from '../types/attrs'
16
+ import type { Extension } from '../types/extension'
17
+ import { groupBy } from '../utils/array-grouping'
18
+ import { assert } from '../utils/assert'
19
+ import { mergeSpecs } from '../utils/merge-specs'
20
+ import {
21
+ wrapOutputSpecAttrs,
22
+ wrapTagParseRuleAttrs,
23
+ } from '../utils/output-spec'
24
+ import { isNotNullish } from '../utils/type-assertion'
25
+
26
+ /**
27
+ * @public
28
+ */
29
+ export interface MarkSpecOptions<
30
+ MarkName extends string = string,
31
+ Attrs extends AnyAttrs = AnyAttrs,
32
+ > extends MarkSpec {
33
+ /**
34
+ * The name of the mark type.
35
+ */
36
+ name: MarkName
37
+
38
+ /**
39
+ * The attributes that marks of this type get.
40
+ */
41
+ attrs?: { [K in keyof Attrs]: AttrSpec<Attrs[K]> }
42
+ }
43
+
44
+ /**
45
+ * @public
46
+ */
47
+ export interface MarkAttrOptions<
48
+ MarkName extends string = string,
49
+ AttrName extends string = string,
50
+ AttrType = any,
51
+ > extends AttrSpec<AttrType> {
52
+ /**
53
+ * The name of the mark type.
54
+ */
55
+ type: MarkName
56
+
57
+ /**
58
+ * The name of the attribute.
59
+ */
60
+ attr: AttrName
61
+
62
+ /**
63
+ * Returns the attribute key and value to be set on the HTML element.
64
+ *
65
+ * If the returned `key` is `"style"`, the value is a string of CSS properties and will
66
+ * be prepended to the existing `style` attribute on the DOM node.
67
+ *
68
+ * @param value - The value of the attribute of current ProseMirror node.
69
+ */
70
+ toDOM?: (value: AttrType) => [key: string, value: string] | null | undefined
71
+
72
+ /**
73
+ * Parses the attribute value from the DOM.
74
+ */
75
+ parseDOM?: (node: HTMLElement) => AttrType
76
+ }
77
+
78
+ /**
79
+ * @public
80
+ */
81
+ export function defineMarkSpec<
82
+ Mark extends string,
83
+ Attrs extends AnyAttrs = AnyAttrs,
84
+ >(
85
+ options: MarkSpecOptions<Mark, Attrs>,
86
+ ): Extension<{
87
+ Marks: { [K in Mark]: Attrs }
88
+ }> {
89
+ const payload: MarkSpecPayload = [options, undefined]
90
+ return defineFacetPayload(markSpecFacet, [payload]) as Extension<{
91
+ Marks: any
92
+ }>
93
+ }
94
+
95
+ /**
96
+ * @public
97
+ */
98
+ export function defineMarkAttr<
99
+ MarkType extends string = string,
100
+ AttrName extends string = string,
101
+ AttrType = any,
102
+ >(
103
+ options: MarkAttrOptions<MarkType, AttrName, AttrType>,
104
+ ): Extension<{
105
+ Marks: { [K in MarkType]: AttrType }
106
+ }> {
107
+ const payload: MarkSpecPayload = [undefined, options]
108
+ return defineFacetPayload(markSpecFacet, [payload]) as Extension<{
109
+ Marks: any
110
+ }>
111
+ }
112
+
113
+ type MarkSpecPayload = [
114
+ MarkSpecOptions | undefined,
115
+ MarkAttrOptions | undefined,
116
+ ]
117
+
118
+ const markSpecFacet = defineFacet<MarkSpecPayload, SchemaSpec>({
119
+ reducer: (payloads: MarkSpecPayload[]): SchemaSpec => {
120
+ let specs = OrderedMap.from<MarkSpec>({})
121
+
122
+ const specPayloads = payloads.map((input) => input[0]).filter(isNotNullish)
123
+ const attrPayloads = payloads.map((input) => input[1]).filter(isNotNullish)
124
+
125
+ for (const { name, ...spec } of specPayloads) {
126
+ const prevSpec = specs.get(name)
127
+ if (prevSpec) {
128
+ specs = specs.update(name, mergeSpecs(prevSpec, spec))
129
+ } else {
130
+ // The latest spec has the highest priority, so we put it at the start
131
+ // of the map.
132
+ specs = specs.addToStart(name, spec)
133
+ }
134
+ }
135
+
136
+ const groupedAttrs = groupBy(attrPayloads, (payload) => payload.type)
137
+
138
+ for (const [type, attrs] of Object.entries(groupedAttrs)) {
139
+ if (!attrs) continue
140
+
141
+ const maybeSpec = specs.get(type)
142
+ assert(maybeSpec, `Mark type ${type} must be defined`)
143
+ const spec = clone(maybeSpec)
144
+
145
+ if (!spec.attrs) {
146
+ spec.attrs = {}
147
+ }
148
+
149
+ for (const attr of attrs) {
150
+ spec.attrs[attr.attr] = {
151
+ default: attr.default as unknown,
152
+ validate: attr.validate,
153
+ }
154
+ }
155
+
156
+ if (spec.toDOM) {
157
+ spec.toDOM = wrapOutputSpecAttrs(spec.toDOM, attrs)
158
+ }
159
+
160
+ if (spec.parseDOM) {
161
+ spec.parseDOM = spec.parseDOM.map((rule) => wrapParseRuleAttrs(rule, attrs))
162
+ }
163
+
164
+ specs = specs.update(type, spec)
165
+ }
166
+
167
+ return { marks: specs, nodes: {} }
168
+ },
169
+ parent: schemaSpecFacet,
170
+ singleton: true,
171
+ })
172
+
173
+ function wrapParseRuleAttrs(
174
+ rule: ParseRule,
175
+ attrs: MarkAttrOptions[],
176
+ ): ParseRule {
177
+ if (rule.tag) {
178
+ return wrapTagParseRuleAttrs(rule, attrs)
179
+ }
180
+ return rule
181
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ PluginKey,
3
+ ProseMirrorPlugin,
4
+ } from '@prosekit/pm/state'
5
+ import type { MarkViewConstructor } from '@prosekit/pm/view'
6
+
7
+ import { defineFacet } from '../facets/facet'
8
+ import { defineFacetPayload } from '../facets/facet-extension'
9
+ import type { Extension } from '../types/extension'
10
+ import { isNotNullish } from '../utils/type-assertion'
11
+
12
+ import {
13
+ pluginFacet,
14
+ type PluginPayload,
15
+ } from './plugin'
16
+
17
+ /**
18
+ * @internal
19
+ */
20
+ export type MarkViewFactoryOptions<T> = {
21
+ group: string
22
+ factory: (args: T) => MarkViewConstructor
23
+ }
24
+
25
+ /**
26
+ * @internal
27
+ */
28
+ export type MarkViewComponentOptions<T> = {
29
+ group: string
30
+ name: string
31
+ args: T
32
+ }
33
+
34
+ type MarkViewFactoryInput = [
35
+ MarkViewFactoryOptions<any> | null,
36
+ MarkViewComponentOptions<any> | null,
37
+ ]
38
+
39
+ /**
40
+ * @internal
41
+ */
42
+ export function defineMarkViewFactory<T>(
43
+ options: MarkViewFactoryOptions<T>,
44
+ ): Extension {
45
+ const input: MarkViewFactoryInput = [options, null]
46
+ return defineFacetPayload(markViewFactoryFacet, [input])
47
+ }
48
+
49
+ /**
50
+ * @internal
51
+ */
52
+ export function defineMarkViewComponent<T>(
53
+ options: MarkViewComponentOptions<T>,
54
+ ): Extension {
55
+ const input: MarkViewFactoryInput = [null, options]
56
+ return defineFacetPayload(markViewFactoryFacet, [input])
57
+ }
58
+
59
+ const isServer = typeof window === 'undefined'
60
+
61
+ const markViewFactoryFacet = defineFacet<MarkViewFactoryInput, PluginPayload>({
62
+ reducer: (inputs: MarkViewFactoryInput[]): PluginPayload => {
63
+ // Don't register mark views on the server
64
+ if (isServer) return []
65
+
66
+ const markViews: { [markName: string]: MarkViewConstructor } = {}
67
+
68
+ const factories = inputs.map((x) => x[0]).filter(isNotNullish)
69
+ const options = inputs.map((x) => x[1]).filter(isNotNullish)
70
+
71
+ for (const { group, name, args } of options) {
72
+ const factory = factories.find((factory) => factory.group === group)
73
+ if (!factory) continue
74
+ markViews[name] = factory.factory(args)
75
+ }
76
+
77
+ return () => [
78
+ new ProseMirrorPlugin({
79
+ key: new PluginKey('prosekit-mark-view-effect'),
80
+ props: { markViews },
81
+ }),
82
+ ]
83
+ },
84
+ parent: pluginFacet,
85
+ })
@@ -0,0 +1,43 @@
1
+ import {
2
+ PluginKey,
3
+ ProseMirrorPlugin,
4
+ } from '@prosekit/pm/state'
5
+ import type { MarkViewConstructor } from '@prosekit/pm/view'
6
+
7
+ import { defineFacet } from '../facets/facet'
8
+ import { defineFacetPayload } from '../facets/facet-extension'
9
+ import type { Extension } from '../types/extension'
10
+
11
+ import {
12
+ pluginFacet,
13
+ type PluginPayload,
14
+ } from './plugin'
15
+
16
+ export interface MarkViewOptions {
17
+ name: string
18
+ constructor: MarkViewConstructor
19
+ }
20
+
21
+ export function defineMarkView(options: MarkViewOptions): Extension {
22
+ return defineFacetPayload(markViewFacet, [options])
23
+ }
24
+
25
+ const markViewFacet = defineFacet<MarkViewOptions, PluginPayload>({
26
+ reducer: (inputs: MarkViewOptions[]): PluginPayload => {
27
+ const markViews: { [markName: string]: MarkViewConstructor } = {}
28
+
29
+ for (const input of inputs) {
30
+ if (!markViews[input.name]) {
31
+ markViews[input.name] = input.constructor
32
+ }
33
+ }
34
+
35
+ return () => [
36
+ new ProseMirrorPlugin({
37
+ key: new PluginKey('prosekit-mark-view'),
38
+ props: { markViews },
39
+ }),
40
+ ]
41
+ },
42
+ parent: pluginFacet,
43
+ })