@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,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
|
+
})
|