@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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/lib/__internal__/index.d.ts +3 -0
- package/lib/__internal__/index.d.ts.map +1 -0
- package/lib/__internal__/remark-handlers.d.ts +3 -0
- package/lib/__internal__/remark-handlers.d.ts.map +1 -0
- package/lib/__internal__/utils.d.ts +3 -0
- package/lib/__internal__/utils.d.ts.map +1 -0
- package/lib/editor/editor.d.ts +28 -0
- package/lib/editor/editor.d.ts.map +1 -0
- package/lib/editor/index.d.ts +2 -0
- package/lib/editor/index.d.ts.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +676 -0
- package/lib/index.js.map +1 -0
- package/lib/internal-plugin/atoms.d.ts +22 -0
- package/lib/internal-plugin/atoms.d.ts.map +1 -0
- package/lib/internal-plugin/commands.d.ts +38 -0
- package/lib/internal-plugin/commands.d.ts.map +1 -0
- package/lib/internal-plugin/config.d.ts +5 -0
- package/lib/internal-plugin/config.d.ts.map +1 -0
- package/lib/internal-plugin/editor-state.d.ts +22 -0
- package/lib/internal-plugin/editor-state.d.ts.map +1 -0
- package/lib/internal-plugin/editor-view.d.ts +13 -0
- package/lib/internal-plugin/editor-view.d.ts.map +1 -0
- package/lib/internal-plugin/index.d.ts +12 -0
- package/lib/internal-plugin/index.d.ts.map +1 -0
- package/lib/internal-plugin/init.d.ts +5 -0
- package/lib/internal-plugin/init.d.ts.map +1 -0
- package/lib/internal-plugin/keymap.d.ts +22 -0
- package/lib/internal-plugin/keymap.d.ts.map +1 -0
- package/lib/internal-plugin/keymap.test.d.ts +2 -0
- package/lib/internal-plugin/keymap.test.d.ts.map +1 -0
- package/lib/internal-plugin/parser.d.ts +7 -0
- package/lib/internal-plugin/parser.d.ts.map +1 -0
- package/lib/internal-plugin/paste-rule.d.ts +12 -0
- package/lib/internal-plugin/paste-rule.d.ts.map +1 -0
- package/lib/internal-plugin/schema.d.ts +10 -0
- package/lib/internal-plugin/schema.d.ts.map +1 -0
- package/lib/internal-plugin/serializer.d.ts +7 -0
- package/lib/internal-plugin/serializer.d.ts.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +45 -0
- package/src/__internal__/index.ts +2 -0
- package/src/__internal__/remark-handlers.ts +48 -0
- package/src/__internal__/utils.ts +14 -0
- package/src/editor/editor.ts +300 -0
- package/src/editor/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/internal-plugin/atoms.ts +69 -0
- package/src/internal-plugin/commands.ts +176 -0
- package/src/internal-plugin/config.ts +34 -0
- package/src/internal-plugin/editor-state.ts +140 -0
- package/src/internal-plugin/editor-view.ts +166 -0
- package/src/internal-plugin/index.ts +11 -0
- package/src/internal-plugin/init.ts +78 -0
- package/src/internal-plugin/keymap.test.ts +136 -0
- package/src/internal-plugin/keymap.ts +167 -0
- package/src/internal-plugin/parser.ts +51 -0
- package/src/internal-plugin/paste-rule.ts +53 -0
- package/src/internal-plugin/schema.ts +88 -0
- 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
|
+
}
|