@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,48 @@
|
|
|
1
|
+
import type { Options } from 'remark-stringify'
|
|
2
|
+
|
|
3
|
+
export const remarkHandlers: Required<Options>['handlers'] = {
|
|
4
|
+
text: (node, _, state, info) => {
|
|
5
|
+
// This config is to remove the `` entity when have trailing spaces
|
|
6
|
+
const value = node.value
|
|
7
|
+
// Check if the text contains only trailing spaces that might be encoded
|
|
8
|
+
if (/^[^*_\\]*\s+$/.test(value)) {
|
|
9
|
+
// For text that ends with spaces but has no markdown special characters that need escaping,
|
|
10
|
+
// return the value directly to preserve trailing spaces
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
// For other text, use safe to handle markdown escaping but prevent space encoding
|
|
14
|
+
return state.safe(value, { ...info, encode: [] })
|
|
15
|
+
},
|
|
16
|
+
strong: (node, _, state, info) => {
|
|
17
|
+
const marker = node.marker || state.options.strong || '*'
|
|
18
|
+
const exit = state.enter('strong')
|
|
19
|
+
const tracker = state.createTracker(info)
|
|
20
|
+
let value = tracker.move(marker + marker)
|
|
21
|
+
value += tracker.move(
|
|
22
|
+
state.containerPhrasing(node, {
|
|
23
|
+
before: value,
|
|
24
|
+
after: marker,
|
|
25
|
+
...tracker.current(),
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
value += tracker.move(marker + marker)
|
|
29
|
+
exit()
|
|
30
|
+
return value
|
|
31
|
+
},
|
|
32
|
+
emphasis: (node, _, state, info) => {
|
|
33
|
+
const marker = node.marker || state.options.emphasis || '*'
|
|
34
|
+
const exit = state.enter('emphasis')
|
|
35
|
+
const tracker = state.createTracker(info)
|
|
36
|
+
let value = tracker.move(marker)
|
|
37
|
+
value += tracker.move(
|
|
38
|
+
state.containerPhrasing(node, {
|
|
39
|
+
before: value,
|
|
40
|
+
after: marker,
|
|
41
|
+
...tracker.current(),
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
value += tracker.move(marker)
|
|
45
|
+
exit()
|
|
46
|
+
return value
|
|
47
|
+
},
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Meta, MilkdownPlugin } from '@jvs-milkdown/ctx'
|
|
2
|
+
|
|
3
|
+
export function withMeta<T extends MilkdownPlugin>(
|
|
4
|
+
plugin: T,
|
|
5
|
+
meta: Partial<Meta> & Pick<Meta, 'displayName'>
|
|
6
|
+
): T {
|
|
7
|
+
plugin.meta = {
|
|
8
|
+
package: '@jvs-milkdown/core',
|
|
9
|
+
group: 'System',
|
|
10
|
+
...meta,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return plugin
|
|
14
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { CtxRunner, MilkdownPlugin, Telemetry } from '@jvs-milkdown/ctx'
|
|
2
|
+
|
|
3
|
+
import { Clock, Container, Ctx } from '@jvs-milkdown/ctx'
|
|
4
|
+
|
|
5
|
+
import type { Config } from '../internal-plugin'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
commands,
|
|
9
|
+
config,
|
|
10
|
+
editorState,
|
|
11
|
+
editorView,
|
|
12
|
+
init,
|
|
13
|
+
keymap,
|
|
14
|
+
parser,
|
|
15
|
+
pasteRule,
|
|
16
|
+
schema,
|
|
17
|
+
serializer,
|
|
18
|
+
} from '../internal-plugin'
|
|
19
|
+
|
|
20
|
+
/// The status of the editor.
|
|
21
|
+
export enum EditorStatus {
|
|
22
|
+
/// The editor is not initialized.
|
|
23
|
+
Idle = 'Idle',
|
|
24
|
+
/// The editor is creating.
|
|
25
|
+
OnCreate = 'OnCreate',
|
|
26
|
+
/// The editor has been created and ready to use.
|
|
27
|
+
Created = 'Created',
|
|
28
|
+
/// The editor is destroying.
|
|
29
|
+
OnDestroy = 'OnDestroy',
|
|
30
|
+
/// The editor has been destroyed.
|
|
31
|
+
Destroyed = 'Destroyed',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Type for the callback called when editor status changed.
|
|
35
|
+
export type OnStatusChange = (status: EditorStatus) => void
|
|
36
|
+
|
|
37
|
+
type EditorPluginStore = Map<
|
|
38
|
+
MilkdownPlugin,
|
|
39
|
+
{
|
|
40
|
+
ctx: Ctx | undefined
|
|
41
|
+
handler: CtxRunner | undefined
|
|
42
|
+
cleanup: ReturnType<CtxRunner>
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
|
|
46
|
+
/// The milkdown editor class.
|
|
47
|
+
export class Editor {
|
|
48
|
+
/// Create a new editor instance.
|
|
49
|
+
static make() {
|
|
50
|
+
return new Editor()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// @internal
|
|
54
|
+
#enableInspector = false
|
|
55
|
+
/// @internal
|
|
56
|
+
#status = EditorStatus.Idle
|
|
57
|
+
/// @internal
|
|
58
|
+
#configureList: Config[] = []
|
|
59
|
+
/// @internal
|
|
60
|
+
#onStatusChange: OnStatusChange = () => undefined
|
|
61
|
+
|
|
62
|
+
/// @internal
|
|
63
|
+
readonly #container = new Container()
|
|
64
|
+
/// @internal
|
|
65
|
+
readonly #clock = new Clock()
|
|
66
|
+
|
|
67
|
+
/// @internal
|
|
68
|
+
readonly #usrPluginStore: EditorPluginStore = new Map()
|
|
69
|
+
|
|
70
|
+
/// @internal
|
|
71
|
+
readonly #sysPluginStore: EditorPluginStore = new Map()
|
|
72
|
+
|
|
73
|
+
/// @internal
|
|
74
|
+
readonly #ctx = new Ctx(this.#container, this.#clock)
|
|
75
|
+
|
|
76
|
+
/// @internal
|
|
77
|
+
readonly #loadInternal = () => {
|
|
78
|
+
const configPlugin = config(async (ctx) => {
|
|
79
|
+
await Promise.all(
|
|
80
|
+
this.#configureList.map((fn) => Promise.resolve(fn(ctx)))
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
const internalPlugins = [
|
|
84
|
+
schema,
|
|
85
|
+
parser,
|
|
86
|
+
serializer,
|
|
87
|
+
commands,
|
|
88
|
+
keymap,
|
|
89
|
+
pasteRule,
|
|
90
|
+
editorState,
|
|
91
|
+
editorView,
|
|
92
|
+
init(this),
|
|
93
|
+
configPlugin,
|
|
94
|
+
]
|
|
95
|
+
this.#prepare(internalPlugins, this.#sysPluginStore)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// @internal
|
|
99
|
+
readonly #prepare = (plugins: MilkdownPlugin[], store: EditorPluginStore) => {
|
|
100
|
+
plugins.forEach((plugin) => {
|
|
101
|
+
const ctx = this.#ctx.produce(
|
|
102
|
+
this.#enableInspector ? plugin.meta : undefined
|
|
103
|
+
)
|
|
104
|
+
const handler = plugin(ctx)
|
|
105
|
+
store.set(plugin, { ctx, handler, cleanup: undefined })
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// @internal
|
|
110
|
+
readonly #cleanup = (plugins: MilkdownPlugin[], remove = false) => {
|
|
111
|
+
return Promise.all(
|
|
112
|
+
[plugins].flat().map(async (plugin) => {
|
|
113
|
+
const loader = this.#usrPluginStore.get(plugin)
|
|
114
|
+
const cleanup = loader?.cleanup
|
|
115
|
+
if (remove) this.#usrPluginStore.delete(plugin)
|
|
116
|
+
else
|
|
117
|
+
this.#usrPluginStore.set(plugin, {
|
|
118
|
+
ctx: undefined,
|
|
119
|
+
handler: undefined,
|
|
120
|
+
cleanup: undefined,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (typeof cleanup === 'function') return cleanup()
|
|
124
|
+
|
|
125
|
+
return cleanup
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// @internal
|
|
131
|
+
readonly #cleanupInternal = async () => {
|
|
132
|
+
await Promise.all(
|
|
133
|
+
[...this.#sysPluginStore.entries()].map(async ([_, { cleanup }]) => {
|
|
134
|
+
if (typeof cleanup === 'function') return cleanup()
|
|
135
|
+
|
|
136
|
+
return cleanup
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
this.#sysPluginStore.clear()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// @internal
|
|
143
|
+
readonly #setStatus = (status: EditorStatus) => {
|
|
144
|
+
this.#status = status
|
|
145
|
+
this.#onStatusChange(status)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// @internal
|
|
149
|
+
readonly #loadPluginInStore = (store: EditorPluginStore) => {
|
|
150
|
+
return [...store.entries()].map(async ([key, loader]) => {
|
|
151
|
+
const { ctx, handler } = loader
|
|
152
|
+
if (!handler) return
|
|
153
|
+
|
|
154
|
+
const cleanup = await handler()
|
|
155
|
+
|
|
156
|
+
store.set(key, { ctx, handler, cleanup })
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Get the ctx of the editor.
|
|
161
|
+
get ctx() {
|
|
162
|
+
return this.#ctx
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Get the status of the editor.
|
|
166
|
+
get status() {
|
|
167
|
+
return this.#status
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Enable the inspector for the editor.
|
|
171
|
+
/// You can also pass `false` to disable the inspector.
|
|
172
|
+
readonly enableInspector = (enable = true) => {
|
|
173
|
+
this.#enableInspector = enable
|
|
174
|
+
|
|
175
|
+
return this
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Subscribe to the status change event for the editor.
|
|
179
|
+
/// The new subscription will replace the old one.
|
|
180
|
+
readonly onStatusChange = (onChange: OnStatusChange) => {
|
|
181
|
+
this.#onStatusChange = onChange
|
|
182
|
+
return this
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Add a config for the editor.
|
|
186
|
+
readonly config = (configure: Config) => {
|
|
187
|
+
this.#configureList.push(configure)
|
|
188
|
+
return this
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Remove a config for the editor.
|
|
192
|
+
readonly removeConfig = (configure: Config) => {
|
|
193
|
+
this.#configureList = this.#configureList.filter((x) => x !== configure)
|
|
194
|
+
return this
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Use a plugin or a list of plugins for the editor.
|
|
198
|
+
readonly use = (plugins: MilkdownPlugin | MilkdownPlugin[]) => {
|
|
199
|
+
const _plugins = [plugins].flat()
|
|
200
|
+
_plugins.flat().forEach((plugin) => {
|
|
201
|
+
this.#usrPluginStore.set(plugin, {
|
|
202
|
+
ctx: undefined,
|
|
203
|
+
handler: undefined,
|
|
204
|
+
cleanup: undefined,
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
if (this.#status === EditorStatus.Created)
|
|
209
|
+
this.#prepare(_plugins, this.#usrPluginStore)
|
|
210
|
+
|
|
211
|
+
return this
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Remove a plugin or a list of plugins from the editor.
|
|
215
|
+
readonly remove = async (
|
|
216
|
+
plugins: MilkdownPlugin | MilkdownPlugin[]
|
|
217
|
+
): Promise<Editor> => {
|
|
218
|
+
if (this.#status === EditorStatus.OnCreate) {
|
|
219
|
+
console.warn(
|
|
220
|
+
'[Milkdown]: You are trying to remove plugins when the editor is creating, this is not recommended, please check your code.'
|
|
221
|
+
)
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
resolve(this.remove(plugins))
|
|
225
|
+
}, 50)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await this.#cleanup([plugins].flat(), true)
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Create the editor with current config and plugins.
|
|
234
|
+
/// If the editor is already created, it will be recreated.
|
|
235
|
+
readonly create = async (): Promise<Editor> => {
|
|
236
|
+
if (this.#status === EditorStatus.OnCreate) return this
|
|
237
|
+
|
|
238
|
+
if (this.#status === EditorStatus.Created) await this.destroy()
|
|
239
|
+
|
|
240
|
+
this.#setStatus(EditorStatus.OnCreate)
|
|
241
|
+
|
|
242
|
+
this.#loadInternal()
|
|
243
|
+
this.#prepare([...this.#usrPluginStore.keys()], this.#usrPluginStore)
|
|
244
|
+
|
|
245
|
+
await Promise.all(
|
|
246
|
+
[
|
|
247
|
+
this.#loadPluginInStore(this.#sysPluginStore),
|
|
248
|
+
this.#loadPluginInStore(this.#usrPluginStore),
|
|
249
|
+
].flat()
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
this.#setStatus(EditorStatus.Created)
|
|
253
|
+
return this
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Destroy the editor.
|
|
257
|
+
/// If you want to clear all plugins, set `clearPlugins` to `true`.
|
|
258
|
+
readonly destroy = async (clearPlugins = false): Promise<Editor> => {
|
|
259
|
+
if (
|
|
260
|
+
this.#status === EditorStatus.Destroyed ||
|
|
261
|
+
this.#status === EditorStatus.OnDestroy
|
|
262
|
+
)
|
|
263
|
+
return this
|
|
264
|
+
|
|
265
|
+
if (this.#status === EditorStatus.OnCreate) {
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
resolve(this.destroy(clearPlugins))
|
|
269
|
+
}, 50)
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (clearPlugins) this.#configureList = []
|
|
274
|
+
|
|
275
|
+
this.#setStatus(EditorStatus.OnDestroy)
|
|
276
|
+
await this.#cleanup([...this.#usrPluginStore.keys()], clearPlugins)
|
|
277
|
+
await this.#cleanupInternal()
|
|
278
|
+
|
|
279
|
+
this.#setStatus(EditorStatus.Destroyed)
|
|
280
|
+
return this
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// Call an action with the ctx of the editor.
|
|
284
|
+
/// This method should be used after the editor is created.
|
|
285
|
+
readonly action = <T>(action: (ctx: Ctx) => T) => action(this.#ctx)
|
|
286
|
+
|
|
287
|
+
/// Get inspections of plugins in editor.
|
|
288
|
+
/// Make sure you have enabled inspector by `editor.enableInspector()` before calling this method.
|
|
289
|
+
readonly inspect = (): Telemetry[] => {
|
|
290
|
+
if (!this.#enableInspector) {
|
|
291
|
+
console.warn(
|
|
292
|
+
'[Milkdown]: You are trying to collect inspection when inspector is disabled, please enable inspector by `editor.enableInspector()` first.'
|
|
293
|
+
)
|
|
294
|
+
return []
|
|
295
|
+
}
|
|
296
|
+
return [...this.#sysPluginStore.values(), ...this.#usrPluginStore.values()]
|
|
297
|
+
.map(({ ctx }) => ctx?.inspector?.read())
|
|
298
|
+
.filter((x): x is Telemetry => Boolean(x))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './editor'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SliceType, TimerType } from '@jvs-milkdown/ctx'
|
|
2
|
+
import type { InputRule } from '@jvs-milkdown/prose/inputrules'
|
|
3
|
+
import type { EditorState, Plugin } from '@jvs-milkdown/prose/state'
|
|
4
|
+
import type {
|
|
5
|
+
EditorView,
|
|
6
|
+
MarkViewConstructor,
|
|
7
|
+
NodeViewConstructor,
|
|
8
|
+
} from '@jvs-milkdown/prose/view'
|
|
9
|
+
import type { RemarkParser, RemarkPlugin } from '@jvs-milkdown/transformer'
|
|
10
|
+
import type { Options } from 'remark-stringify'
|
|
11
|
+
|
|
12
|
+
import { createSlice } from '@jvs-milkdown/ctx'
|
|
13
|
+
import remarkParse from 'remark-parse'
|
|
14
|
+
import remarkStringify from 'remark-stringify'
|
|
15
|
+
import { unified } from 'unified'
|
|
16
|
+
|
|
17
|
+
import type { Editor } from '../editor'
|
|
18
|
+
|
|
19
|
+
import { remarkHandlers } from '../__internal__'
|
|
20
|
+
|
|
21
|
+
/// A slice which contains the editor view instance.
|
|
22
|
+
export const editorViewCtx = createSlice({} as EditorView, 'editorView')
|
|
23
|
+
|
|
24
|
+
/// A slice which contains the editor state.
|
|
25
|
+
export const editorStateCtx = createSlice({} as EditorState, 'editorState')
|
|
26
|
+
|
|
27
|
+
/// A slice which stores timers that need to be waited for before starting to run the plugin.
|
|
28
|
+
/// By default, it's `[ConfigReady]`.
|
|
29
|
+
export const initTimerCtx = createSlice([] as TimerType[], 'initTimer')
|
|
30
|
+
|
|
31
|
+
/// A slice which stores the editor instance.
|
|
32
|
+
export const editorCtx = createSlice({} as Editor, 'editor')
|
|
33
|
+
|
|
34
|
+
/// A slice which stores the input rules.
|
|
35
|
+
export const inputRulesCtx = createSlice([] as InputRule[], 'inputRules')
|
|
36
|
+
|
|
37
|
+
/// A slice which stores the prosemirror plugins.
|
|
38
|
+
export const prosePluginsCtx = createSlice([] as Plugin[], 'prosePlugins')
|
|
39
|
+
|
|
40
|
+
/// A slice which stores the remark plugins.
|
|
41
|
+
export const remarkPluginsCtx = createSlice(
|
|
42
|
+
[] as RemarkPlugin[],
|
|
43
|
+
'remarkPlugins'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
type NodeView = [nodeId: string, view: NodeViewConstructor]
|
|
47
|
+
|
|
48
|
+
/// A slice which stores the prosemirror node views.
|
|
49
|
+
export const nodeViewCtx = createSlice([] as NodeView[], 'nodeView')
|
|
50
|
+
|
|
51
|
+
type MarkView = [nodeId: string, view: MarkViewConstructor]
|
|
52
|
+
|
|
53
|
+
/// A slice which stores the prosemirror mark views.
|
|
54
|
+
export const markViewCtx = createSlice([] as MarkView[], 'markView')
|
|
55
|
+
|
|
56
|
+
/// A slice which stores the remark instance.
|
|
57
|
+
export const remarkCtx: SliceType<RemarkParser, 'remark'> = createSlice(
|
|
58
|
+
unified().use(remarkParse).use(remarkStringify),
|
|
59
|
+
'remark'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
/// A slice which stores the remark stringify options.
|
|
63
|
+
export const remarkStringifyOptionsCtx = createSlice(
|
|
64
|
+
{
|
|
65
|
+
handlers: remarkHandlers,
|
|
66
|
+
encode: [],
|
|
67
|
+
} as Options,
|
|
68
|
+
'remarkStringifyOptions'
|
|
69
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { Ctx, MilkdownPlugin, SliceType } from '@jvs-milkdown/ctx'
|
|
2
|
+
import type { Command } from '@jvs-milkdown/prose/state'
|
|
3
|
+
|
|
4
|
+
import { Container, createSlice, createTimer } from '@jvs-milkdown/ctx'
|
|
5
|
+
import { callCommandBeforeEditorView } from '@jvs-milkdown/exception'
|
|
6
|
+
import { chainCommands } from '@jvs-milkdown/prose/commands'
|
|
7
|
+
|
|
8
|
+
import { withMeta } from '../__internal__'
|
|
9
|
+
import { editorViewCtx } from './atoms'
|
|
10
|
+
import { SchemaReady } from './schema'
|
|
11
|
+
|
|
12
|
+
/// @internal
|
|
13
|
+
export type Cmd<T = undefined> = (payload?: T) => Command
|
|
14
|
+
|
|
15
|
+
/// @internal
|
|
16
|
+
export type CmdKey<T = undefined> = SliceType<Cmd<T>>
|
|
17
|
+
|
|
18
|
+
type InferParams<T> = T extends CmdKey<infer U> ? U : never
|
|
19
|
+
|
|
20
|
+
/// A chainable command helper.
|
|
21
|
+
export interface CommandChain {
|
|
22
|
+
/// Run the command chain.
|
|
23
|
+
run: () => boolean
|
|
24
|
+
/// Add an inline command to the chain.
|
|
25
|
+
inline: (command: Command) => CommandChain
|
|
26
|
+
/// Add a registered command to the chain.
|
|
27
|
+
pipe: {
|
|
28
|
+
<T extends CmdKey<any>>(
|
|
29
|
+
slice: string,
|
|
30
|
+
payload?: InferParams<T>
|
|
31
|
+
): CommandChain
|
|
32
|
+
<T>(slice: CmdKey<T>, payload?: T): CommandChain
|
|
33
|
+
(slice: string | CmdKey<any>, payload?: any): CommandChain
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// The command manager.
|
|
38
|
+
/// This manager will manage all commands in editor.
|
|
39
|
+
/// Generally, you don't need to use this manager directly.
|
|
40
|
+
/// You can use the `$command` and `$commandAsync` in `@jvs-milkdown/utils` to create and call a command.
|
|
41
|
+
export class CommandManager {
|
|
42
|
+
/// @internal
|
|
43
|
+
#container = new Container()
|
|
44
|
+
|
|
45
|
+
/// @internal
|
|
46
|
+
#ctx: Ctx | null = null
|
|
47
|
+
|
|
48
|
+
/// @internal
|
|
49
|
+
setCtx = (ctx: Ctx) => {
|
|
50
|
+
this.#ctx = ctx
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get ctx() {
|
|
54
|
+
return this.#ctx
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Register a command into the manager.
|
|
58
|
+
create<T>(meta: CmdKey<T>, value: Cmd<T>) {
|
|
59
|
+
const slice = meta.create(this.#container.sliceMap)
|
|
60
|
+
slice.set(value)
|
|
61
|
+
return slice
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Get a command from the manager.
|
|
65
|
+
get<T extends CmdKey<any>>(slice: string): Cmd<InferParams<T>>
|
|
66
|
+
get<T>(slice: CmdKey<T>): Cmd<T>
|
|
67
|
+
get(slice: string | CmdKey<any>): Cmd<any>
|
|
68
|
+
get(slice: string | CmdKey<any>): Cmd<any> {
|
|
69
|
+
return this.#container.get(slice).get()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Remove a command from the manager.
|
|
73
|
+
remove<T extends CmdKey<any>>(slice: string): void
|
|
74
|
+
remove<T>(slice: CmdKey<T>): void
|
|
75
|
+
remove(slice: string | CmdKey<any>): void
|
|
76
|
+
remove(slice: string | CmdKey<any>): void {
|
|
77
|
+
return this.#container.remove(slice)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Call a registered command.
|
|
81
|
+
call<T extends CmdKey<any>>(slice: string, payload?: InferParams<T>): boolean
|
|
82
|
+
call<T>(slice: CmdKey<T>, payload?: T): boolean
|
|
83
|
+
call(slice: string | CmdKey<any>, payload?: any): boolean
|
|
84
|
+
call(slice: string | CmdKey<any>, payload?: any): boolean {
|
|
85
|
+
if (this.#ctx == null) throw callCommandBeforeEditorView()
|
|
86
|
+
|
|
87
|
+
const cmd = this.get(slice)
|
|
88
|
+
const command = cmd(payload)
|
|
89
|
+
const view = this.#ctx.get(editorViewCtx)
|
|
90
|
+
return command(view.state, view.dispatch, view)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Call an inline command.
|
|
94
|
+
inline(command: Command) {
|
|
95
|
+
if (this.#ctx == null) throw callCommandBeforeEditorView()
|
|
96
|
+
const view = this.#ctx.get(editorViewCtx)
|
|
97
|
+
return command(view.state, view.dispatch, view)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Create a command chain.
|
|
101
|
+
/// All commands added by `pipe` will be run in order until one of them returns `true`.
|
|
102
|
+
chain = (): CommandChain => {
|
|
103
|
+
if (this.#ctx == null) throw callCommandBeforeEditorView()
|
|
104
|
+
const ctx = this.#ctx
|
|
105
|
+
const commands: Command[] = []
|
|
106
|
+
const get = this.get.bind(this)
|
|
107
|
+
|
|
108
|
+
const chains: CommandChain = {
|
|
109
|
+
run: () => {
|
|
110
|
+
const chained = chainCommands(...commands)
|
|
111
|
+
const view = ctx.get(editorViewCtx)
|
|
112
|
+
return chained(view.state, view.dispatch, view)
|
|
113
|
+
},
|
|
114
|
+
inline: (command: Command) => {
|
|
115
|
+
commands.push(command)
|
|
116
|
+
return chains
|
|
117
|
+
},
|
|
118
|
+
pipe: pipe.bind(this),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pipe<T extends CmdKey<any>>(
|
|
122
|
+
slice: string,
|
|
123
|
+
payload?: InferParams<T>
|
|
124
|
+
): typeof chains
|
|
125
|
+
function pipe<T>(slice: CmdKey<T>, payload?: T): typeof chains
|
|
126
|
+
function pipe(slice: string | CmdKey<any>, payload?: any): typeof chains
|
|
127
|
+
function pipe(slice: string | CmdKey<any>, payload?: any) {
|
|
128
|
+
const cmd = get(slice)
|
|
129
|
+
commands.push(cmd(payload))
|
|
130
|
+
return chains
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return chains
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Create a command key, which is a slice type that contains a command.
|
|
138
|
+
export function createCmdKey<T = undefined>(key = 'cmdKey'): CmdKey<T> {
|
|
139
|
+
return createSlice((() => () => false) as Cmd<T>, key)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// A slice which contains the command manager.
|
|
143
|
+
export const commandsCtx = createSlice(new CommandManager(), 'commands')
|
|
144
|
+
|
|
145
|
+
/// A slice which stores timers that need to be waited for before starting to run the plugin.
|
|
146
|
+
/// By default, it's `[SchemaReady]`.
|
|
147
|
+
export const commandsTimerCtx = createSlice([SchemaReady], 'commandsTimer')
|
|
148
|
+
|
|
149
|
+
/// The timer which will be resolved when the commands plugin is ready.
|
|
150
|
+
export const CommandsReady = createTimer('CommandsReady')
|
|
151
|
+
|
|
152
|
+
/// The commands plugin.
|
|
153
|
+
/// This plugin will create a command manager.
|
|
154
|
+
///
|
|
155
|
+
/// This plugin will wait for the schema plugin.
|
|
156
|
+
export const commands: MilkdownPlugin = (ctx) => {
|
|
157
|
+
const cmd = new CommandManager()
|
|
158
|
+
cmd.setCtx(ctx)
|
|
159
|
+
ctx
|
|
160
|
+
.inject(commandsCtx, cmd)
|
|
161
|
+
.inject(commandsTimerCtx, [SchemaReady])
|
|
162
|
+
.record(CommandsReady)
|
|
163
|
+
return async () => {
|
|
164
|
+
await ctx.waitTimers(commandsTimerCtx)
|
|
165
|
+
|
|
166
|
+
ctx.done(CommandsReady)
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
ctx.remove(commandsCtx).remove(commandsTimerCtx).clearTimer(CommandsReady)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
withMeta(commands, {
|
|
175
|
+
displayName: 'Commands',
|
|
176
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Ctx, MilkdownPlugin } from '@jvs-milkdown/ctx'
|
|
2
|
+
|
|
3
|
+
import { createTimer } from '@jvs-milkdown/ctx'
|
|
4
|
+
|
|
5
|
+
import { withMeta } from '../__internal__'
|
|
6
|
+
|
|
7
|
+
/// @internal
|
|
8
|
+
export type Config = (ctx: Ctx) => void | Promise<void>
|
|
9
|
+
|
|
10
|
+
/// The timer which will be resolved when the config plugin is ready.
|
|
11
|
+
export const ConfigReady = createTimer('ConfigReady')
|
|
12
|
+
|
|
13
|
+
/// The config plugin.
|
|
14
|
+
/// This plugin will load all user configs.
|
|
15
|
+
export function config(configure: Config): MilkdownPlugin {
|
|
16
|
+
const plugin: MilkdownPlugin = (ctx) => {
|
|
17
|
+
ctx.record(ConfigReady)
|
|
18
|
+
|
|
19
|
+
return async () => {
|
|
20
|
+
await configure(ctx)
|
|
21
|
+
ctx.done(ConfigReady)
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
ctx.clearTimer(ConfigReady)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
withMeta(plugin, {
|
|
30
|
+
displayName: 'Config',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return plugin
|
|
34
|
+
}
|