@jvs-milkdown/core 1.0.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/lib/__internal__/index.d.ts +3 -0
  4. package/lib/__internal__/index.d.ts.map +1 -0
  5. package/lib/__internal__/remark-handlers.d.ts +3 -0
  6. package/lib/__internal__/remark-handlers.d.ts.map +1 -0
  7. package/lib/__internal__/utils.d.ts +3 -0
  8. package/lib/__internal__/utils.d.ts.map +1 -0
  9. package/lib/editor/editor.d.ts +28 -0
  10. package/lib/editor/editor.d.ts.map +1 -0
  11. package/lib/editor/index.d.ts +2 -0
  12. package/lib/editor/index.d.ts.map +1 -0
  13. package/lib/index.d.ts +3 -0
  14. package/lib/index.d.ts.map +1 -0
  15. package/lib/index.js +676 -0
  16. package/lib/index.js.map +1 -0
  17. package/lib/internal-plugin/atoms.d.ts +22 -0
  18. package/lib/internal-plugin/atoms.d.ts.map +1 -0
  19. package/lib/internal-plugin/commands.d.ts +38 -0
  20. package/lib/internal-plugin/commands.d.ts.map +1 -0
  21. package/lib/internal-plugin/config.d.ts +5 -0
  22. package/lib/internal-plugin/config.d.ts.map +1 -0
  23. package/lib/internal-plugin/editor-state.d.ts +22 -0
  24. package/lib/internal-plugin/editor-state.d.ts.map +1 -0
  25. package/lib/internal-plugin/editor-view.d.ts +13 -0
  26. package/lib/internal-plugin/editor-view.d.ts.map +1 -0
  27. package/lib/internal-plugin/index.d.ts +12 -0
  28. package/lib/internal-plugin/index.d.ts.map +1 -0
  29. package/lib/internal-plugin/init.d.ts +5 -0
  30. package/lib/internal-plugin/init.d.ts.map +1 -0
  31. package/lib/internal-plugin/keymap.d.ts +22 -0
  32. package/lib/internal-plugin/keymap.d.ts.map +1 -0
  33. package/lib/internal-plugin/keymap.test.d.ts +2 -0
  34. package/lib/internal-plugin/keymap.test.d.ts.map +1 -0
  35. package/lib/internal-plugin/parser.d.ts +7 -0
  36. package/lib/internal-plugin/parser.d.ts.map +1 -0
  37. package/lib/internal-plugin/paste-rule.d.ts +12 -0
  38. package/lib/internal-plugin/paste-rule.d.ts.map +1 -0
  39. package/lib/internal-plugin/schema.d.ts +10 -0
  40. package/lib/internal-plugin/schema.d.ts.map +1 -0
  41. package/lib/internal-plugin/serializer.d.ts +7 -0
  42. package/lib/internal-plugin/serializer.d.ts.map +1 -0
  43. package/lib/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +45 -0
  45. package/src/__internal__/index.ts +2 -0
  46. package/src/__internal__/remark-handlers.ts +48 -0
  47. package/src/__internal__/utils.ts +14 -0
  48. package/src/editor/editor.ts +300 -0
  49. package/src/editor/index.ts +1 -0
  50. package/src/index.ts +2 -0
  51. package/src/internal-plugin/atoms.ts +69 -0
  52. package/src/internal-plugin/commands.ts +176 -0
  53. package/src/internal-plugin/config.ts +34 -0
  54. package/src/internal-plugin/editor-state.ts +140 -0
  55. package/src/internal-plugin/editor-view.ts +166 -0
  56. package/src/internal-plugin/index.ts +11 -0
  57. package/src/internal-plugin/init.ts +78 -0
  58. package/src/internal-plugin/keymap.test.ts +136 -0
  59. package/src/internal-plugin/keymap.ts +167 -0
  60. package/src/internal-plugin/parser.ts +51 -0
  61. package/src/internal-plugin/paste-rule.ts +53 -0
  62. package/src/internal-plugin/schema.ts +88 -0
  63. package/src/internal-plugin/serializer.ts +61 -0
@@ -0,0 +1,140 @@
1
+ import type { MilkdownPlugin, TimerType } from '@jvs-milkdown/ctx'
2
+ import type { Schema } from '@jvs-milkdown/prose/model'
3
+ import type { JSONRecord, Parser } from '@jvs-milkdown/transformer'
4
+
5
+ import { createSlice, createTimer } from '@jvs-milkdown/ctx'
6
+ import { docTypeError } from '@jvs-milkdown/exception'
7
+ import { customInputRules as createInputRules } from '@jvs-milkdown/prose'
8
+ import { keymap as createKeymap } from '@jvs-milkdown/prose/keymap'
9
+ import { DOMParser, Node } from '@jvs-milkdown/prose/model'
10
+ import { EditorState, Plugin, PluginKey } from '@jvs-milkdown/prose/state'
11
+
12
+ import { withMeta } from '../__internal__'
13
+ import { editorStateCtx, inputRulesCtx, prosePluginsCtx } from './atoms'
14
+ import { CommandsReady } from './commands'
15
+ import { keymapCtx, KeymapReady } from './keymap'
16
+ import { ParserReady, parserCtx } from './parser'
17
+ import { schemaCtx } from './schema'
18
+ import { SerializerReady } from './serializer'
19
+
20
+ /// @internal
21
+ export type DefaultValue =
22
+ | string
23
+ | { type: 'html'; dom: HTMLElement }
24
+ | { type: 'json'; value: JSONRecord }
25
+ type StateOptions = Parameters<typeof EditorState.create>[0]
26
+ type StateOptionsOverride = (prev: StateOptions) => StateOptions
27
+
28
+ /// A slice which contains the default value of the editor.
29
+ /// Can be markdown string, html string or json.
30
+ export const defaultValueCtx = createSlice('' as DefaultValue, 'defaultValue')
31
+
32
+ /// A slice which contains the options which is used to create the editor state.
33
+ export const editorStateOptionsCtx = createSlice<StateOptionsOverride>(
34
+ (x) => x,
35
+ 'stateOptions'
36
+ )
37
+
38
+ /// A slice which stores timers that need to be waited for before starting to run the plugin.
39
+ /// By default, it's `[ParserReady, SerializerReady, CommandsReady]`.
40
+ export const editorStateTimerCtx = createSlice(
41
+ [] as TimerType[],
42
+ 'editorStateTimer'
43
+ )
44
+
45
+ /// The timer which will be resolved when the editor state plugin is ready.
46
+ export const EditorStateReady = createTimer('EditorStateReady')
47
+
48
+ /// @internal
49
+ export function getDoc(
50
+ defaultValue: DefaultValue,
51
+ parser: Parser,
52
+ schema: Schema
53
+ ) {
54
+ if (typeof defaultValue === 'string') return parser(defaultValue)
55
+
56
+ if (defaultValue.type === 'html')
57
+ return DOMParser.fromSchema(schema).parse(defaultValue.dom)
58
+
59
+ if (defaultValue.type === 'json')
60
+ return Node.fromJSON(schema, defaultValue.value)
61
+
62
+ throw docTypeError(defaultValue)
63
+ }
64
+
65
+ const key = new PluginKey('MILKDOWN_STATE_TRACKER')
66
+
67
+ /// The editor state plugin.
68
+ /// This plugin will create a prosemirror editor state.
69
+ ///
70
+ /// This plugin will wait for the parser plugin, serializer plugin and commands plugin.
71
+ export const editorState: MilkdownPlugin = (ctx) => {
72
+ ctx
73
+ .inject(defaultValueCtx, '')
74
+ .inject(editorStateCtx, {} as EditorState)
75
+ .inject(editorStateOptionsCtx, (x) => x)
76
+ .inject(editorStateTimerCtx, [
77
+ ParserReady,
78
+ SerializerReady,
79
+ CommandsReady,
80
+ KeymapReady,
81
+ ])
82
+ .record(EditorStateReady)
83
+
84
+ return async () => {
85
+ await ctx.waitTimers(editorStateTimerCtx)
86
+
87
+ const schema = ctx.get(schemaCtx)
88
+ const parser = ctx.get(parserCtx)
89
+ const rules = ctx.get(inputRulesCtx)
90
+ const optionsOverride = ctx.get(editorStateOptionsCtx)
91
+ const prosePlugins = ctx.get(prosePluginsCtx)
92
+ const defaultValue = ctx.get(defaultValueCtx)
93
+ const doc = getDoc(defaultValue, parser, schema)
94
+ const km = ctx.get(keymapCtx)
95
+ const disposeBaseKeymap = km.addBaseKeymap()
96
+
97
+ const plugins = [
98
+ ...prosePlugins,
99
+ new Plugin({
100
+ key,
101
+ state: {
102
+ init: () => {
103
+ // do nothing
104
+ },
105
+ apply: (_tr, _value, _oldState, newState) => {
106
+ ctx.set(editorStateCtx, newState)
107
+ },
108
+ },
109
+ }),
110
+ createInputRules({ rules }),
111
+ createKeymap(km.build()),
112
+ ]
113
+
114
+ ctx.set(prosePluginsCtx, plugins)
115
+
116
+ const options = optionsOverride({
117
+ schema,
118
+ doc,
119
+ plugins,
120
+ })
121
+
122
+ const state = EditorState.create(options)
123
+ ctx.set(editorStateCtx, state)
124
+ ctx.done(EditorStateReady)
125
+
126
+ return () => {
127
+ disposeBaseKeymap()
128
+ ctx
129
+ .remove(defaultValueCtx)
130
+ .remove(editorStateCtx)
131
+ .remove(editorStateOptionsCtx)
132
+ .remove(editorStateTimerCtx)
133
+ .clearTimer(EditorStateReady)
134
+ }
135
+ }
136
+ }
137
+
138
+ withMeta(editorState, {
139
+ displayName: 'EditorState',
140
+ })
@@ -0,0 +1,166 @@
1
+ import type { Ctx, MilkdownPlugin, TimerType } from '@jvs-milkdown/ctx'
2
+ import type { DirectEditorProps } from '@jvs-milkdown/prose/view'
3
+
4
+ import { createSlice, createTimer } from '@jvs-milkdown/ctx'
5
+ import { Plugin, PluginKey } from '@jvs-milkdown/prose/state'
6
+ import { EditorView } from '@jvs-milkdown/prose/view'
7
+
8
+ import { withMeta } from '../__internal__'
9
+ import {
10
+ editorStateCtx,
11
+ editorViewCtx,
12
+ markViewCtx,
13
+ nodeViewCtx,
14
+ prosePluginsCtx,
15
+ } from './atoms'
16
+ import { EditorStateReady } from './editor-state'
17
+ import { InitReady } from './init'
18
+ import { pasteRulesCtx, PasteRulesReady } from './paste-rule'
19
+
20
+ type EditorOptions = Omit<DirectEditorProps, 'state'>
21
+
22
+ type RootType = Node | undefined | null | string
23
+
24
+ /// The timer which will be resolved when the editor view plugin is ready.
25
+ export const EditorViewReady = createTimer('EditorViewReady')
26
+
27
+ /// A slice which stores timers that need to be waited for before starting to run the plugin.
28
+ /// By default, it's `[EditorStateReady]`.
29
+ export const editorViewTimerCtx = createSlice(
30
+ [] as TimerType[],
31
+ 'editorViewTimer'
32
+ )
33
+
34
+ /// A slice which contains the editor view options which will be passed to the editor view.
35
+ export const editorViewOptionsCtx = createSlice(
36
+ {} as Partial<EditorOptions>,
37
+ 'editorViewOptions'
38
+ )
39
+
40
+ /// A slice which contains the value to get the root element.
41
+ /// Can be a selector string, a node or null.
42
+ /// If it's null, the editor will be created in the body.
43
+ export const rootCtx = createSlice(null as RootType, 'root')
44
+
45
+ /// A slice which contains the actually root element.
46
+ export const rootDOMCtx = createSlice(null as unknown as HTMLElement, 'rootDOM')
47
+
48
+ /// A slice which contains the root element attributes.
49
+ /// You can add attributes to the root element by this slice.
50
+ export const rootAttrsCtx = createSlice(
51
+ {} as Record<string, string>,
52
+ 'rootAttrs'
53
+ )
54
+
55
+ function createViewContainer(root: Node, ctx: Ctx) {
56
+ const container = document.createElement('div')
57
+ container.className = 'milkdown'
58
+ root.appendChild(container)
59
+ ctx.set(rootDOMCtx, container)
60
+
61
+ const attrs = ctx.get(rootAttrsCtx)
62
+ Object.entries(attrs).forEach(([key, value]) =>
63
+ container.setAttribute(key, value)
64
+ )
65
+
66
+ return container
67
+ }
68
+
69
+ function prepareViewDom(dom: Element) {
70
+ dom.classList.add('editor')
71
+ dom.setAttribute('role', 'textbox')
72
+ }
73
+
74
+ const key = new PluginKey('MILKDOWN_VIEW_CLEAR')
75
+
76
+ /// The editor view plugin.
77
+ /// This plugin will create an editor view.
78
+ ///
79
+ /// This plugin will wait for the editor state plugin.
80
+ export const editorView: MilkdownPlugin = (ctx) => {
81
+ ctx
82
+ .inject(rootCtx, document.body)
83
+ .inject(editorViewCtx, {} as EditorView)
84
+ .inject(editorViewOptionsCtx, {})
85
+ .inject(rootDOMCtx, null as unknown as HTMLElement)
86
+ .inject(rootAttrsCtx, {})
87
+ .inject(editorViewTimerCtx, [EditorStateReady, PasteRulesReady])
88
+ .record(EditorViewReady)
89
+
90
+ return async () => {
91
+ await ctx.wait(InitReady)
92
+
93
+ const root = ctx.get(rootCtx) || document.body
94
+ const el = typeof root === 'string' ? document.querySelector(root) : root
95
+
96
+ ctx.update(prosePluginsCtx, (xs) => [
97
+ new Plugin({
98
+ key,
99
+ view: (editorView) => {
100
+ const container = el ? createViewContainer(el, ctx) : undefined
101
+
102
+ const handleDOM = () => {
103
+ if (container && el) {
104
+ const editor = editorView.dom
105
+ el.replaceChild(container, editor)
106
+ container.appendChild(editor)
107
+ }
108
+ }
109
+ handleDOM()
110
+ return {
111
+ destroy: () => {
112
+ if (container?.parentNode)
113
+ container?.parentNode.replaceChild(editorView.dom, container)
114
+
115
+ container?.remove()
116
+ },
117
+ }
118
+ },
119
+ }),
120
+ ...xs,
121
+ ])
122
+
123
+ await ctx.waitTimers(editorViewTimerCtx)
124
+
125
+ const state = ctx.get(editorStateCtx)
126
+ const options = ctx.get(editorViewOptionsCtx)
127
+ const nodeViews = Object.fromEntries(ctx.get(nodeViewCtx))
128
+ const markViews = Object.fromEntries(ctx.get(markViewCtx))
129
+ const view = new EditorView(el as Node, {
130
+ state,
131
+ nodeViews,
132
+ markViews,
133
+ transformPasted: (slice, view, isPlainText) => {
134
+ ctx
135
+ .get(pasteRulesCtx)
136
+ .sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50))
137
+ .map((rule) => rule.run)
138
+ .forEach((runner) => {
139
+ slice = runner(slice, view, isPlainText)
140
+ })
141
+
142
+ return slice
143
+ },
144
+ ...options,
145
+ })
146
+ prepareViewDom(view.dom)
147
+ ctx.set(editorViewCtx, view)
148
+ ctx.done(EditorViewReady)
149
+
150
+ return () => {
151
+ view?.destroy()
152
+ ctx
153
+ .remove(rootCtx)
154
+ .remove(editorViewCtx)
155
+ .remove(editorViewOptionsCtx)
156
+ .remove(rootDOMCtx)
157
+ .remove(rootAttrsCtx)
158
+ .remove(editorViewTimerCtx)
159
+ .clearTimer(EditorViewReady)
160
+ }
161
+ }
162
+ }
163
+
164
+ withMeta(editorView, {
165
+ displayName: 'EditorView',
166
+ })
@@ -0,0 +1,11 @@
1
+ export * from './atoms'
2
+ export * from './commands'
3
+ export * from './config'
4
+ export * from './editor-state'
5
+ export * from './editor-view'
6
+ export * from './init'
7
+ export * from './parser'
8
+ export * from './schema'
9
+ export * from './serializer'
10
+ export * from './keymap'
11
+ export * from './paste-rule'
@@ -0,0 +1,78 @@
1
+ import type { MilkdownPlugin } from '@jvs-milkdown/ctx'
2
+
3
+ import { createTimer } from '@jvs-milkdown/ctx'
4
+ import remarkParse from 'remark-parse'
5
+ import remarkStringify, { type Options } from 'remark-stringify'
6
+ import { unified } from 'unified'
7
+
8
+ import type { Editor } from '../editor'
9
+
10
+ import { remarkHandlers, withMeta } from '../__internal__'
11
+ import {
12
+ editorCtx,
13
+ initTimerCtx,
14
+ inputRulesCtx,
15
+ markViewCtx,
16
+ nodeViewCtx,
17
+ prosePluginsCtx,
18
+ remarkCtx,
19
+ remarkPluginsCtx,
20
+ remarkStringifyOptionsCtx,
21
+ } from './atoms'
22
+ import { ConfigReady } from './config'
23
+
24
+ /// The timer which will be resolved when the init plugin is ready.
25
+ export const InitReady = createTimer('InitReady')
26
+
27
+ /// The init plugin.
28
+ /// This plugin prepare slices that needed by other plugins. And create a remark instance.
29
+ ///
30
+ /// This plugin will wait for the config plugin.
31
+ export function init(editor: Editor): MilkdownPlugin {
32
+ const plugin: MilkdownPlugin = (ctx) => {
33
+ ctx
34
+ .inject(editorCtx, editor)
35
+ .inject(prosePluginsCtx, [])
36
+ .inject(remarkPluginsCtx, [])
37
+ .inject(inputRulesCtx, [])
38
+ .inject(nodeViewCtx, [])
39
+ .inject(markViewCtx, [])
40
+ .inject(remarkStringifyOptionsCtx, {
41
+ handlers: remarkHandlers,
42
+ encode: [],
43
+ } as Options)
44
+ .inject(remarkCtx, unified().use(remarkParse).use(remarkStringify))
45
+ .inject(initTimerCtx, [ConfigReady])
46
+ .record(InitReady)
47
+
48
+ return async () => {
49
+ await ctx.waitTimers(initTimerCtx)
50
+ const options = ctx.get(remarkStringifyOptionsCtx)
51
+ ctx.set(
52
+ remarkCtx,
53
+ unified().use(remarkParse).use(remarkStringify, options)
54
+ )
55
+
56
+ ctx.done(InitReady)
57
+
58
+ return () => {
59
+ ctx
60
+ .remove(editorCtx)
61
+ .remove(prosePluginsCtx)
62
+ .remove(remarkPluginsCtx)
63
+ .remove(inputRulesCtx)
64
+ .remove(nodeViewCtx)
65
+ .remove(markViewCtx)
66
+ .remove(remarkStringifyOptionsCtx)
67
+ .remove(remarkCtx)
68
+ .remove(initTimerCtx)
69
+ .clearTimer(InitReady)
70
+ }
71
+ }
72
+ }
73
+ withMeta(plugin, {
74
+ displayName: 'Init',
75
+ })
76
+
77
+ return plugin
78
+ }
@@ -0,0 +1,136 @@
1
+ import { Clock, Container, Ctx } from '@jvs-milkdown/ctx'
2
+ import { expect, test, vi } from 'vitest'
3
+
4
+ import { KeymapManager } from './keymap'
5
+
6
+ test('should work with basic keymap', async () => {
7
+ const km = new KeymapManager()
8
+ const container = new Container()
9
+ const clock = new Clock()
10
+ const ctx = new Ctx(container, clock)
11
+ km.setCtx(ctx)
12
+ km.addBaseKeymap()
13
+
14
+ const keymap = km.build()
15
+ expect(keymap['Backspace']).toBeDefined()
16
+ expect(keymap['Enter']).toBeDefined()
17
+ })
18
+
19
+ test('should chain commands for same key', async () => {
20
+ const km = new KeymapManager()
21
+ const container = new Container()
22
+ const clock = new Clock()
23
+ const ctx = new Ctx(container, clock)
24
+ km.setCtx(ctx)
25
+
26
+ const mock1 = vi.fn()
27
+ const mock2 = vi.fn()
28
+ const mock3 = vi.fn()
29
+
30
+ km.add({
31
+ key: 'Backspace',
32
+ onRun: () => {
33
+ return () => {
34
+ mock1()
35
+ return false
36
+ }
37
+ },
38
+ })
39
+
40
+ km.add({
41
+ key: 'Backspace',
42
+ onRun: () => {
43
+ return () => {
44
+ mock2()
45
+ return true
46
+ }
47
+ },
48
+ })
49
+
50
+ km.add({
51
+ key: 'Backspace',
52
+ onRun: () => {
53
+ return () => {
54
+ mock3()
55
+ return false
56
+ }
57
+ },
58
+ })
59
+
60
+ const keymap = km.build()
61
+ keymap['Backspace']!({} as never, () => {}, {} as never)
62
+
63
+ expect(mock1).toHaveBeenCalledTimes(1)
64
+ expect(mock2).toHaveBeenCalledTimes(1)
65
+ expect(mock3).not.toHaveBeenCalled()
66
+ })
67
+
68
+ test('should call high priority keymap first', async () => {
69
+ const km = new KeymapManager()
70
+ const container = new Container()
71
+ const clock = new Clock()
72
+ const ctx = new Ctx(container, clock)
73
+ km.setCtx(ctx)
74
+
75
+ const mock1 = vi.fn()
76
+ const mock2 = vi.fn()
77
+ const mock3 = vi.fn()
78
+
79
+ km.add({
80
+ key: 'Backspace',
81
+ onRun: () => {
82
+ return () => {
83
+ mock1()
84
+ return false
85
+ }
86
+ },
87
+ })
88
+
89
+ km.add({
90
+ key: 'Backspace',
91
+ onRun: () => {
92
+ return () => {
93
+ mock2()
94
+ return true
95
+ }
96
+ },
97
+ })
98
+
99
+ km.add({
100
+ key: 'Backspace',
101
+ priority: 100,
102
+ onRun: () => {
103
+ return () => {
104
+ mock3()
105
+ return true
106
+ }
107
+ },
108
+ })
109
+
110
+ const keymap = km.build()
111
+ keymap['Backspace']!({} as never, () => {}, {} as never)
112
+
113
+ expect(mock1).not.toHaveBeenCalled()
114
+ expect(mock2).not.toHaveBeenCalled()
115
+ expect(mock3).toHaveBeenCalledTimes(1)
116
+ })
117
+
118
+ test('should add object keymap', async () => {
119
+ const km = new KeymapManager()
120
+ const container = new Container()
121
+ const clock = new Clock()
122
+ const ctx = new Ctx(container, clock)
123
+ km.setCtx(ctx)
124
+
125
+ const mockBackspace = vi.fn()
126
+ const mockEnter = vi.fn()
127
+
128
+ km.addObjectKeymap({
129
+ Backspace: mockBackspace,
130
+ Enter: mockEnter,
131
+ })
132
+
133
+ const keymap = km.build()
134
+ expect(keymap['Backspace']).toBeDefined()
135
+ expect(keymap['Enter']).toBeDefined()
136
+ })
@@ -0,0 +1,167 @@
1
+ import type { Command } from '@jvs-milkdown/prose/state'
2
+
3
+ import {
4
+ createSlice,
5
+ createTimer,
6
+ type Ctx,
7
+ type MilkdownPlugin,
8
+ type SliceType,
9
+ } from '@jvs-milkdown/ctx'
10
+ import { ctxCallOutOfScope } from '@jvs-milkdown/exception'
11
+ import {
12
+ baseKeymap,
13
+ chainCommands,
14
+ deleteSelection,
15
+ joinTextblockBackward,
16
+ selectNodeBackward,
17
+ } from '@jvs-milkdown/prose/commands'
18
+ import { undoInputRule } from '@jvs-milkdown/prose/inputrules'
19
+
20
+ import { SchemaReady } from './schema'
21
+
22
+ /// @internal
23
+ export type KeymapItem = {
24
+ key: string
25
+ onRun: (ctx: Ctx) => Command
26
+ priority?: number
27
+ }
28
+
29
+ /// @internal
30
+ export type KeymapKey = SliceType<KeymapItem>
31
+
32
+ function overrideBaseKeymap(keymap: Record<string, Command>) {
33
+ const handleBackspace = chainCommands(
34
+ undoInputRule,
35
+ deleteSelection,
36
+ joinTextblockBackward,
37
+ selectNodeBackward
38
+ )
39
+ keymap.Backspace = handleBackspace
40
+ return keymap
41
+ }
42
+
43
+ /// The keymap manager.
44
+ /// This class is used to manage the keymap.
45
+ export class KeymapManager {
46
+ /// @internal
47
+ #ctx: Ctx | null = null
48
+
49
+ #keymap: KeymapItem[] = []
50
+
51
+ /// @internal
52
+ setCtx = (ctx: Ctx) => {
53
+ this.#ctx = ctx
54
+ }
55
+
56
+ get ctx() {
57
+ return this.#ctx
58
+ }
59
+
60
+ /// Add a keymap item.
61
+ /// When not passing a priority, the priority will be 50.
62
+ /// For the same key, the keymap with higher priority will be executed first.
63
+ /// If the priority is the same, the keymap will be executed in the order of addition.
64
+ add = (keymap: KeymapItem) => {
65
+ this.#keymap.push(keymap)
66
+
67
+ return () => {
68
+ this.#keymap = this.#keymap.filter((item) => item !== keymap)
69
+ }
70
+ }
71
+
72
+ /// Add an object of keymap items.
73
+ addObjectKeymap = (keymaps: Record<string, Command | KeymapItem>) => {
74
+ const remove: (() => void)[] = []
75
+ Object.entries(keymaps).forEach(([key, command]) => {
76
+ if (typeof command === 'function') {
77
+ const keymapItem = {
78
+ key,
79
+ onRun: () => command,
80
+ }
81
+
82
+ this.#keymap.push(keymapItem)
83
+ remove.push(() => {
84
+ this.#keymap = this.#keymap.filter((item) => item !== keymapItem)
85
+ })
86
+ } else {
87
+ this.#keymap.push(command)
88
+ remove.push(() => {
89
+ this.#keymap = this.#keymap.filter((item) => item !== command)
90
+ })
91
+ }
92
+ })
93
+
94
+ return () => {
95
+ remove.forEach((fn) => fn())
96
+ }
97
+ }
98
+
99
+ /// Add the prosemirror base keymap.
100
+ addBaseKeymap = () => {
101
+ const base = overrideBaseKeymap(baseKeymap)
102
+ return this.addObjectKeymap(base)
103
+ }
104
+
105
+ /// @internal
106
+ build = () => {
107
+ const keymap: Record<string, KeymapItem[]> = {}
108
+ this.#keymap.forEach((item) => {
109
+ keymap[item.key] = [...(keymap[item.key] || []), item]
110
+ })
111
+
112
+ const output: Record<string, Command> = Object.fromEntries(
113
+ Object.entries(keymap).map(([key, items]) => {
114
+ const sortedItems = items.sort(
115
+ (a, b) => (b.priority ?? 50) - (a.priority ?? 50)
116
+ )
117
+
118
+ const command: Command = (state, dispatch, view) => {
119
+ const ctx = this.#ctx
120
+ if (ctx == null) throw ctxCallOutOfScope()
121
+
122
+ const commands = sortedItems.map((item) => item.onRun(ctx))
123
+ const chained = chainCommands(...commands)
124
+
125
+ return chained(state, dispatch, view)
126
+ }
127
+
128
+ return [key, command] as const
129
+ })
130
+ )
131
+
132
+ return output
133
+ }
134
+ }
135
+
136
+ /// A slice which stores the keymap manager.
137
+ export const keymapCtx = createSlice(new KeymapManager(), 'keymap')
138
+
139
+ /// A slice which stores timers that need to be waited for before starting to run the plugin.
140
+ /// By default, it's `[SchemaReady]`.
141
+ export const keymapTimerCtx = createSlice([SchemaReady], 'keymapTimer')
142
+
143
+ /// The timer which will be resolved when the keymap plugin is ready.
144
+ export const KeymapReady = createTimer('KeymapReady')
145
+
146
+ /// The keymap plugin.
147
+ /// This plugin will create a keymap manager.
148
+ ///
149
+ /// This plugin will wait for the schema plugin.
150
+ export const keymap: MilkdownPlugin = (ctx) => {
151
+ const km = new KeymapManager()
152
+ km.setCtx(ctx)
153
+ ctx
154
+ .inject(keymapCtx, km)
155
+ .inject(keymapTimerCtx, [SchemaReady])
156
+ .record(KeymapReady)
157
+
158
+ return async () => {
159
+ await ctx.waitTimers(keymapTimerCtx)
160
+
161
+ ctx.done(KeymapReady)
162
+
163
+ return () => {
164
+ ctx.remove(keymapCtx).remove(keymapTimerCtx).clearTimer(KeymapReady)
165
+ }
166
+ }
167
+ }