@jvs-milkdown/preset-commonmark 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__/serialize-text.d.ts +4 -0
- package/lib/__internal__/serialize-text.d.ts.map +1 -0
- package/lib/__internal__/with-meta.d.ts +3 -0
- package/lib/__internal__/with-meta.d.ts.map +1 -0
- package/lib/__test__/html.spec.d.ts +2 -0
- package/lib/__test__/html.spec.d.ts.map +1 -0
- package/lib/__test__/trailing-space.spec.d.ts +2 -0
- package/lib/__test__/trailing-space.spec.d.ts.map +1 -0
- package/lib/__test__/vitest.setup.d.ts +1 -0
- package/lib/__test__/vitest.setup.d.ts.map +1 -0
- package/lib/commands/index.d.ts +20 -0
- package/lib/commands/index.d.ts.map +1 -0
- package/lib/composed/commands.d.ts +3 -0
- package/lib/composed/commands.d.ts.map +1 -0
- package/lib/composed/index.d.ts +6 -0
- package/lib/composed/index.d.ts.map +1 -0
- package/lib/composed/inputrules.d.ts +4 -0
- package/lib/composed/inputrules.d.ts.map +1 -0
- package/lib/composed/keymap.d.ts +3 -0
- package/lib/composed/keymap.d.ts.map +1 -0
- package/lib/composed/plugins.d.ts +3 -0
- package/lib/composed/plugins.d.ts.map +1 -0
- package/lib/composed/schema.d.ts +3 -0
- package/lib/composed/schema.d.ts.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2153 -0
- package/lib/index.js.map +1 -0
- package/lib/mark/emphasis.d.ts +7 -0
- package/lib/mark/emphasis.d.ts.map +1 -0
- package/lib/mark/index.d.ts +5 -0
- package/lib/mark/index.d.ts.map +1 -0
- package/lib/mark/inline-code.d.ts +6 -0
- package/lib/mark/inline-code.d.ts.map +1 -0
- package/lib/mark/link.d.ts +9 -0
- package/lib/mark/link.d.ts.map +1 -0
- package/lib/mark/strong.d.ts +6 -0
- package/lib/mark/strong.d.ts.map +1 -0
- package/lib/node/blockquote.d.ts +7 -0
- package/lib/node/blockquote.d.ts.map +1 -0
- package/lib/node/bullet-list.d.ts +6 -0
- package/lib/node/bullet-list.d.ts.map +1 -0
- package/lib/node/code-block.d.ts +10 -0
- package/lib/node/code-block.d.ts.map +1 -0
- package/lib/node/doc.d.ts +2 -0
- package/lib/node/doc.d.ts.map +1 -0
- package/lib/node/hardbreak.d.ts +5 -0
- package/lib/node/hardbreak.d.ts.map +1 -0
- package/lib/node/heading.d.ts +11 -0
- package/lib/node/heading.d.ts.map +1 -0
- package/lib/node/hr.d.ts +5 -0
- package/lib/node/hr.d.ts.map +1 -0
- package/lib/node/html.d.ts +3 -0
- package/lib/node/html.d.ts.map +1 -0
- package/lib/node/image.d.ts +11 -0
- package/lib/node/image.d.ts.map +1 -0
- package/lib/node/index.d.ts +14 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/list-item.d.ts +8 -0
- package/lib/node/list-item.d.ts.map +1 -0
- package/lib/node/ordered-list.d.ts +6 -0
- package/lib/node/ordered-list.d.ts.map +1 -0
- package/lib/node/paragraph.d.ts +5 -0
- package/lib/node/paragraph.d.ts.map +1 -0
- package/lib/node/text.d.ts +2 -0
- package/lib/node/text.d.ts.map +1 -0
- package/lib/plugin/hardbreak-clear-mark-plugin.d.ts +2 -0
- package/lib/plugin/hardbreak-clear-mark-plugin.d.ts.map +1 -0
- package/lib/plugin/hardbreak-filter-plugin.d.ts +3 -0
- package/lib/plugin/hardbreak-filter-plugin.d.ts.map +1 -0
- package/lib/plugin/index.d.ts +12 -0
- package/lib/plugin/index.d.ts.map +1 -0
- package/lib/plugin/inline-nodes-cursor-plugin.d.ts +2 -0
- package/lib/plugin/inline-nodes-cursor-plugin.d.ts.map +1 -0
- package/lib/plugin/remark-add-order-in-list-plugin.d.ts +2 -0
- package/lib/plugin/remark-add-order-in-list-plugin.d.ts.map +1 -0
- package/lib/plugin/remark-html-transformer.d.ts +2 -0
- package/lib/plugin/remark-html-transformer.d.ts.map +1 -0
- package/lib/plugin/remark-inline-link-plugin.d.ts +2 -0
- package/lib/plugin/remark-inline-link-plugin.d.ts.map +1 -0
- package/lib/plugin/remark-line-break.d.ts +2 -0
- package/lib/plugin/remark-line-break.d.ts.map +1 -0
- package/lib/plugin/remark-marker-plugin.d.ts +2 -0
- package/lib/plugin/remark-marker-plugin.d.ts.map +1 -0
- package/lib/plugin/remark-preserve-empty-line.d.ts +2 -0
- package/lib/plugin/remark-preserve-empty-line.d.ts.map +1 -0
- package/lib/plugin/sync-heading-id-plugin.d.ts +2 -0
- package/lib/plugin/sync-heading-id-plugin.d.ts.map +1 -0
- package/lib/plugin/sync-list-order-plugin.d.ts +2 -0
- package/lib/plugin/sync-list-order-plugin.d.ts.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +44 -0
- package/src/__internal__/index.ts +2 -0
- package/src/__internal__/serialize-text.ts +21 -0
- package/src/__internal__/with-meta.ts +15 -0
- package/src/__test__/html.spec.ts +46 -0
- package/src/__test__/trailing-space.spec.ts +27 -0
- package/src/__test__/vitest.setup.ts +65 -0
- package/src/commands/index.ts +140 -0
- package/src/composed/commands.ts +72 -0
- package/src/composed/index.ts +5 -0
- package/src/composed/inputrules.ts +34 -0
- package/src/composed/keymap.ts +29 -0
- package/src/composed/plugins.ts +35 -0
- package/src/composed/schema.ts +92 -0
- package/src/index.ts +26 -0
- package/src/mark/emphasis.ts +130 -0
- package/src/mark/index.ts +4 -0
- package/src/mark/inline-code.ts +123 -0
- package/src/mark/link.ts +134 -0
- package/src/mark/strong.ts +130 -0
- package/src/node/blockquote.ts +100 -0
- package/src/node/bullet-list.ts +129 -0
- package/src/node/code-block.ts +176 -0
- package/src/node/doc.ts +26 -0
- package/src/node/hardbreak.ts +134 -0
- package/src/node/heading.ts +271 -0
- package/src/node/hr.ts +87 -0
- package/src/node/html.ts +66 -0
- package/src/node/image.ts +173 -0
- package/src/node/index.ts +14 -0
- package/src/node/list-item.ts +244 -0
- package/src/node/ordered-list.ts +141 -0
- package/src/node/paragraph.ts +136 -0
- package/src/node/text.ts +25 -0
- package/src/plugin/hardbreak-clear-mark-plugin.ts +58 -0
- package/src/plugin/hardbreak-filter-plugin.ts +46 -0
- package/src/plugin/index.ts +14 -0
- package/src/plugin/inline-nodes-cursor-plugin.ts +103 -0
- package/src/plugin/remark-add-order-in-list-plugin.ts +29 -0
- package/src/plugin/remark-html-transformer.ts +74 -0
- package/src/plugin/remark-inline-link-plugin.ts +20 -0
- package/src/plugin/remark-line-break.ts +69 -0
- package/src/plugin/remark-marker-plugin.ts +33 -0
- package/src/plugin/remark-preserve-empty-line.ts +49 -0
- package/src/plugin/sync-heading-id-plugin.ts +67 -0
- package/src/plugin/sync-list-order-plugin.ts +112 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { expectDomTypeError } from '@jvs-milkdown/exception'
|
|
2
|
+
import { findSelectedNodeOfType } from '@jvs-milkdown/prose'
|
|
3
|
+
import { InputRule } from '@jvs-milkdown/prose/inputrules'
|
|
4
|
+
import { $command, $inputRule, $nodeAttr, $nodeSchema } from '@jvs-milkdown/utils'
|
|
5
|
+
|
|
6
|
+
import { withMeta } from '../__internal__'
|
|
7
|
+
|
|
8
|
+
/// HTML attributes for image node.
|
|
9
|
+
export const imageAttr = $nodeAttr('image')
|
|
10
|
+
|
|
11
|
+
withMeta(imageAttr, {
|
|
12
|
+
displayName: 'Attr<image>',
|
|
13
|
+
group: 'Image',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/// Schema for image node.
|
|
17
|
+
export const imageSchema = $nodeSchema('image', (ctx) => {
|
|
18
|
+
return {
|
|
19
|
+
inline: true,
|
|
20
|
+
group: 'inline',
|
|
21
|
+
selectable: true,
|
|
22
|
+
draggable: true,
|
|
23
|
+
marks: '',
|
|
24
|
+
atom: true,
|
|
25
|
+
defining: true,
|
|
26
|
+
isolating: true,
|
|
27
|
+
attrs: {
|
|
28
|
+
src: { default: '', validate: 'string' },
|
|
29
|
+
alt: { default: '', validate: 'string' },
|
|
30
|
+
title: { default: '', validate: 'string' },
|
|
31
|
+
},
|
|
32
|
+
parseDOM: [
|
|
33
|
+
{
|
|
34
|
+
tag: 'img[src]',
|
|
35
|
+
getAttrs: (dom) => {
|
|
36
|
+
if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
src: dom.getAttribute('src') || '',
|
|
40
|
+
alt: dom.getAttribute('alt') || '',
|
|
41
|
+
title: dom.getAttribute('title') || dom.getAttribute('alt') || '',
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
toDOM: (node) => {
|
|
47
|
+
return ['img', { ...ctx.get(imageAttr.key)(node), ...node.attrs }]
|
|
48
|
+
},
|
|
49
|
+
parseMarkdown: {
|
|
50
|
+
match: ({ type }) => type === 'image',
|
|
51
|
+
runner: (state, node, type) => {
|
|
52
|
+
const url = node.url as string
|
|
53
|
+
const alt = node.alt as string
|
|
54
|
+
const title = node.title as string
|
|
55
|
+
state.addNode(type, {
|
|
56
|
+
src: url,
|
|
57
|
+
alt,
|
|
58
|
+
title,
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
toMarkdown: {
|
|
63
|
+
match: (node) => node.type.name === 'image',
|
|
64
|
+
runner: (state, node) => {
|
|
65
|
+
state.addNode('image', undefined, undefined, {
|
|
66
|
+
title: node.attrs.title,
|
|
67
|
+
url: node.attrs.src,
|
|
68
|
+
alt: node.attrs.alt,
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
withMeta(imageSchema.node, {
|
|
76
|
+
displayName: 'NodeSchema<image>',
|
|
77
|
+
group: 'Image',
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
withMeta(imageSchema.ctx, {
|
|
81
|
+
displayName: 'NodeSchemaCtx<image>',
|
|
82
|
+
group: 'Image',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/// @internal
|
|
86
|
+
export interface UpdateImageCommandPayload {
|
|
87
|
+
src?: string
|
|
88
|
+
title?: string
|
|
89
|
+
alt?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// This command will insert a image node.
|
|
93
|
+
/// You can pass a payload to set `src`, `alt` and `title` for the image node.
|
|
94
|
+
export const insertImageCommand = $command(
|
|
95
|
+
'InsertImage',
|
|
96
|
+
(ctx) =>
|
|
97
|
+
(payload: UpdateImageCommandPayload = {}) =>
|
|
98
|
+
(state, dispatch) => {
|
|
99
|
+
if (!dispatch) return true
|
|
100
|
+
|
|
101
|
+
const { src = '', alt = '', title = '' } = payload
|
|
102
|
+
|
|
103
|
+
const node = imageSchema.type(ctx).create({ src, alt, title })
|
|
104
|
+
if (!node) return true
|
|
105
|
+
|
|
106
|
+
dispatch(state.tr.replaceSelectionWith(node).scrollIntoView())
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
withMeta(insertImageCommand, {
|
|
112
|
+
displayName: 'Command<insertImageCommand>',
|
|
113
|
+
group: 'Image',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/// This command will update the selected image node.
|
|
117
|
+
/// You can pass a payload to update `src`, `alt` and `title` for the image node.
|
|
118
|
+
export const updateImageCommand = $command(
|
|
119
|
+
'UpdateImage',
|
|
120
|
+
(ctx) =>
|
|
121
|
+
(payload: UpdateImageCommandPayload = {}) =>
|
|
122
|
+
(state, dispatch) => {
|
|
123
|
+
const nodeWithPos = findSelectedNodeOfType(
|
|
124
|
+
state.selection,
|
|
125
|
+
imageSchema.type(ctx)
|
|
126
|
+
)
|
|
127
|
+
if (!nodeWithPos) return false
|
|
128
|
+
|
|
129
|
+
const { node, pos } = nodeWithPos
|
|
130
|
+
|
|
131
|
+
const newAttrs = { ...node.attrs }
|
|
132
|
+
const { src, alt, title } = payload
|
|
133
|
+
if (src !== undefined) newAttrs.src = src
|
|
134
|
+
if (alt !== undefined) newAttrs.alt = alt
|
|
135
|
+
if (title !== undefined) newAttrs.title = title
|
|
136
|
+
|
|
137
|
+
dispatch?.(
|
|
138
|
+
state.tr.setNodeMarkup(pos, undefined, newAttrs).scrollIntoView()
|
|
139
|
+
)
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
withMeta(updateImageCommand, {
|
|
145
|
+
displayName: 'Command<updateImageCommand>',
|
|
146
|
+
group: 'Image',
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
/// This input rule will insert a image node.
|
|
150
|
+
/// You can input `` to insert a image node.
|
|
151
|
+
/// The `title` is optional.
|
|
152
|
+
export const insertImageInputRule = $inputRule(
|
|
153
|
+
(ctx) =>
|
|
154
|
+
new InputRule(
|
|
155
|
+
/!\[(?<alt>.*?)]\((?<filename>.*?)\s*(?="|\))"?(?<title>[^"]+)?"?\)/,
|
|
156
|
+
(state, match, start, end) => {
|
|
157
|
+
const [matched, alt, src = '', title] = match
|
|
158
|
+
if (matched)
|
|
159
|
+
return state.tr.replaceWith(
|
|
160
|
+
start,
|
|
161
|
+
end,
|
|
162
|
+
imageSchema.type(ctx).create({ src, alt, title })
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
withMeta(insertImageInputRule, {
|
|
171
|
+
displayName: 'InputRule<insertImageInputRule>',
|
|
172
|
+
group: 'Image',
|
|
173
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './doc'
|
|
2
|
+
export * from './heading'
|
|
3
|
+
export * from './blockquote'
|
|
4
|
+
export * from './code-block'
|
|
5
|
+
export * from './image'
|
|
6
|
+
export * from './hardbreak'
|
|
7
|
+
export * from './hr'
|
|
8
|
+
export * from './bullet-list'
|
|
9
|
+
export * from './ordered-list'
|
|
10
|
+
export * from './list-item'
|
|
11
|
+
export * from './paragraph'
|
|
12
|
+
export * from './text'
|
|
13
|
+
|
|
14
|
+
export * from './html'
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { Ctx } from '@jvs-milkdown/ctx'
|
|
2
|
+
|
|
3
|
+
import { commandsCtx } from '@jvs-milkdown/core'
|
|
4
|
+
import { expectDomTypeError } from '@jvs-milkdown/exception'
|
|
5
|
+
import { joinBackward } from '@jvs-milkdown/prose/commands'
|
|
6
|
+
import {
|
|
7
|
+
liftListItem,
|
|
8
|
+
sinkListItem,
|
|
9
|
+
splitListItem,
|
|
10
|
+
} from '@jvs-milkdown/prose/schema-list'
|
|
11
|
+
import { type Command, TextSelection } from '@jvs-milkdown/prose/state'
|
|
12
|
+
import { $command, $nodeAttr, $nodeSchema, $useKeymap } from '@jvs-milkdown/utils'
|
|
13
|
+
|
|
14
|
+
import { withMeta } from '../__internal__'
|
|
15
|
+
|
|
16
|
+
/// HTML attributes for list item node.
|
|
17
|
+
export const listItemAttr = $nodeAttr('listItem')
|
|
18
|
+
|
|
19
|
+
withMeta(listItemAttr, {
|
|
20
|
+
displayName: 'Attr<listItem>',
|
|
21
|
+
group: 'ListItem',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/// Schema for list item node.
|
|
25
|
+
export const listItemSchema = $nodeSchema('list_item', (ctx) => ({
|
|
26
|
+
group: 'listItem',
|
|
27
|
+
content: 'paragraph block*',
|
|
28
|
+
attrs: {
|
|
29
|
+
label: {
|
|
30
|
+
default: '•',
|
|
31
|
+
validate: 'string',
|
|
32
|
+
},
|
|
33
|
+
listType: {
|
|
34
|
+
default: 'bullet',
|
|
35
|
+
validate: 'string',
|
|
36
|
+
},
|
|
37
|
+
spread: {
|
|
38
|
+
default: true,
|
|
39
|
+
validate: 'boolean',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defining: true,
|
|
43
|
+
parseDOM: [
|
|
44
|
+
{
|
|
45
|
+
tag: 'li',
|
|
46
|
+
getAttrs: (dom) => {
|
|
47
|
+
if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
label: dom.dataset.label,
|
|
51
|
+
listType: dom.dataset.listType,
|
|
52
|
+
spread: dom.dataset.spread === 'true',
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
toDOM: (node) => [
|
|
58
|
+
'li',
|
|
59
|
+
{
|
|
60
|
+
...ctx.get(listItemAttr.key)(node),
|
|
61
|
+
'data-label': node.attrs.label,
|
|
62
|
+
'data-list-type': node.attrs.listType,
|
|
63
|
+
'data-spread': node.attrs.spread,
|
|
64
|
+
},
|
|
65
|
+
0,
|
|
66
|
+
],
|
|
67
|
+
parseMarkdown: {
|
|
68
|
+
match: ({ type }) => type === 'listItem',
|
|
69
|
+
runner: (state, node, type) => {
|
|
70
|
+
const label = node.label != null ? `${node.label}.` : '•'
|
|
71
|
+
const listType = node.label != null ? 'ordered' : 'bullet'
|
|
72
|
+
const spread = node.spread != null ? `${node.spread}` : 'true'
|
|
73
|
+
state.openNode(type, { label, listType, spread })
|
|
74
|
+
state.next(node.children)
|
|
75
|
+
state.closeNode()
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
toMarkdown: {
|
|
79
|
+
match: (node) => node.type.name === 'list_item',
|
|
80
|
+
runner: (state, node) => {
|
|
81
|
+
state.openNode('listItem', undefined, {
|
|
82
|
+
spread: node.attrs.spread,
|
|
83
|
+
})
|
|
84
|
+
state.next(node.content)
|
|
85
|
+
state.closeNode()
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
withMeta(listItemSchema.node, {
|
|
91
|
+
displayName: 'NodeSchema<listItem>',
|
|
92
|
+
group: 'ListItem',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
withMeta(listItemSchema.ctx, {
|
|
96
|
+
displayName: 'NodeSchemaCtx<listItem>',
|
|
97
|
+
group: 'ListItem',
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
/// The command to sink list item.
|
|
101
|
+
///
|
|
102
|
+
/// For example:
|
|
103
|
+
/// ```md
|
|
104
|
+
/// * List item 1
|
|
105
|
+
/// * List item 2 <- cursor here
|
|
106
|
+
/// ```
|
|
107
|
+
/// Will get:
|
|
108
|
+
/// ```md
|
|
109
|
+
/// * List item 1
|
|
110
|
+
/// * List item 2
|
|
111
|
+
/// ```
|
|
112
|
+
export const sinkListItemCommand = $command(
|
|
113
|
+
'SinkListItem',
|
|
114
|
+
(ctx) => () => sinkListItem(listItemSchema.type(ctx))
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
withMeta(sinkListItemCommand, {
|
|
118
|
+
displayName: 'Command<sinkListItemCommand>',
|
|
119
|
+
group: 'ListItem',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
/// The command to lift list item.
|
|
123
|
+
///
|
|
124
|
+
/// For example:
|
|
125
|
+
/// ```md
|
|
126
|
+
/// * List item 1
|
|
127
|
+
/// * List item 2 <- cursor here
|
|
128
|
+
/// ```
|
|
129
|
+
/// Will get:
|
|
130
|
+
/// ```md
|
|
131
|
+
/// * List item 1
|
|
132
|
+
/// * List item 2
|
|
133
|
+
/// ```
|
|
134
|
+
export const liftListItemCommand = $command(
|
|
135
|
+
'LiftListItem',
|
|
136
|
+
(ctx) => () => liftListItem(listItemSchema.type(ctx))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
withMeta(liftListItemCommand, {
|
|
140
|
+
displayName: 'Command<liftListItemCommand>',
|
|
141
|
+
group: 'ListItem',
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
/// The command to split a list item.
|
|
145
|
+
///
|
|
146
|
+
/// For example:
|
|
147
|
+
/// ```md
|
|
148
|
+
/// * List item 1
|
|
149
|
+
/// * List item 2 <- cursor here
|
|
150
|
+
/// ```
|
|
151
|
+
/// Will get:
|
|
152
|
+
/// ```md
|
|
153
|
+
/// * List item 1
|
|
154
|
+
/// * List item 2
|
|
155
|
+
/// * <- cursor here
|
|
156
|
+
/// ```
|
|
157
|
+
export const splitListItemCommand = $command(
|
|
158
|
+
'SplitListItem',
|
|
159
|
+
(ctx) => () => splitListItem(listItemSchema.type(ctx))
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
withMeta(splitListItemCommand, {
|
|
163
|
+
displayName: 'Command<splitListItemCommand>',
|
|
164
|
+
group: 'ListItem',
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
function liftFirstListItem(ctx: Ctx): Command {
|
|
168
|
+
return (state, dispatch, view) => {
|
|
169
|
+
const { selection } = state
|
|
170
|
+
if (!(selection instanceof TextSelection)) return false
|
|
171
|
+
|
|
172
|
+
const { empty, $from } = selection
|
|
173
|
+
|
|
174
|
+
// selection should be empty and at the start of the node
|
|
175
|
+
if (!empty || $from.parentOffset !== 0) return false
|
|
176
|
+
|
|
177
|
+
const parentItem = $from.node(-1)
|
|
178
|
+
// selection should be in list item
|
|
179
|
+
if (parentItem.type !== listItemSchema.type(ctx)) return false
|
|
180
|
+
|
|
181
|
+
return joinBackward(state, dispatch, view)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// The command to remove list item **only if**:
|
|
186
|
+
///
|
|
187
|
+
/// - Selection is at the start of the list item.
|
|
188
|
+
/// - List item is the only child of the list.
|
|
189
|
+
///
|
|
190
|
+
/// Most of the time, you shouldn't use this command directly.
|
|
191
|
+
export const liftFirstListItemCommand = $command(
|
|
192
|
+
'LiftFirstListItem',
|
|
193
|
+
(ctx) => () => liftFirstListItem(ctx)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
withMeta(liftFirstListItemCommand, {
|
|
197
|
+
displayName: 'Command<liftFirstListItemCommand>',
|
|
198
|
+
group: 'ListItem',
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
/// Keymap for list item node.
|
|
202
|
+
/// - `<Enter>`: Split the current list item.
|
|
203
|
+
/// - `<Tab>/<Mod-]>`: Sink the current list item.
|
|
204
|
+
/// - `<Shift-Tab>/<Mod-[>`: Lift the current list item.
|
|
205
|
+
export const listItemKeymap = $useKeymap('listItemKeymap', {
|
|
206
|
+
NextListItem: {
|
|
207
|
+
shortcuts: 'Enter',
|
|
208
|
+
command: (ctx) => {
|
|
209
|
+
const commands = ctx.get(commandsCtx)
|
|
210
|
+
return () => commands.call(splitListItemCommand.key)
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
SinkListItem: {
|
|
214
|
+
shortcuts: ['Tab', 'Mod-]'],
|
|
215
|
+
command: (ctx) => {
|
|
216
|
+
const commands = ctx.get(commandsCtx)
|
|
217
|
+
return () => commands.call(sinkListItemCommand.key)
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
LiftListItem: {
|
|
221
|
+
shortcuts: ['Shift-Tab', 'Mod-['],
|
|
222
|
+
command: (ctx) => {
|
|
223
|
+
const commands = ctx.get(commandsCtx)
|
|
224
|
+
return () => commands.call(liftListItemCommand.key)
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
LiftFirstListItem: {
|
|
228
|
+
shortcuts: ['Backspace', 'Delete'],
|
|
229
|
+
command: (ctx) => {
|
|
230
|
+
const commands = ctx.get(commandsCtx)
|
|
231
|
+
return () => commands.call(liftFirstListItemCommand.key)
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
withMeta(listItemKeymap.ctx, {
|
|
237
|
+
displayName: 'KeymapCtx<listItem>',
|
|
238
|
+
group: 'ListItem',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
withMeta(listItemKeymap.shortcuts, {
|
|
242
|
+
displayName: 'Keymap<listItem>',
|
|
243
|
+
group: 'ListItem',
|
|
244
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { commandsCtx } from '@jvs-milkdown/core'
|
|
2
|
+
import { expectDomTypeError } from '@jvs-milkdown/exception'
|
|
3
|
+
import { wrapIn } from '@jvs-milkdown/prose/commands'
|
|
4
|
+
import { wrappingInputRule } from '@jvs-milkdown/prose/inputrules'
|
|
5
|
+
import {
|
|
6
|
+
$command,
|
|
7
|
+
$inputRule,
|
|
8
|
+
$nodeAttr,
|
|
9
|
+
$nodeSchema,
|
|
10
|
+
$useKeymap,
|
|
11
|
+
} from '@jvs-milkdown/utils'
|
|
12
|
+
|
|
13
|
+
import { withMeta } from '../__internal__'
|
|
14
|
+
|
|
15
|
+
/// HTML attributes for ordered list node.
|
|
16
|
+
export const orderedListAttr = $nodeAttr('orderedList')
|
|
17
|
+
|
|
18
|
+
withMeta(orderedListAttr, {
|
|
19
|
+
displayName: 'Attr<orderedList>',
|
|
20
|
+
group: 'OrderedList',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/// Schema for ordered list node.
|
|
24
|
+
export const orderedListSchema = $nodeSchema('ordered_list', (ctx) => ({
|
|
25
|
+
content: 'listItem+',
|
|
26
|
+
group: 'block',
|
|
27
|
+
attrs: {
|
|
28
|
+
order: {
|
|
29
|
+
default: 1,
|
|
30
|
+
validate: 'number',
|
|
31
|
+
},
|
|
32
|
+
spread: {
|
|
33
|
+
default: false,
|
|
34
|
+
validate: 'boolean',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
parseDOM: [
|
|
38
|
+
{
|
|
39
|
+
tag: 'ol',
|
|
40
|
+
getAttrs: (dom) => {
|
|
41
|
+
if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
spread: dom.dataset.spread,
|
|
45
|
+
order: dom.hasAttribute('start')
|
|
46
|
+
? Number(dom.getAttribute('start'))
|
|
47
|
+
: 1,
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
toDOM: (node) => [
|
|
53
|
+
'ol',
|
|
54
|
+
{
|
|
55
|
+
...ctx.get(orderedListAttr.key)(node),
|
|
56
|
+
...(node.attrs.order === 1 ? {} : { start: node.attrs.order }),
|
|
57
|
+
'data-spread': node.attrs.spread,
|
|
58
|
+
},
|
|
59
|
+
0,
|
|
60
|
+
],
|
|
61
|
+
parseMarkdown: {
|
|
62
|
+
match: ({ type, ordered }) => type === 'list' && !!ordered,
|
|
63
|
+
runner: (state, node, type) => {
|
|
64
|
+
const spread = node.spread != null ? `${node.spread}` : 'true'
|
|
65
|
+
state
|
|
66
|
+
.openNode(type, { spread, order: node.start ?? 1 })
|
|
67
|
+
.next(node.children)
|
|
68
|
+
.closeNode()
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
toMarkdown: {
|
|
72
|
+
match: (node) => node.type.name === 'ordered_list',
|
|
73
|
+
runner: (state, node) => {
|
|
74
|
+
state.openNode('list', undefined, {
|
|
75
|
+
ordered: true,
|
|
76
|
+
start: node.attrs.order ?? 1,
|
|
77
|
+
spread: node.attrs.spread === 'true',
|
|
78
|
+
})
|
|
79
|
+
state.next(node.content)
|
|
80
|
+
state.closeNode()
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
withMeta(orderedListSchema.node, {
|
|
86
|
+
displayName: 'NodeSchema<orderedList>',
|
|
87
|
+
group: 'OrderedList',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
withMeta(orderedListSchema.ctx, {
|
|
91
|
+
displayName: 'NodeSchemaCtx<orderedList>',
|
|
92
|
+
group: 'OrderedList',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/// Input rule for wrapping a block in ordered list node.
|
|
96
|
+
export const wrapInOrderedListInputRule = $inputRule((ctx) =>
|
|
97
|
+
wrappingInputRule(
|
|
98
|
+
/^\s*(\d+)\.\s$/,
|
|
99
|
+
orderedListSchema.type(ctx),
|
|
100
|
+
(match) => ({ order: Number(match[1]) }),
|
|
101
|
+
(match, node) => node.childCount + node.attrs.order === Number(match[1])
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
withMeta(wrapInOrderedListInputRule, {
|
|
106
|
+
displayName: 'InputRule<wrapInOrderedListInputRule>',
|
|
107
|
+
group: 'OrderedList',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/// Command for wrapping a block in ordered list node.
|
|
111
|
+
export const wrapInOrderedListCommand = $command(
|
|
112
|
+
'WrapInOrderedList',
|
|
113
|
+
(ctx) => () => wrapIn(orderedListSchema.type(ctx))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
withMeta(wrapInOrderedListCommand, {
|
|
117
|
+
displayName: 'Command<wrapInOrderedListCommand>',
|
|
118
|
+
group: 'OrderedList',
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/// Keymap for ordered list node.
|
|
122
|
+
/// - `Mod-Alt-7`: Wrap a block in ordered list.
|
|
123
|
+
export const orderedListKeymap = $useKeymap('orderedListKeymap', {
|
|
124
|
+
WrapInOrderedList: {
|
|
125
|
+
shortcuts: 'Mod-Alt-7',
|
|
126
|
+
command: (ctx) => {
|
|
127
|
+
const commands = ctx.get(commandsCtx)
|
|
128
|
+
return () => commands.call(wrapInOrderedListCommand.key)
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
withMeta(orderedListKeymap.ctx, {
|
|
134
|
+
displayName: 'KeymapCtx<orderedList>',
|
|
135
|
+
group: 'OrderedList',
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
withMeta(orderedListKeymap.shortcuts, {
|
|
139
|
+
displayName: 'Keymap<orderedList>',
|
|
140
|
+
group: 'OrderedList',
|
|
141
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Ctx } from '@jvs-milkdown/ctx'
|
|
2
|
+
|
|
3
|
+
import { commandsCtx, editorViewCtx } from '@jvs-milkdown/core'
|
|
4
|
+
import { setBlockType } from '@jvs-milkdown/prose/commands'
|
|
5
|
+
import { $command, $nodeAttr, $nodeSchema, $useKeymap } from '@jvs-milkdown/utils'
|
|
6
|
+
|
|
7
|
+
import { serializeText, withMeta } from '../__internal__'
|
|
8
|
+
import { remarkPreserveEmptyLinePlugin } from '../plugin/remark-preserve-empty-line'
|
|
9
|
+
|
|
10
|
+
/// HTML attributes for paragraph node.
|
|
11
|
+
export const paragraphAttr = $nodeAttr('paragraph')
|
|
12
|
+
|
|
13
|
+
withMeta(paragraphAttr, {
|
|
14
|
+
displayName: 'Attr<paragraph>',
|
|
15
|
+
group: 'Paragraph',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/// Schema for paragraph node.
|
|
19
|
+
export const paragraphSchema = $nodeSchema('paragraph', (ctx) => ({
|
|
20
|
+
content: 'inline*',
|
|
21
|
+
group: 'block',
|
|
22
|
+
attrs: {
|
|
23
|
+
align: { default: null },
|
|
24
|
+
indent: { default: 0 },
|
|
25
|
+
},
|
|
26
|
+
parseDOM: [
|
|
27
|
+
{
|
|
28
|
+
tag: 'p',
|
|
29
|
+
getAttrs: (dom) => {
|
|
30
|
+
if (!(dom instanceof HTMLElement)) return null
|
|
31
|
+
return {
|
|
32
|
+
align: dom.style.textAlign || dom.getAttribute('data-align') || null,
|
|
33
|
+
indent: parseInt(dom.getAttribute('data-indent') || '0', 10) || 0,
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
toDOM: (node) => {
|
|
39
|
+
const { align, indent } = node.attrs
|
|
40
|
+
const attrs = { ...ctx.get(paragraphAttr.key)(node) } as Record<
|
|
41
|
+
string,
|
|
42
|
+
string
|
|
43
|
+
>
|
|
44
|
+
if (align) {
|
|
45
|
+
attrs.style = (attrs.style || '') + `text-align: ${align};`
|
|
46
|
+
attrs['data-align'] = align
|
|
47
|
+
}
|
|
48
|
+
if (indent) {
|
|
49
|
+
attrs.style = (attrs.style || '') + `margin-left: ${indent * 2}em;`
|
|
50
|
+
attrs['data-indent'] = indent.toString()
|
|
51
|
+
}
|
|
52
|
+
return ['p', attrs, 0]
|
|
53
|
+
},
|
|
54
|
+
parseMarkdown: {
|
|
55
|
+
match: (node) => node.type === 'paragraph',
|
|
56
|
+
runner: (state, node, type) => {
|
|
57
|
+
state.openNode(type)
|
|
58
|
+
if (node.children) state.next(node.children)
|
|
59
|
+
else state.addText((node.value || '') as string)
|
|
60
|
+
|
|
61
|
+
state.closeNode()
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
toMarkdown: {
|
|
65
|
+
match: (node) => node.type.name === 'paragraph',
|
|
66
|
+
runner: (state, node) => {
|
|
67
|
+
const view = ctx.get(editorViewCtx)
|
|
68
|
+
const lastNode = view.state?.doc.lastChild
|
|
69
|
+
|
|
70
|
+
state.openNode('paragraph')
|
|
71
|
+
if (
|
|
72
|
+
(!node.content || node.content.size === 0) &&
|
|
73
|
+
node !== lastNode &&
|
|
74
|
+
shouldPreserveEmptyLine(ctx)
|
|
75
|
+
) {
|
|
76
|
+
state.addNode('html', undefined, '<br />')
|
|
77
|
+
} else {
|
|
78
|
+
serializeText(state, node)
|
|
79
|
+
}
|
|
80
|
+
state.closeNode()
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
function shouldPreserveEmptyLine(ctx: Ctx) {
|
|
86
|
+
let shouldPreserveEmptyLine = false
|
|
87
|
+
try {
|
|
88
|
+
ctx.get(remarkPreserveEmptyLinePlugin.id)
|
|
89
|
+
shouldPreserveEmptyLine = true
|
|
90
|
+
} catch {
|
|
91
|
+
shouldPreserveEmptyLine = false
|
|
92
|
+
}
|
|
93
|
+
return shouldPreserveEmptyLine
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
withMeta(paragraphSchema.node, {
|
|
97
|
+
displayName: 'NodeSchema<paragraph>',
|
|
98
|
+
group: 'Paragraph',
|
|
99
|
+
})
|
|
100
|
+
withMeta(paragraphSchema.ctx, {
|
|
101
|
+
displayName: 'NodeSchemaCtx<paragraph>',
|
|
102
|
+
group: 'Paragraph',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
/// This command can turn the selected block into paragraph.
|
|
106
|
+
export const turnIntoTextCommand = $command(
|
|
107
|
+
'TurnIntoText',
|
|
108
|
+
(ctx) => () => setBlockType(paragraphSchema.type(ctx))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
withMeta(turnIntoTextCommand, {
|
|
112
|
+
displayName: 'Command<turnIntoTextCommand>',
|
|
113
|
+
group: 'Paragraph',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/// Keymap for paragraph node.
|
|
117
|
+
/// - `<Mod-Alt-0>`: Turn the selected block into paragraph.
|
|
118
|
+
export const paragraphKeymap = $useKeymap('paragraphKeymap', {
|
|
119
|
+
TurnIntoText: {
|
|
120
|
+
shortcuts: 'Mod-Alt-0',
|
|
121
|
+
command: (ctx) => {
|
|
122
|
+
const commands = ctx.get(commandsCtx)
|
|
123
|
+
return () => commands.call(turnIntoTextCommand.key)
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
withMeta(paragraphKeymap.ctx, {
|
|
129
|
+
displayName: 'KeymapCtx<paragraph>',
|
|
130
|
+
group: 'Paragraph',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
withMeta(paragraphKeymap.shortcuts, {
|
|
134
|
+
displayName: 'Keymap<paragraph>',
|
|
135
|
+
group: 'Paragraph',
|
|
136
|
+
})
|