@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.
- package/dist/editor-CfkZ4TNU.d.ts +748 -0
- package/dist/editor-CfkZ4TNU.d.ts.map +1 -0
- package/dist/{editor-DbMrpnmL.js → editor-CizSwUN8.js} +102 -192
- package/dist/editor-CizSwUN8.js.map +1 -0
- package/dist/prosekit-core-test.d.ts +20 -19
- package/dist/prosekit-core-test.d.ts.map +1 -0
- package/dist/prosekit-core-test.js +4 -5
- package/dist/prosekit-core-test.js.map +1 -0
- package/dist/prosekit-core.d.ts +782 -757
- package/dist/prosekit-core.d.ts.map +1 -0
- package/dist/prosekit-core.js +30 -45
- package/dist/prosekit-core.js.map +1 -0
- package/package.json +14 -11
- package/src/commands/add-mark.ts +53 -0
- package/src/commands/expand-mark.ts +96 -0
- package/src/commands/insert-default-block.spec.ts +102 -0
- package/src/commands/insert-default-block.ts +49 -0
- package/src/commands/insert-node.ts +71 -0
- package/src/commands/insert-text.ts +24 -0
- package/src/commands/remove-mark.ts +54 -0
- package/src/commands/remove-node.ts +43 -0
- package/src/commands/select-all.ts +16 -0
- package/src/commands/set-block-type.ts +64 -0
- package/src/commands/set-node-attrs.ts +68 -0
- package/src/commands/toggle-mark.ts +65 -0
- package/src/commands/toggle-node.ts +47 -0
- package/src/commands/toggle-wrap.spec.ts +35 -0
- package/src/commands/toggle-wrap.ts +42 -0
- package/src/commands/unset-block-type.spec.ts +49 -0
- package/src/commands/unset-block-type.ts +84 -0
- package/src/commands/unset-mark.spec.ts +35 -0
- package/src/commands/unset-mark.ts +38 -0
- package/src/commands/wrap.ts +50 -0
- package/src/editor/action.spec.ts +143 -0
- package/src/editor/action.ts +248 -0
- package/src/editor/editor.spec.ts +186 -0
- package/src/editor/editor.ts +563 -0
- package/src/editor/union.spec.ts +108 -0
- package/src/editor/union.ts +47 -0
- package/src/editor/with-priority.ts +25 -0
- package/src/error.ts +28 -0
- package/src/extensions/clipboard-serializer.ts +107 -0
- package/src/extensions/command.ts +121 -0
- package/src/extensions/default-state.spec.ts +60 -0
- package/src/extensions/default-state.ts +76 -0
- package/src/extensions/doc.ts +31 -0
- package/src/extensions/events/doc-change.ts +34 -0
- package/src/extensions/events/dom-event.spec.ts +70 -0
- package/src/extensions/events/dom-event.ts +117 -0
- package/src/extensions/events/editor-event.ts +293 -0
- package/src/extensions/events/focus.spec.ts +50 -0
- package/src/extensions/events/focus.ts +28 -0
- package/src/extensions/events/plugin-view.ts +132 -0
- package/src/extensions/history.ts +81 -0
- package/src/extensions/keymap-base.ts +60 -0
- package/src/extensions/keymap.spec.ts +89 -0
- package/src/extensions/keymap.ts +96 -0
- package/src/extensions/mark-spec.spec.ts +177 -0
- package/src/extensions/mark-spec.ts +181 -0
- package/src/extensions/mark-view-effect.ts +85 -0
- package/src/extensions/mark-view.ts +43 -0
- package/src/extensions/node-spec.spec.ts +224 -0
- package/src/extensions/node-spec.ts +199 -0
- package/src/extensions/node-view-effect.ts +85 -0
- package/src/extensions/node-view.ts +43 -0
- package/src/extensions/paragraph.ts +61 -0
- package/src/extensions/plugin.ts +91 -0
- package/src/extensions/text.ts +34 -0
- package/src/facets/base-extension.ts +54 -0
- package/src/facets/command.ts +21 -0
- package/src/facets/facet-extension.spec.ts +173 -0
- package/src/facets/facet-extension.ts +53 -0
- package/src/facets/facet-node.spec.ts +265 -0
- package/src/facets/facet-node.ts +185 -0
- package/src/facets/facet-types.ts +9 -0
- package/src/facets/facet.spec.ts +76 -0
- package/src/facets/facet.ts +84 -0
- package/src/facets/root.ts +44 -0
- package/src/facets/schema-spec.ts +30 -0
- package/src/facets/schema.ts +26 -0
- package/src/facets/state.ts +57 -0
- package/src/facets/union-extension.ts +41 -0
- package/src/index.ts +302 -0
- package/src/test/index.ts +4 -0
- package/src/test/test-builder.ts +68 -0
- package/src/test/test-editor.spec.ts +104 -0
- package/src/test/test-editor.ts +113 -0
- package/src/testing/index.ts +283 -0
- package/src/testing/keyboard.ts +5 -0
- package/src/types/any-function.ts +4 -0
- package/src/types/assert-type-equal.ts +8 -0
- package/src/types/attrs.ts +32 -0
- package/src/types/base-node-view-options.ts +33 -0
- package/src/types/dom-node.ts +1 -0
- package/src/types/extension-command.ts +52 -0
- package/src/types/extension-mark.ts +15 -0
- package/src/types/extension-node.ts +15 -0
- package/src/types/extension.spec.ts +56 -0
- package/src/types/extension.ts +168 -0
- package/src/types/model.ts +54 -0
- package/src/types/object-entries.ts +13 -0
- package/src/types/pick-string-literal.spec.ts +10 -0
- package/src/types/pick-string-literal.ts +6 -0
- package/src/types/pick-sub-type.spec.ts +20 -0
- package/src/types/pick-sub-type.ts +6 -0
- package/src/types/priority.ts +12 -0
- package/src/types/setter.ts +4 -0
- package/src/types/simplify-deeper.spec.ts +40 -0
- package/src/types/simplify-deeper.ts +6 -0
- package/src/types/simplify-union.spec.ts +21 -0
- package/src/types/simplify-union.ts +11 -0
- package/src/utils/array-grouping.spec.ts +29 -0
- package/src/utils/array-grouping.ts +25 -0
- package/src/utils/array.ts +21 -0
- package/src/utils/assert.ts +13 -0
- package/src/utils/attrs-match.ts +20 -0
- package/src/utils/can-use-regex-lookbehind.ts +12 -0
- package/src/utils/clsx.spec.ts +14 -0
- package/src/utils/clsx.ts +12 -0
- package/src/utils/collect-children.ts +21 -0
- package/src/utils/collect-nodes.ts +37 -0
- package/src/utils/combine-event-handlers.spec.ts +27 -0
- package/src/utils/combine-event-handlers.ts +27 -0
- package/src/utils/contains-inline-node.ts +17 -0
- package/src/utils/deep-equals.spec.ts +26 -0
- package/src/utils/deep-equals.ts +29 -0
- package/src/utils/default-block-at.ts +15 -0
- package/src/utils/editor-content.spec.ts +47 -0
- package/src/utils/editor-content.ts +77 -0
- package/src/utils/env.ts +6 -0
- package/src/utils/find-parent-node-of-type.ts +29 -0
- package/src/utils/find-parent-node.spec.ts +68 -0
- package/src/utils/find-parent-node.ts +55 -0
- package/src/utils/get-custom-selection.ts +19 -0
- package/src/utils/get-dom-api.ts +56 -0
- package/src/utils/get-id.spec.ts +14 -0
- package/src/utils/get-id.ts +13 -0
- package/src/utils/get-mark-type.ts +20 -0
- package/src/utils/get-node-type.ts +20 -0
- package/src/utils/get-node-types.ts +19 -0
- package/src/utils/includes-mark.ts +18 -0
- package/src/utils/is-at-block-start.ts +26 -0
- package/src/utils/is-in-code-block.ts +18 -0
- package/src/utils/is-mark-absent.spec.ts +53 -0
- package/src/utils/is-mark-absent.ts +42 -0
- package/src/utils/is-mark-active.ts +27 -0
- package/src/utils/is-node-active.ts +25 -0
- package/src/utils/is-subset.spec.ts +12 -0
- package/src/utils/is-subset.ts +11 -0
- package/src/utils/maybe-run.spec.ts +39 -0
- package/src/utils/maybe-run.ts +11 -0
- package/src/utils/merge-objects.spec.ts +30 -0
- package/src/utils/merge-objects.ts +11 -0
- package/src/utils/merge-specs.ts +35 -0
- package/src/utils/object-equal.spec.ts +26 -0
- package/src/utils/object-equal.ts +28 -0
- package/src/utils/output-spec.test.ts +95 -0
- package/src/utils/output-spec.ts +130 -0
- package/src/utils/parse.spec.ts +46 -0
- package/src/utils/parse.ts +321 -0
- package/src/utils/remove-undefined-values.spec.ts +15 -0
- package/src/utils/remove-undefined-values.ts +9 -0
- package/src/utils/set-selection-around.ts +11 -0
- package/src/utils/type-assertion.ts +91 -0
- package/src/utils/unicode.spec.ts +10 -0
- package/src/utils/unicode.ts +4 -0
- package/src/utils/with-skip-code-block.ts +15 -0
- package/dist/editor-CjVyjJqw.d.ts +0 -739
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DOMOutputSpec,
|
|
3
|
+
Schema,
|
|
4
|
+
TagParseRule,
|
|
5
|
+
} from '@prosekit/pm/model'
|
|
6
|
+
import formatHTML from 'diffable-html'
|
|
7
|
+
import {
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
} from 'vitest'
|
|
12
|
+
|
|
13
|
+
import { union } from '../editor/union'
|
|
14
|
+
import {
|
|
15
|
+
defineDoc,
|
|
16
|
+
defineParagraph,
|
|
17
|
+
defineText,
|
|
18
|
+
setupTestFromExtension,
|
|
19
|
+
} from '../testing'
|
|
20
|
+
|
|
21
|
+
import { defineHistory } from './history'
|
|
22
|
+
import { defineBaseKeymap } from './keymap-base'
|
|
23
|
+
import {
|
|
24
|
+
defineNodeAttr,
|
|
25
|
+
defineNodeSpec,
|
|
26
|
+
} from './node-spec'
|
|
27
|
+
|
|
28
|
+
describe('defineNodeSpec', () => {
|
|
29
|
+
it('can merge node specs', () => {
|
|
30
|
+
const toDOM1 = (): DOMOutputSpec => ['p', { 'data-ext1': '' }]
|
|
31
|
+
const toDOM2 = (): DOMOutputSpec => ['p', { 'data-ext2': '' }]
|
|
32
|
+
const parseDOM1: TagParseRule = { tag: 'p[data-ext1]' }
|
|
33
|
+
const parseDOM2: TagParseRule = { tag: 'p[data-ext2]' }
|
|
34
|
+
const toDebugString2 = () => 'ext2'
|
|
35
|
+
const leafText1 = () => 'text'
|
|
36
|
+
|
|
37
|
+
const ext1 = defineNodeSpec({
|
|
38
|
+
name: 'paragraph',
|
|
39
|
+
content: 'text*',
|
|
40
|
+
leafText: leafText1,
|
|
41
|
+
parseDOM: [parseDOM1],
|
|
42
|
+
toDOM: toDOM1,
|
|
43
|
+
attrs: {
|
|
44
|
+
foo: { default: undefined },
|
|
45
|
+
bar: { default: 'bar' },
|
|
46
|
+
baz: { default: 'baz:1 ' },
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
const ext2 = defineNodeSpec({
|
|
50
|
+
name: 'paragraph',
|
|
51
|
+
group: 'block',
|
|
52
|
+
leafText: undefined,
|
|
53
|
+
parseDOM: [parseDOM2],
|
|
54
|
+
toDOM: toDOM2,
|
|
55
|
+
toDebugString: toDebugString2,
|
|
56
|
+
attrs: {
|
|
57
|
+
foo: { default: 'foo' },
|
|
58
|
+
bar: { default: undefined },
|
|
59
|
+
baz: { default: 'baz:2' },
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const extension = union(ext1, ext2, defineDoc(), defineText())
|
|
64
|
+
const schema = extension.schema
|
|
65
|
+
expect(schema).toBeTruthy()
|
|
66
|
+
expect(schema?.spec.nodes.get('paragraph')).toEqual({
|
|
67
|
+
group: 'block',
|
|
68
|
+
content: 'text*',
|
|
69
|
+
leafText: leafText1,
|
|
70
|
+
parseDOM: [parseDOM1, parseDOM2],
|
|
71
|
+
toDOM: toDOM2,
|
|
72
|
+
toDebugString: toDebugString2,
|
|
73
|
+
attrs: {
|
|
74
|
+
foo: { default: 'foo' },
|
|
75
|
+
bar: { default: 'bar' },
|
|
76
|
+
baz: { default: 'baz:2' },
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('can reuse schema', () => {
|
|
82
|
+
const ext1 = union(defineDoc(), defineText(), defineParagraph())
|
|
83
|
+
const ext2 = union(defineBaseKeymap())
|
|
84
|
+
const ext3 = union(ext1, ext2)
|
|
85
|
+
|
|
86
|
+
const schema1 = ext1.schema
|
|
87
|
+
const schema2 = ext2.schema
|
|
88
|
+
const schema3 = ext3.schema
|
|
89
|
+
|
|
90
|
+
expect(schema1).toBeTruthy()
|
|
91
|
+
expect(schema2).toEqual(null)
|
|
92
|
+
expect(schema3).toBeTruthy()
|
|
93
|
+
|
|
94
|
+
expect(schema3 === schema1).toBe(true)
|
|
95
|
+
|
|
96
|
+
expect(formatSchema(schema3)).toMatchInlineSnapshot(`
|
|
97
|
+
{
|
|
98
|
+
"marks": "",
|
|
99
|
+
"nodes": "doc, paragraph, text",
|
|
100
|
+
"topNode": "doc",
|
|
101
|
+
}
|
|
102
|
+
`)
|
|
103
|
+
|
|
104
|
+
const ext4 = defineCodeBlockSpec()
|
|
105
|
+
const ext5 = union(ext3, ext4)
|
|
106
|
+
const schema5 = ext5.schema
|
|
107
|
+
expect(schema5).toBeTruthy()
|
|
108
|
+
|
|
109
|
+
expect(formatSchema(schema5)).toMatchInlineSnapshot(`
|
|
110
|
+
{
|
|
111
|
+
"marks": "",
|
|
112
|
+
"nodes": "codeBlock, doc, paragraph, text",
|
|
113
|
+
"topNode": "doc",
|
|
114
|
+
}
|
|
115
|
+
`)
|
|
116
|
+
|
|
117
|
+
expect(schema5 !== schema1).toBe(true)
|
|
118
|
+
|
|
119
|
+
const ext6 = union(ext5, defineHistory())
|
|
120
|
+
const schema6 = ext6.schema
|
|
121
|
+
expect(schema6).toBeTruthy()
|
|
122
|
+
|
|
123
|
+
expect(schema6 === schema5).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('defineNodeAttr', () => {
|
|
128
|
+
it('can add a new attribute', () => {
|
|
129
|
+
const textColorExt = defineNodeAttr({
|
|
130
|
+
type: 'paragraph',
|
|
131
|
+
attr: 'textColor',
|
|
132
|
+
default: 'black',
|
|
133
|
+
toDOM: (value) => ['style', `color: ${value}`],
|
|
134
|
+
parseDOM: (node: HTMLElement) => node.style.color,
|
|
135
|
+
})
|
|
136
|
+
const backgroundColorExt = defineNodeAttr({
|
|
137
|
+
type: 'paragraph',
|
|
138
|
+
attr: 'backgroundColor',
|
|
139
|
+
default: 'white',
|
|
140
|
+
toDOM: (value) => ['style', `background-color: ${value}`],
|
|
141
|
+
parseDOM: (node: HTMLElement) => node.style.backgroundColor,
|
|
142
|
+
})
|
|
143
|
+
const nodeIdExt = defineNodeAttr<'paragraph', 'nodeId', string | null>({
|
|
144
|
+
type: 'paragraph',
|
|
145
|
+
attr: 'nodeId',
|
|
146
|
+
default: null,
|
|
147
|
+
toDOM: (value) => (value ? ['data-node-id', value] : null),
|
|
148
|
+
parseDOM: (node: HTMLElement) => node.dataset.nodeId ?? null,
|
|
149
|
+
})
|
|
150
|
+
const extension = union(
|
|
151
|
+
defineDoc(),
|
|
152
|
+
defineText(),
|
|
153
|
+
defineParagraph(),
|
|
154
|
+
textColorExt,
|
|
155
|
+
backgroundColorExt,
|
|
156
|
+
nodeIdExt,
|
|
157
|
+
)
|
|
158
|
+
const { editor } = setupTestFromExtension(extension)
|
|
159
|
+
|
|
160
|
+
expect(Object.keys(editor.schema.nodes.paragraph.spec.attrs || {}))
|
|
161
|
+
.toMatchInlineSnapshot(`
|
|
162
|
+
[
|
|
163
|
+
"textColor",
|
|
164
|
+
"backgroundColor",
|
|
165
|
+
"nodeId",
|
|
166
|
+
]
|
|
167
|
+
`)
|
|
168
|
+
|
|
169
|
+
editor.setContent(
|
|
170
|
+
'<p data-node-id="123" style="background-color:blue;color:red;">Hello</p>',
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const json1 = editor.getDocJSON()
|
|
174
|
+
const html1 = editor.getDocHTML()
|
|
175
|
+
expect(json1).toEqual({
|
|
176
|
+
type: 'doc',
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'paragraph',
|
|
180
|
+
attrs: { nodeId: '123', textColor: 'red', backgroundColor: 'blue' },
|
|
181
|
+
content: [{ type: 'text', text: 'Hello' }],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
})
|
|
185
|
+
expect(formatHTML(html1)).toMatchInlineSnapshot(`
|
|
186
|
+
"
|
|
187
|
+
<div>
|
|
188
|
+
<p
|
|
189
|
+
data-node-id="123"
|
|
190
|
+
style="background-color: blue; color: red;"
|
|
191
|
+
>
|
|
192
|
+
Hello
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
"
|
|
196
|
+
`)
|
|
197
|
+
editor.setContent(html1)
|
|
198
|
+
const json2 = editor.getDocJSON()
|
|
199
|
+
expect(json2).toEqual(json1)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
function defineCodeBlockSpec() {
|
|
204
|
+
return defineNodeSpec({
|
|
205
|
+
name: 'codeBlock',
|
|
206
|
+
content: 'text*',
|
|
207
|
+
group: 'block',
|
|
208
|
+
code: true,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function formatSchema(schema: Schema | null | undefined) {
|
|
213
|
+
if (!schema) {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const nodes = Object.keys(schema.spec.nodes.toObject())
|
|
218
|
+
const marks = Object.keys(schema.spec.marks.toObject())
|
|
219
|
+
const topNode = schema.spec.topNode
|
|
220
|
+
|
|
221
|
+
nodes.sort()
|
|
222
|
+
marks.sort()
|
|
223
|
+
return { nodes: nodes.join(', '), marks: marks.join(', '), topNode }
|
|
224
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AttributeSpec,
|
|
3
|
+
NodeSpec,
|
|
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 NodeSpecOptions<
|
|
30
|
+
NodeName extends string = string,
|
|
31
|
+
Attrs extends AnyAttrs = AnyAttrs,
|
|
32
|
+
> extends NodeSpec {
|
|
33
|
+
/**
|
|
34
|
+
* The name of the node type.
|
|
35
|
+
*/
|
|
36
|
+
name: NodeName
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether this is the top-level node type. Only one node type can be the
|
|
40
|
+
* top-level node type in a schema.
|
|
41
|
+
*/
|
|
42
|
+
topNode?: boolean
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The attributes that nodes of this type get.
|
|
46
|
+
*/
|
|
47
|
+
attrs?: {
|
|
48
|
+
[key in keyof Attrs]: AttrSpec<Attrs[key]>
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export interface NodeAttrOptions<
|
|
56
|
+
NodeName extends string = string,
|
|
57
|
+
AttrName extends string = string,
|
|
58
|
+
AttrType = any,
|
|
59
|
+
> extends AttrSpec<AttrType> {
|
|
60
|
+
/**
|
|
61
|
+
* The name of the node type.
|
|
62
|
+
*/
|
|
63
|
+
type: NodeName
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The name of the attribute.
|
|
67
|
+
*/
|
|
68
|
+
attr: AttrName
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether the attribute should be kept when the node is split. Set it to
|
|
72
|
+
* `true` if you want to inherit the attribute from the previous node when
|
|
73
|
+
* splitting the node by pressing `Enter`.
|
|
74
|
+
*
|
|
75
|
+
* @default undefined
|
|
76
|
+
*/
|
|
77
|
+
splittable?: boolean
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the attribute key and value to be set on the HTML element.
|
|
81
|
+
*
|
|
82
|
+
* If the returned `key` is `"style"`, the value is a string of CSS properties and will
|
|
83
|
+
* be prepended to the existing `style` attribute on the DOM node.
|
|
84
|
+
*
|
|
85
|
+
* @param value - The value of the attribute of current ProseMirror node.
|
|
86
|
+
*/
|
|
87
|
+
toDOM?: (value: AttrType) => [key: string, value: string] | null | undefined
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses the attribute value from the DOM.
|
|
91
|
+
*/
|
|
92
|
+
parseDOM?: (node: HTMLElement) => AttrType
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Defines a node type.
|
|
97
|
+
*
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
export function defineNodeSpec<
|
|
101
|
+
Node extends string,
|
|
102
|
+
Attrs extends AnyAttrs = AnyAttrs,
|
|
103
|
+
>(
|
|
104
|
+
options: NodeSpecOptions<Node, Attrs>,
|
|
105
|
+
): Extension<{
|
|
106
|
+
Nodes: { [K in Node]: Attrs }
|
|
107
|
+
}> {
|
|
108
|
+
const payload: NodeSpecPayload = [options, undefined]
|
|
109
|
+
return defineFacetPayload(nodeSpecFacet, [payload]) as Extension<{
|
|
110
|
+
Nodes: any
|
|
111
|
+
}>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Defines an attribute for a node type.
|
|
116
|
+
*
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
119
|
+
export function defineNodeAttr<
|
|
120
|
+
NodeType extends string = string,
|
|
121
|
+
AttrName extends string = string,
|
|
122
|
+
AttrType = any,
|
|
123
|
+
>(
|
|
124
|
+
options: NodeAttrOptions<NodeType, AttrName, AttrType>,
|
|
125
|
+
): Extension<{
|
|
126
|
+
Nodes: { [K in NodeType]: { [K in AttrName]: AttrType } }
|
|
127
|
+
}> {
|
|
128
|
+
const payload: NodeSpecPayload = [undefined, options]
|
|
129
|
+
return defineFacetPayload(nodeSpecFacet, [payload]) as Extension<{
|
|
130
|
+
Nodes: any
|
|
131
|
+
}>
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type NodeSpecPayload = [
|
|
135
|
+
NodeSpecOptions | undefined,
|
|
136
|
+
NodeAttrOptions | undefined,
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
const nodeSpecFacet = defineFacet<NodeSpecPayload, SchemaSpec>({
|
|
140
|
+
reducer: (payloads: NodeSpecPayload[]): SchemaSpec => {
|
|
141
|
+
let specs = OrderedMap.from<NodeSpec>({})
|
|
142
|
+
let topNodeName: string | undefined = undefined
|
|
143
|
+
|
|
144
|
+
const specPayloads = payloads.map((input) => input[0]).filter(isNotNullish)
|
|
145
|
+
const attrPayloads = payloads.map((input) => input[1]).filter(isNotNullish)
|
|
146
|
+
|
|
147
|
+
for (const { name, topNode, ...spec } of specPayloads) {
|
|
148
|
+
if (topNode) {
|
|
149
|
+
topNodeName = name
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const prevSpec = specs.get(name)
|
|
153
|
+
if (prevSpec) {
|
|
154
|
+
specs = specs.update(name, mergeSpecs(prevSpec, spec))
|
|
155
|
+
} else {
|
|
156
|
+
// The latest spec has the highest priority, so we put it at the start
|
|
157
|
+
// of the map.
|
|
158
|
+
specs = specs.addToStart(name, spec)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const groupedAttrs = groupBy(attrPayloads, (payload) => payload.type)
|
|
163
|
+
|
|
164
|
+
for (const [type, attrs] of Object.entries(groupedAttrs)) {
|
|
165
|
+
if (!attrs) continue
|
|
166
|
+
|
|
167
|
+
const maybeSpec = specs.get(type)
|
|
168
|
+
assert(maybeSpec, `Node type ${type} must be defined`)
|
|
169
|
+
|
|
170
|
+
const spec = clone(maybeSpec)
|
|
171
|
+
|
|
172
|
+
if (!spec.attrs) {
|
|
173
|
+
spec.attrs = {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const attr of attrs) {
|
|
177
|
+
spec.attrs[attr.attr] = {
|
|
178
|
+
default: attr.default as unknown,
|
|
179
|
+
validate: attr.validate,
|
|
180
|
+
splittable: attr.splittable,
|
|
181
|
+
} as AttributeSpec
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (spec.toDOM) {
|
|
185
|
+
spec.toDOM = wrapOutputSpecAttrs(spec.toDOM, attrs)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (spec.parseDOM) {
|
|
189
|
+
spec.parseDOM = spec.parseDOM.map((rule) => wrapTagParseRuleAttrs(rule, attrs))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
specs = specs.update(type, spec)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { nodes: specs, topNode: topNodeName }
|
|
196
|
+
},
|
|
197
|
+
parent: schemaSpecFacet,
|
|
198
|
+
singleton: true,
|
|
199
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PluginKey,
|
|
3
|
+
ProseMirrorPlugin,
|
|
4
|
+
} from '@prosekit/pm/state'
|
|
5
|
+
import type { NodeViewConstructor } 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 NodeViewFactoryOptions<T> = {
|
|
21
|
+
group: string
|
|
22
|
+
factory: (args: T) => NodeViewConstructor
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export type NodeViewComponentOptions<T> = {
|
|
29
|
+
group: string
|
|
30
|
+
name: string
|
|
31
|
+
args: T
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type NodeViewFactoryInput = [
|
|
35
|
+
NodeViewFactoryOptions<any> | null,
|
|
36
|
+
NodeViewComponentOptions<any> | null,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function defineNodeViewFactory<T>(
|
|
43
|
+
options: NodeViewFactoryOptions<T>,
|
|
44
|
+
): Extension {
|
|
45
|
+
const input: NodeViewFactoryInput = [options, null]
|
|
46
|
+
return defineFacetPayload(nodeViewFactoryFacet, [input])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export function defineNodeViewComponent<T>(
|
|
53
|
+
options: NodeViewComponentOptions<T>,
|
|
54
|
+
): Extension {
|
|
55
|
+
const input: NodeViewFactoryInput = [null, options]
|
|
56
|
+
return defineFacetPayload(nodeViewFactoryFacet, [input])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isServer = typeof window === 'undefined'
|
|
60
|
+
|
|
61
|
+
const nodeViewFactoryFacet = defineFacet<NodeViewFactoryInput, PluginPayload>({
|
|
62
|
+
reducer: (inputs: NodeViewFactoryInput[]): PluginPayload => {
|
|
63
|
+
// Don't register node views on the server
|
|
64
|
+
if (isServer) return []
|
|
65
|
+
|
|
66
|
+
const nodeViews: { [nodeName: string]: NodeViewConstructor } = {}
|
|
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
|
+
nodeViews[name] = factory.factory(args)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return () => [
|
|
78
|
+
new ProseMirrorPlugin({
|
|
79
|
+
key: new PluginKey('prosekit-node-view-effect'),
|
|
80
|
+
props: { nodeViews },
|
|
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 { NodeViewConstructor } 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 NodeViewOptions {
|
|
17
|
+
name: string
|
|
18
|
+
constructor: NodeViewConstructor
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defineNodeView(options: NodeViewOptions): Extension {
|
|
22
|
+
return defineFacetPayload(nodeViewFacet, [options])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const nodeViewFacet = defineFacet<NodeViewOptions, PluginPayload>({
|
|
26
|
+
reducer: (inputs: NodeViewOptions[]): PluginPayload => {
|
|
27
|
+
const nodeViews: { [nodeName: string]: NodeViewConstructor } = {}
|
|
28
|
+
|
|
29
|
+
for (const input of inputs) {
|
|
30
|
+
if (!nodeViews[input.name]) {
|
|
31
|
+
nodeViews[input.name] = input.constructor
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return () => [
|
|
36
|
+
new ProseMirrorPlugin({
|
|
37
|
+
key: new PluginKey('prosekit-node-view'),
|
|
38
|
+
props: { nodeViews },
|
|
39
|
+
}),
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
parent: pluginFacet,
|
|
43
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Attrs } from '@prosekit/pm/model'
|
|
2
|
+
|
|
3
|
+
import { withPriority } from '../editor/with-priority'
|
|
4
|
+
import type { Extension } from '../types/extension'
|
|
5
|
+
import { Priority } from '../types/priority'
|
|
6
|
+
|
|
7
|
+
import { defineNodeSpec } from './node-spec'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
type ParagraphSpecExtension = Extension<{
|
|
13
|
+
Nodes: {
|
|
14
|
+
paragraph: Attrs
|
|
15
|
+
}
|
|
16
|
+
}>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Defines a paragraph node spec.
|
|
20
|
+
*/
|
|
21
|
+
function defineParagraphSpec(): ParagraphSpecExtension {
|
|
22
|
+
return defineNodeSpec({
|
|
23
|
+
name: 'paragraph',
|
|
24
|
+
content: 'inline*',
|
|
25
|
+
group: 'block',
|
|
26
|
+
parseDOM: [{ tag: 'p' }],
|
|
27
|
+
toDOM() {
|
|
28
|
+
return ['p', 0]
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*
|
|
36
|
+
* @deprecated Use the following import instead:
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* import type { ParagraphExtension } from 'prosekit/extensions/paragraph'
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export type ParagraphExtension = ParagraphSpecExtension
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @public
|
|
46
|
+
*
|
|
47
|
+
* Defines a paragraph node spec as the highest priority, because it should be the default block node for most cases.
|
|
48
|
+
*
|
|
49
|
+
* @deprecated Use the following import instead:
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { defineParagraph } from 'prosekit/extensions/paragraph'
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function defineParagraph(): ParagraphExtension {
|
|
56
|
+
console.warn(
|
|
57
|
+
'[prosekit] The `defineParagraph` function from `prosekit/core` is deprecated. Use the following import instead: `import { defineParagraph } from "prosekit/extensions/paragraph"`.',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return withPriority(defineParagraphSpec(), Priority.highest)
|
|
61
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Schema } from '@prosekit/pm/model'
|
|
2
|
+
import {
|
|
3
|
+
Plugin,
|
|
4
|
+
type ProseMirrorPlugin,
|
|
5
|
+
} from '@prosekit/pm/state'
|
|
6
|
+
|
|
7
|
+
import { ProseKitError } from '../error'
|
|
8
|
+
import {
|
|
9
|
+
defineFacet,
|
|
10
|
+
type Facet,
|
|
11
|
+
} from '../facets/facet'
|
|
12
|
+
import { defineFacetPayload } from '../facets/facet-extension'
|
|
13
|
+
import {
|
|
14
|
+
stateFacet,
|
|
15
|
+
type StatePayload,
|
|
16
|
+
} from '../facets/state'
|
|
17
|
+
import type { PlainExtension } from '../types/extension'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Adds a ProseMirror plugin to the editor.
|
|
21
|
+
*
|
|
22
|
+
* @param plugin - The ProseMirror plugin to add, or an array of plugins, or a
|
|
23
|
+
* function that returns one or multiple plugins.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export function definePlugin(
|
|
28
|
+
plugin:
|
|
29
|
+
| Plugin
|
|
30
|
+
| Plugin[]
|
|
31
|
+
| ((context: { schema: Schema }) => Plugin | Plugin[]),
|
|
32
|
+
): PlainExtension {
|
|
33
|
+
if (
|
|
34
|
+
plugin instanceof Plugin
|
|
35
|
+
|| (Array.isArray(plugin) && plugin.every((p) => p instanceof Plugin))
|
|
36
|
+
) {
|
|
37
|
+
return definePluginPayload(() => plugin)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof plugin === 'function') {
|
|
41
|
+
return definePluginPayload(plugin)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new TypeError('Invalid plugin')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function definePluginPayload(payload: PluginPayload): PlainExtension {
|
|
48
|
+
return defineFacetPayload(pluginFacet, [payload]) as PlainExtension
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export type PluginPayload =
|
|
55
|
+
| Plugin
|
|
56
|
+
| Plugin[]
|
|
57
|
+
| ((context: { schema: Schema }) => Plugin | Plugin[])
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export const pluginFacet: Facet<PluginPayload, StatePayload> = defineFacet({
|
|
63
|
+
reducer: (payloads): StatePayload => {
|
|
64
|
+
return ({ schema }) => {
|
|
65
|
+
const plugins: ProseMirrorPlugin[] = []
|
|
66
|
+
|
|
67
|
+
for (const payload of payloads) {
|
|
68
|
+
if (payload instanceof Plugin) {
|
|
69
|
+
plugins.push(payload)
|
|
70
|
+
} else if (
|
|
71
|
+
Array.isArray(payload)
|
|
72
|
+
&& payload.every((p) => p instanceof Plugin)
|
|
73
|
+
) {
|
|
74
|
+
plugins.push(...payload)
|
|
75
|
+
} else if (typeof payload === 'function') {
|
|
76
|
+
plugins.push(...[payload({ schema })].flat())
|
|
77
|
+
} else {
|
|
78
|
+
throw new ProseKitError('Invalid plugin')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// In ProseMirror, the plugins at the beginning have a higher priority.
|
|
83
|
+
// However, in ProseKit, the extensions at the end have a higher priority
|
|
84
|
+
// because we want to easily override the default behaviors by appending
|
|
85
|
+
// new extensions. Therefore, we need to reverse plugins here.
|
|
86
|
+
plugins.reverse()
|
|
87
|
+
return { plugins }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
parent: stateFacet,
|
|
91
|
+
})
|