@jvs-milkdown/utils 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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/lib/composable/$command.d.ts +9 -0
  4. package/lib/composable/$command.d.ts.map +1 -0
  5. package/lib/composable/$ctx.d.ts +6 -0
  6. package/lib/composable/$ctx.d.ts.map +1 -0
  7. package/lib/composable/$input-rule.d.ts +8 -0
  8. package/lib/composable/$input-rule.d.ts.map +1 -0
  9. package/lib/composable/$mark.d.ts +11 -0
  10. package/lib/composable/$mark.d.ts.map +1 -0
  11. package/lib/composable/$node.d.ts +11 -0
  12. package/lib/composable/$node.d.ts.map +1 -0
  13. package/lib/composable/$paste-rule.d.ts +8 -0
  14. package/lib/composable/$paste-rule.d.ts.map +1 -0
  15. package/lib/composable/$prose.d.ts +9 -0
  16. package/lib/composable/$prose.d.ts.map +1 -0
  17. package/lib/composable/$shortcut.d.ts +10 -0
  18. package/lib/composable/$shortcut.d.ts.map +1 -0
  19. package/lib/composable/$view.d.ts +11 -0
  20. package/lib/composable/$view.d.ts.map +1 -0
  21. package/lib/composable/composed/$attr.d.ts +7 -0
  22. package/lib/composable/composed/$attr.d.ts.map +1 -0
  23. package/lib/composable/composed/$mark-schema.d.ts +18 -0
  24. package/lib/composable/composed/$mark-schema.d.ts.map +1 -0
  25. package/lib/composable/composed/$node-schema.d.ts +18 -0
  26. package/lib/composable/composed/$node-schema.d.ts.map +1 -0
  27. package/lib/composable/composed/$node-schema.spec.d.ts +2 -0
  28. package/lib/composable/composed/$node-schema.spec.d.ts.map +1 -0
  29. package/lib/composable/composed/$remark.d.ts +13 -0
  30. package/lib/composable/composed/$remark.d.ts.map +1 -0
  31. package/lib/composable/composed/$use-keymap.d.ts +25 -0
  32. package/lib/composable/composed/$use-keymap.d.ts.map +1 -0
  33. package/lib/composable/composed/index.d.ts +6 -0
  34. package/lib/composable/composed/index.d.ts.map +1 -0
  35. package/lib/composable/index.d.ts +12 -0
  36. package/lib/composable/index.d.ts.map +1 -0
  37. package/lib/composable/utils.d.ts +7 -0
  38. package/lib/composable/utils.d.ts.map +1 -0
  39. package/lib/index.d.ts +4 -0
  40. package/lib/index.d.ts.map +1 -0
  41. package/lib/index.js +549 -0
  42. package/lib/index.js.map +1 -0
  43. package/lib/macro/call-command.d.ts +8 -0
  44. package/lib/macro/call-command.d.ts.map +1 -0
  45. package/lib/macro/force-update.d.ts +3 -0
  46. package/lib/macro/force-update.d.ts.map +1 -0
  47. package/lib/macro/get-html.d.ts +3 -0
  48. package/lib/macro/get-html.d.ts.map +1 -0
  49. package/lib/macro/get-markdown.d.ts +6 -0
  50. package/lib/macro/get-markdown.d.ts.map +1 -0
  51. package/lib/macro/index.d.ts +12 -0
  52. package/lib/macro/index.d.ts.map +1 -0
  53. package/lib/macro/insert-pos.d.ts +3 -0
  54. package/lib/macro/insert-pos.d.ts.map +1 -0
  55. package/lib/macro/insert.d.ts +3 -0
  56. package/lib/macro/insert.d.ts.map +1 -0
  57. package/lib/macro/markdown-to-slice.d.ts +3 -0
  58. package/lib/macro/markdown-to-slice.d.ts.map +1 -0
  59. package/lib/macro/outline.d.ts +7 -0
  60. package/lib/macro/outline.d.ts.map +1 -0
  61. package/lib/macro/replace-all.d.ts +3 -0
  62. package/lib/macro/replace-all.d.ts.map +1 -0
  63. package/lib/macro/replace-range.d.ts +6 -0
  64. package/lib/macro/replace-range.d.ts.map +1 -0
  65. package/lib/macro/set-attr.d.ts +4 -0
  66. package/lib/macro/set-attr.d.ts.map +1 -0
  67. package/lib/pipe.d.ts +7 -0
  68. package/lib/pipe.d.ts.map +1 -0
  69. package/lib/tsconfig.tsbuildinfo +1 -0
  70. package/package.json +35 -0
  71. package/src/composable/$command.ts +102 -0
  72. package/src/composable/$ctx.ts +31 -0
  73. package/src/composable/$input-rule.ts +55 -0
  74. package/src/composable/$mark.ts +87 -0
  75. package/src/composable/$node.ts +90 -0
  76. package/src/composable/$paste-rule.ts +59 -0
  77. package/src/composable/$prose.ts +70 -0
  78. package/src/composable/$shortcut.ts +66 -0
  79. package/src/composable/$view.ts +110 -0
  80. package/src/composable/composed/$attr.ts +29 -0
  81. package/src/composable/composed/$mark-schema.ts +65 -0
  82. package/src/composable/composed/$node-schema.spec.ts +139 -0
  83. package/src/composable/composed/$node-schema.ts +65 -0
  84. package/src/composable/composed/$remark.ts +53 -0
  85. package/src/composable/composed/$use-keymap.ts +91 -0
  86. package/src/composable/composed/index.ts +5 -0
  87. package/src/composable/index.ts +12 -0
  88. package/src/composable/utils.ts +63 -0
  89. package/src/index.ts +3 -0
  90. package/src/macro/call-command.ts +28 -0
  91. package/src/macro/force-update.ts +14 -0
  92. package/src/macro/get-html.ts +20 -0
  93. package/src/macro/get-markdown.ts +27 -0
  94. package/src/macro/index.ts +11 -0
  95. package/src/macro/insert-pos.ts +27 -0
  96. package/src/macro/insert.ts +38 -0
  97. package/src/macro/markdown-to-slice.ts +18 -0
  98. package/src/macro/outline.ts +21 -0
  99. package/src/macro/replace-all.ts +46 -0
  100. package/src/macro/replace-range.ts +18 -0
  101. package/src/macro/set-attr.ts +16 -0
  102. package/src/pipe.ts +72 -0
@@ -0,0 +1,139 @@
1
+ import { Editor, schemaCtx } from '@jvs-milkdown/core'
2
+ import { test, expect } from 'vitest'
3
+
4
+ import { $nodeSchema } from './$node-schema'
5
+
6
+ const docSchema = $nodeSchema('doc', () => {
7
+ return {
8
+ content: 'block*',
9
+ parseMarkdown: {
10
+ match: ({ type }) => type === 'root',
11
+ runner: (state, node, type) => {
12
+ state.injectRoot(node, type)
13
+ },
14
+ },
15
+ toMarkdown: {
16
+ match: (node) => node.type.name === 'doc',
17
+ runner: () => {},
18
+ },
19
+ }
20
+ })
21
+
22
+ const textSchema = $nodeSchema('text', () => ({
23
+ group: 'inline',
24
+ parseMarkdown: {
25
+ match: ({ type }) => type === 'text',
26
+ runner: () => {},
27
+ },
28
+ toMarkdown: {
29
+ match: (node) => node.type.name === 'text',
30
+ runner: () => {},
31
+ },
32
+ }))
33
+
34
+ const paragraphSchema = $nodeSchema('paragraph', () => ({
35
+ content: 'inline*',
36
+ group: 'block',
37
+ parseDOM: [{ tag: 'p' }],
38
+ toDOM: () => ['p', 0],
39
+ parseMarkdown: {
40
+ match: (node) => node.type === 'paragraph',
41
+ runner: () => {},
42
+ },
43
+ toMarkdown: {
44
+ match: (node) => node.type.name === 'paragraph',
45
+ runner: () => {},
46
+ },
47
+ }))
48
+
49
+ test('create schema', async () => {
50
+ const editor = Editor.make()
51
+ editor.use(docSchema)
52
+ editor.use(textSchema)
53
+ editor.use(paragraphSchema)
54
+ await editor.create()
55
+
56
+ expect(docSchema.node.schema.content).toEqual('block*')
57
+ })
58
+
59
+ test('extend schema', async () => {
60
+ const extended = docSchema.extendSchema((prev) => (ctx) => ({
61
+ ...prev(ctx),
62
+ defining: true,
63
+ }))
64
+
65
+ const editor = await Editor.make()
66
+ .use(docSchema)
67
+ .use(extended)
68
+ .use(paragraphSchema)
69
+ .use(textSchema)
70
+ .create()
71
+
72
+ expect(extended.node.schema.defining).toBe(true)
73
+ expect(docSchema.node.schema.defining).toBe(undefined)
74
+
75
+ const schema = editor.ctx.get(schemaCtx)
76
+ expect(schema.nodes.doc?.spec).toEqual(extended.node.schema)
77
+ expect(Object.keys(schema.nodes)).toEqual(['doc', 'paragraph', 'text'])
78
+ })
79
+
80
+ test('double extend schema', async () => {
81
+ const extended = docSchema.extendSchema((prev) => (ctx) => ({
82
+ ...prev(ctx),
83
+ defining: true,
84
+ }))
85
+
86
+ const extended2 = extended.extendSchema((prev) => (ctx) => ({
87
+ ...prev(ctx),
88
+ atom: true,
89
+ }))
90
+
91
+ const editor = await Editor.make()
92
+ .use(docSchema)
93
+ .use(extended)
94
+ .use(extended2)
95
+ .use(paragraphSchema)
96
+ .use(textSchema)
97
+ .create()
98
+
99
+ expect(extended2.node.schema.atom).toBe(true)
100
+ expect(extended2.node.schema.defining).toBe(true)
101
+
102
+ expect(extended.node.schema.atom).toBe(undefined)
103
+ expect(extended.node.schema.defining).toBe(true)
104
+
105
+ expect(docSchema.node.schema.atom).toBe(undefined)
106
+ expect(docSchema.node.schema.defining).toBe(undefined)
107
+
108
+ const schema = editor.ctx.get(schemaCtx)
109
+ expect(schema.nodes.doc?.spec).toEqual(extended2.node.schema)
110
+ expect(Object.keys(schema.nodes)).toEqual(['doc', 'paragraph', 'text'])
111
+ })
112
+
113
+ test('should can register extended schema only', async () => {
114
+ const extended = docSchema.extendSchema((prev) => (ctx) => ({
115
+ ...prev(ctx),
116
+ defining: true,
117
+ }))
118
+
119
+ const extended2 = extended.extendSchema((prev) => (ctx) => ({
120
+ ...prev(ctx),
121
+ atom: true,
122
+ }))
123
+
124
+ const editor = await Editor.make()
125
+ .use(extended2)
126
+ .use(paragraphSchema)
127
+ .use(textSchema)
128
+ .create()
129
+
130
+ expect(extended2.node.schema.atom).toBe(true)
131
+ expect(extended2.node.schema.defining).toBe(true)
132
+
133
+ // this schema is not registered, so it should be undefined
134
+ expect(extended.node.schema).toBe(undefined)
135
+
136
+ const schema = editor.ctx.get(schemaCtx)
137
+ expect(schema.nodes.doc?.spec).toEqual(extended2.node.schema)
138
+ expect(Object.keys(schema.nodes)).toEqual(['doc', 'paragraph', 'text'])
139
+ })
@@ -0,0 +1,65 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+ import type { NodeSchema } from '@jvs-milkdown/transformer'
3
+
4
+ import type { $Ctx } from '../$ctx'
5
+ import type { $Node } from '../$node'
6
+
7
+ import { $ctx } from '../$ctx'
8
+ import { $node } from '../$node'
9
+
10
+ /// @internal
11
+ export type GetNodeSchema = (ctx: Ctx) => NodeSchema
12
+
13
+ /// @internal
14
+ export type $NodeSchema<T extends string> = [
15
+ schemaCtx: $Ctx<GetNodeSchema, T>,
16
+ schema: $Node,
17
+ ] & {
18
+ id: $Node['id']
19
+ type: $Node['type']
20
+ node: $Node
21
+ ctx: $Ctx<GetNodeSchema, T>
22
+ key: $Ctx<GetNodeSchema, T>['key']
23
+ extendSchema: (
24
+ handler: (prev: GetNodeSchema) => GetNodeSchema
25
+ ) => $NodeSchema<T>
26
+ }
27
+
28
+ /// Create a plugin for node schema.
29
+ /// The first parameter is the id of the node schema.
30
+ /// The second parameter is the function that returns the node schema.
31
+ ///
32
+ /// The function will return a plugin with additional properties:
33
+ /// - `id`: The id of the node schema.
34
+ /// - `type`: A function witch will return the type of the node schema.
35
+ /// - `ctx`: The context of the node schema.
36
+ /// - `node`: The node schema plugin.
37
+ /// - `schema`: The node schema.
38
+ /// - `key`: The key of slice which contains the node schema factory.
39
+ /// - `extendSchema`: A function witch will return a plugin that can extend the node schema.
40
+ export function $nodeSchema<T extends string>(
41
+ id: T,
42
+ schema: GetNodeSchema
43
+ ): $NodeSchema<T> {
44
+ const schemaCtx = $ctx(schema, id)
45
+
46
+ const nodeSchema = $node(id, (ctx) => {
47
+ const userSchema = ctx.get(schemaCtx.key)
48
+ return userSchema(ctx)
49
+ })
50
+
51
+ const result = [schemaCtx, nodeSchema] as $NodeSchema<T>
52
+ result.id = nodeSchema.id
53
+ result.node = nodeSchema
54
+
55
+ result.type = (ctx: Ctx) => nodeSchema.type(ctx)
56
+ result.ctx = schemaCtx
57
+ result.key = schemaCtx.key
58
+ result.extendSchema = (handler) => {
59
+ const nextSchema = handler(schema)
60
+
61
+ return $nodeSchema(id, nextSchema)
62
+ }
63
+
64
+ return result
65
+ }
@@ -0,0 +1,53 @@
1
+ import type { Ctx, MilkdownPlugin } from '@jvs-milkdown/ctx'
2
+ import type { RemarkPlugin, RemarkPluginRaw } from '@jvs-milkdown/transformer'
3
+
4
+ import { InitReady, remarkPluginsCtx } from '@jvs-milkdown/core'
5
+
6
+ import type { $Ctx } from '../$ctx'
7
+
8
+ import { $ctx } from '../$ctx'
9
+
10
+ /// @internal
11
+ export type $Remark<Id extends string, Options> = [
12
+ optionsCtx: $Ctx<Options, Id>,
13
+ plugin: MilkdownPlugin,
14
+ ] & {
15
+ id: Id
16
+ plugin: MilkdownPlugin
17
+ options: $Ctx<Options, Id>
18
+ }
19
+
20
+ /// Create a milkdown wrapper for [remark plugin](https://github.com/remarkjs/remark/blob/main/doc/plugins.md).
21
+ /// It takes a factory function which returns a [remark plugin](https://github.com/remarkjs/remark/blob/main/doc/plugins.md).
22
+ ///
23
+ /// Additional property:
24
+ /// - `id`: The id of the remark plugin.
25
+ /// - `plugin`: The remark plugin created.
26
+ /// - `options`: The ctx contains the options of the remark plugin.
27
+ export function $remark<Id extends string, Options>(
28
+ id: Id,
29
+ remark: (ctx: Ctx) => RemarkPluginRaw<Options>,
30
+ initialOptions?: Options
31
+ ): $Remark<Id, Options> {
32
+ const options = $ctx<Options, Id>(initialOptions ?? ({} as Options), id)
33
+ const plugin: MilkdownPlugin = (ctx) => async () => {
34
+ await ctx.wait(InitReady)
35
+ const re = remark(ctx)
36
+ const remarkPlugin: RemarkPlugin<Options> = {
37
+ plugin: re,
38
+ options: ctx.get(options.key),
39
+ }
40
+ ctx.update(remarkPluginsCtx, (rp) => [...rp, remarkPlugin as RemarkPlugin])
41
+
42
+ return () => {
43
+ ctx.update(remarkPluginsCtx, (rp) => rp.filter((x) => x !== remarkPlugin))
44
+ }
45
+ }
46
+
47
+ const result = [options, plugin] as $Remark<Id, Options>
48
+ result.id = id
49
+ result.plugin = plugin
50
+ result.options = options
51
+
52
+ return result
53
+ }
@@ -0,0 +1,91 @@
1
+ import type { Ctx, SliceType } from '@jvs-milkdown/ctx'
2
+ import type { Command } from '@jvs-milkdown/prose/state'
3
+
4
+ import type { $Ctx } from '../$ctx'
5
+ import type { $Shortcut, Keymap } from '../$shortcut'
6
+
7
+ import { $ctx } from '../$ctx'
8
+ import { $shortcut } from '../$shortcut'
9
+
10
+ /// @internal
11
+ export type KeymapConfig<K extends string> = Record<
12
+ K,
13
+ {
14
+ shortcuts: string | string[]
15
+ priority?: number
16
+ }
17
+ >
18
+
19
+ /// @internal
20
+ export interface KeymapItem {
21
+ shortcuts: string | string[]
22
+ priority?: number
23
+ command: (ctx: Ctx) => Command
24
+ }
25
+
26
+ /// @internal
27
+ export type UserKeymapConfig<Key extends string> = Record<Key, KeymapItem>
28
+
29
+ /// @internal
30
+ export type $UserKeymap<N extends string, Key extends string> = [
31
+ $Ctx<KeymapConfig<Key>, `${N}Keymap`>,
32
+ $Shortcut,
33
+ ] & {
34
+ key: SliceType<KeymapConfig<Key>, `${N}Keymap`>
35
+ keymap: Keymap
36
+ ctx: $Ctx<KeymapConfig<Key>, `${N}Keymap`>
37
+ shortcuts: $Shortcut
38
+ }
39
+
40
+ /// Create a keymap which can be customized by user.
41
+ /// It takes two arguments:
42
+ /// - `name`: The name of the keymap.
43
+ /// - `userKeymap`: The keymap config which contains the shortcuts and the command.
44
+ export function $useKeymap<N extends string, Key extends string>(
45
+ name: N,
46
+ userKeymap: UserKeymapConfig<Key>
47
+ ) {
48
+ const key = Object.fromEntries(
49
+ Object.entries<KeymapItem>(userKeymap).map(
50
+ ([key, { shortcuts, priority }]) => {
51
+ return [key, { shortcuts, priority }]
52
+ }
53
+ )
54
+ ) as KeymapConfig<Key>
55
+
56
+ const keymapDef = $ctx<KeymapConfig<Key>, `${N}Keymap`>(key, `${name}Keymap`)
57
+
58
+ const shortcuts = $shortcut((ctx) => {
59
+ const keys = ctx.get(keymapDef.key)
60
+
61
+ const keymapTuple = Object.entries<KeymapItem>(userKeymap).flatMap(
62
+ ([key, { command }]) => {
63
+ const target = keys[key as Key]
64
+ const targetKeys = [target.shortcuts].flat()
65
+ const priority = target.priority
66
+
67
+ return targetKeys.map(
68
+ (targetKey) =>
69
+ [
70
+ targetKey,
71
+ {
72
+ key: targetKey,
73
+ onRun: command,
74
+ priority,
75
+ },
76
+ ] as const
77
+ )
78
+ }
79
+ )
80
+
81
+ return Object.fromEntries(keymapTuple)
82
+ })
83
+
84
+ const result = [keymapDef, shortcuts] as $UserKeymap<N, Key>
85
+ result.ctx = keymapDef
86
+ result.shortcuts = shortcuts
87
+ result.key = keymapDef.key
88
+ result.keymap = shortcuts.keymap
89
+
90
+ return result
91
+ }
@@ -0,0 +1,5 @@
1
+ export * from './$node-schema'
2
+ export * from './$mark-schema'
3
+ export * from './$use-keymap'
4
+ export * from './$attr'
5
+ export * from './$remark'
@@ -0,0 +1,12 @@
1
+ export * from './$command'
2
+ export * from './$input-rule'
3
+ export * from './$paste-rule'
4
+ export * from './$mark'
5
+ export * from './$node'
6
+ export * from './$prose'
7
+ export * from './$shortcut'
8
+ export * from './$view'
9
+ export * from './$ctx'
10
+
11
+ export * from './composed'
12
+ export * from './utils'
@@ -0,0 +1,63 @@
1
+ import type {
2
+ Cleanup,
3
+ Ctx,
4
+ MilkdownPlugin,
5
+ SliceType,
6
+ TimerType,
7
+ } from '@jvs-milkdown/ctx'
8
+
9
+ import { createTimer } from '@jvs-milkdown/ctx'
10
+ import { customAlphabet } from 'nanoid'
11
+
12
+ /// @internal
13
+ export const nanoid = customAlphabet('abcedfghicklmn', 10)
14
+
15
+ /// @internal
16
+ export type WithTimer<T> = T & { timer: TimerType }
17
+
18
+ /// @internal
19
+ export function addTimer<
20
+ T extends MilkdownPlugin,
21
+ PluginWithTimer extends T = WithTimer<T>,
22
+ >(
23
+ runner: (
24
+ ctx: Ctx,
25
+ plugin: PluginWithTimer,
26
+ done: () => void
27
+ ) => Promise<void | Cleanup>,
28
+ injectTo: SliceType<TimerType[], string>,
29
+ timerName?: string
30
+ ): PluginWithTimer {
31
+ const timer = createTimer(timerName || nanoid())
32
+ let doneCalled = false
33
+
34
+ const plugin: MilkdownPlugin = (ctx) => {
35
+ ctx.record(timer)
36
+ ctx.update(injectTo, (x) => x.concat(timer))
37
+
38
+ return async () => {
39
+ const done = () => {
40
+ ctx.done(timer)
41
+ doneCalled = true
42
+ }
43
+
44
+ const cleanup = await runner(ctx, <PluginWithTimer>plugin, done)
45
+
46
+ if (!doneCalled) ctx.done(timer)
47
+
48
+ return () => {
49
+ ctx.update(injectTo, (x) => x.filter((y) => y !== timer))
50
+ ctx.clearTimer(timer)
51
+ if (cleanup) {
52
+ const result = cleanup()
53
+ if (result && 'then' in result) {
54
+ result.catch(console.error)
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ;(<T & { timer: TimerType }>plugin).timer = timer
61
+
62
+ return <PluginWithTimer>plugin
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './composable'
2
+ export * from './macro'
3
+ export * from './pipe'
@@ -0,0 +1,28 @@
1
+ import type { CmdKey } from '@jvs-milkdown/core'
2
+ import type { Ctx } from '@jvs-milkdown/ctx'
3
+
4
+ import { commandsCtx } from '@jvs-milkdown/core'
5
+
6
+ type InferParams<T> = T extends CmdKey<infer U> ? U : never
7
+
8
+ /// Call a command. You can pass the command key and the payload to the macro.
9
+ export function callCommand<T extends CmdKey<any>>(
10
+ slice: string,
11
+ payload?: InferParams<T>
12
+ ): (ctx: Ctx) => boolean
13
+ export function callCommand<T>(
14
+ slice: CmdKey<T>,
15
+ payload?: T
16
+ ): (ctx: Ctx) => boolean
17
+ export function callCommand(
18
+ slice: string | CmdKey<any>,
19
+ payload?: any
20
+ ): (ctx: Ctx) => boolean
21
+ export function callCommand(
22
+ slice: string | CmdKey<any>,
23
+ payload?: any
24
+ ): (ctx: Ctx) => boolean {
25
+ return (ctx: Ctx) => {
26
+ return ctx.get(commandsCtx).call(slice, payload)
27
+ }
28
+ }
@@ -0,0 +1,14 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { editorViewCtx } from '@jvs-milkdown/core'
4
+
5
+ /// Force update the editor.
6
+ export function forceUpdate() {
7
+ return (ctx: Ctx): void => {
8
+ const view = ctx.get(editorViewCtx)
9
+ const { tr } = view.state
10
+
11
+ const nextTr = Object.assign(Object.create(tr), tr).setTime(Date.now())
12
+ return view.dispatch(nextTr)
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { editorViewCtx, schemaCtx } from '@jvs-milkdown/core'
4
+ import { DOMSerializer } from '@jvs-milkdown/prose/model'
5
+
6
+ /// Get content of the editor as HTML string.
7
+ export function getHTML() {
8
+ return (ctx: Ctx): string => {
9
+ const div = document.createElement('div')
10
+ const schema = ctx.get(schemaCtx)
11
+ const view = ctx.get(editorViewCtx)
12
+ const fragment = DOMSerializer.fromSchema(schema).serializeFragment(
13
+ view.state.doc.content
14
+ )
15
+
16
+ div.appendChild(fragment)
17
+
18
+ return div.innerHTML
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { editorViewCtx, schemaCtx, serializerCtx } from '@jvs-milkdown/core'
4
+
5
+ /// Get content of the editor as markdown string.
6
+ /// If range is provided, it will return the markdown string of the range.
7
+ /// If range is not provided, it will return the markdown string of the whole document.
8
+ export function getMarkdown(range?: { from: number; to: number }) {
9
+ return (ctx: Ctx): string => {
10
+ const view = ctx.get(editorViewCtx)
11
+ const schema = ctx.get(schemaCtx)
12
+ const serializer = ctx.get(serializerCtx)
13
+
14
+ if (!range) {
15
+ return serializer(view.state.doc)
16
+ }
17
+
18
+ const state = view.state
19
+ const slice = state.doc.slice(range.from, range.to, true)
20
+ const doc = schema.topNodeType.createAndFill(null, slice.content)
21
+ if (!doc) {
22
+ console.error('No document found')
23
+ return ''
24
+ }
25
+ return serializer(doc)
26
+ }
27
+ }
@@ -0,0 +1,11 @@
1
+ export * from './call-command'
2
+ export * from './force-update'
3
+ export * from './get-html'
4
+ export * from './get-markdown'
5
+ export * from './insert'
6
+ export * from './outline'
7
+ export * from './replace-all'
8
+ export * from './set-attr'
9
+ export * from './insert-pos'
10
+ export * from './replace-range'
11
+ export * from './markdown-to-slice'
@@ -0,0 +1,27 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { editorViewCtx } from '@jvs-milkdown/core'
4
+
5
+ import { markdownToSlice } from './markdown-to-slice'
6
+
7
+ /// Insert markdown string to the given position.
8
+ /// If inline is true, the markdown will be inserted as inline text.
9
+ /// If inline is false, the markdown will be inserted as block text.
10
+ export function insertPos(
11
+ markdown: string,
12
+ pos: number,
13
+ inline: boolean = false
14
+ ) {
15
+ return (ctx: Ctx) => {
16
+ const slice = markdownToSlice(markdown)(ctx)
17
+ const view = ctx.get(editorViewCtx)
18
+ const toPos = view.state.doc.resolve(pos)
19
+
20
+ const min = 0
21
+ const max = view.state.doc.content.size
22
+ const resolved = inline ? toPos.pos : toPos.after(toPos.depth - 1)
23
+ const to = Math.min(Math.max(resolved, min), max)
24
+
25
+ view.dispatch(view.state.tr.replace(resolved, to, slice))
26
+ }
27
+ }
@@ -0,0 +1,38 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { editorViewCtx, parserCtx, schemaCtx } from '@jvs-milkdown/core'
4
+ import { isTextOnlySlice } from '@jvs-milkdown/prose'
5
+ import { DOMParser, DOMSerializer, Slice } from '@jvs-milkdown/prose/model'
6
+
7
+ /// Insert markdown string into the editor.
8
+ export function insert(markdown: string, inline: boolean = false) {
9
+ return (ctx: Ctx) => {
10
+ const view = ctx.get(editorViewCtx)
11
+ const parser = ctx.get(parserCtx)
12
+ const doc = parser(markdown)
13
+ if (!doc) return
14
+
15
+ if (!inline) {
16
+ const contentSlice = view.state.selection.content()
17
+ return view.dispatch(
18
+ view.state.tr
19
+ .replaceSelection(
20
+ new Slice(doc.content, contentSlice.openStart, contentSlice.openEnd)
21
+ )
22
+ .scrollIntoView()
23
+ )
24
+ }
25
+
26
+ const schema = ctx.get(schemaCtx)
27
+ const dom = DOMSerializer.fromSchema(schema).serializeFragment(doc.content)
28
+ const domParser = DOMParser.fromSchema(schema)
29
+ const slice = domParser.parseSlice(dom)
30
+ const node = isTextOnlySlice(slice)
31
+ if (node) {
32
+ view.dispatch(view.state.tr.replaceSelectionWith(node, true))
33
+ return
34
+ }
35
+
36
+ view.dispatch(view.state.tr.replaceSelection(slice))
37
+ }
38
+ }
@@ -0,0 +1,18 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { parserCtx, schemaCtx } from '@jvs-milkdown/core'
4
+ import { DOMSerializer, DOMParser } from '@jvs-milkdown/prose/model'
5
+
6
+ /// Convert markdown string to slice.
7
+ export function markdownToSlice(markdown: string) {
8
+ return (ctx: Ctx) => {
9
+ const parser = ctx.get(parserCtx)
10
+ const doc = parser(markdown)
11
+ const schema = ctx.get(schemaCtx)
12
+ const dom = DOMSerializer.fromSchema(schema).serializeFragment(doc.content)
13
+ const domParser = DOMParser.fromSchema(schema)
14
+ const slice = domParser.parseSlice(dom)
15
+
16
+ return slice
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ import type { Ctx } from '@jvs-milkdown/ctx'
2
+
3
+ import { editorViewCtx } from '@jvs-milkdown/core'
4
+
5
+ /// Get outline of the editor.
6
+ export function outline() {
7
+ return (ctx: Ctx): Array<{ text: string; level: number; id: string }> => {
8
+ const view = ctx.get(editorViewCtx)
9
+ const data: { text: string; level: number; id: string }[] = []
10
+ const doc = view.state.doc
11
+ doc.descendants((node) => {
12
+ if (node.type.name === 'heading' && node.attrs.level)
13
+ data.push({
14
+ text: node.textContent,
15
+ level: node.attrs.level,
16
+ id: node.attrs.id,
17
+ })
18
+ })
19
+ return data
20
+ }
21
+ }