@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,2 @@
1
+ export * from './utils'
2
+ export * from './remark-handlers'
@@ -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 `&#20;` 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,2 @@
1
+ export * from './editor'
2
+ export * from './internal-plugin'
@@ -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
+ }