@milkdown/preset-commonmark 4.12.0 → 4.13.3

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/src/node/hr.ts ADDED
@@ -0,0 +1,67 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { css } from '@emotion/css';
3
+ import { createCmd, createCmdKey } from '@milkdown/core';
4
+ import { createNode } from '@milkdown/utils';
5
+ import { InputRule } from 'prosemirror-inputrules';
6
+ import { Selection } from 'prosemirror-state';
7
+
8
+ const id = 'hr';
9
+ export const InsertHr = createCmdKey<string>();
10
+ export const hr = createNode((_, utils) => {
11
+ const style = utils.getStyle(
12
+ (themeTool) => css`
13
+ height: ${themeTool.size.lineWidth};
14
+ background-color: ${themeTool.palette('line')};
15
+ border-width: 0;
16
+ `,
17
+ );
18
+ return {
19
+ id,
20
+ schema: {
21
+ group: 'block',
22
+ parseDOM: [{ tag: 'hr' }],
23
+ toDOM: (node) => ['hr', { class: utils.getClassName(node.attrs, id, style) }],
24
+ },
25
+ parser: {
26
+ match: ({ type }) => type === 'thematicBreak',
27
+ runner: (state, _, type) => {
28
+ state.addNode(type);
29
+ },
30
+ },
31
+ serializer: {
32
+ match: (node) => node.type.name === id,
33
+ runner: (state) => {
34
+ state.addNode('thematicBreak');
35
+ },
36
+ },
37
+ inputRules: (nodeType) => [
38
+ new InputRule(/^(?:---|___\s|\*\*\*\s)$/, (state, match, start, end) => {
39
+ const { tr } = state;
40
+
41
+ if (match[0]) {
42
+ tr.replaceWith(start, end, nodeType.create({}));
43
+ }
44
+
45
+ return tr;
46
+ }),
47
+ ],
48
+ commands: (nodeType, schema) => [
49
+ createCmd(InsertHr, () => (state, dispatch) => {
50
+ if (!dispatch) return true;
51
+ const { tr, selection } = state;
52
+ const from = selection.from;
53
+ const node = nodeType.create();
54
+ if (!node) {
55
+ return true;
56
+ }
57
+ const _tr = tr.replaceSelectionWith(node).insert(from, schema.node('paragraph'));
58
+ const sel = Selection.findFrom(_tr.doc.resolve(from), 1, true);
59
+ if (!sel) {
60
+ return true;
61
+ }
62
+ dispatch(_tr.setSelection(sel).scrollIntoView());
63
+ return true;
64
+ }),
65
+ ],
66
+ };
67
+ });
@@ -0,0 +1,334 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { css } from '@emotion/css';
3
+ import { createCmd, createCmdKey, themeToolCtx } from '@milkdown/core';
4
+ import type { Icon } from '@milkdown/design-system';
5
+ import { createNode, findSelectedNodeOfType } from '@milkdown/utils';
6
+ import { InputRule } from 'prosemirror-inputrules';
7
+
8
+ export const ModifyImage = createCmdKey<string>();
9
+ export const InsertImage = createCmdKey<string>();
10
+ const id = 'image';
11
+ export type ImageOptions = {
12
+ isBlock: boolean;
13
+ placeholder: {
14
+ loading: string;
15
+ empty: string;
16
+ failed: string;
17
+ };
18
+ };
19
+ export const image = createNode<string, ImageOptions>((options, utils) => {
20
+ const placeholder = {
21
+ loading: 'Loading...',
22
+ empty: 'Add an Image',
23
+ failed: 'Image loads failed',
24
+ ...(options?.placeholder ?? {}),
25
+ };
26
+ const isBlock = options?.isBlock ?? false;
27
+ const containerStyle = utils.getStyle(
28
+ (themeTool) =>
29
+ css`
30
+ display: inline-block;
31
+ position: relative;
32
+ text-align: center;
33
+ font-size: 0;
34
+ vertical-align: text-bottom;
35
+ ${isBlock
36
+ ? `
37
+ width: 100%;
38
+ margin: 0 auto;
39
+ `
40
+ : ''}
41
+ img {
42
+ max-width: 100%;
43
+ height: auto;
44
+ object-fit: contain;
45
+ margin: 0 2px;
46
+ }
47
+ .icon,
48
+ .placeholder {
49
+ display: none;
50
+ }
51
+
52
+ &.system {
53
+ width: 100%;
54
+ padding: 0 2rem;
55
+
56
+ img {
57
+ width: 0;
58
+ height: 0;
59
+ display: none;
60
+ }
61
+
62
+ .icon,
63
+ .placeholder {
64
+ display: inline;
65
+ }
66
+
67
+ box-sizing: border-box;
68
+ height: 3rem;
69
+ background-color: ${themeTool.palette('background')};
70
+ border-radius: ${themeTool.size.radius};
71
+ display: inline-flex;
72
+ gap: 2rem;
73
+ justify-content: flex-start;
74
+ align-items: center;
75
+ .placeholder {
76
+ margin: 0;
77
+ line-height: 1;
78
+ &::before {
79
+ content: '';
80
+ font-size: 0.875rem;
81
+ color: ${themeTool.palette('neutral', 0.6)};
82
+ }
83
+ }
84
+ }
85
+
86
+ &.loading {
87
+ .placeholder {
88
+ &::before {
89
+ content: '${placeholder.loading}';
90
+ }
91
+ }
92
+ }
93
+
94
+ &.empty {
95
+ .placeholder {
96
+ &::before {
97
+ content: '${placeholder.empty}';
98
+ }
99
+ }
100
+ }
101
+
102
+ &.failed {
103
+ .placeholder {
104
+ &::before {
105
+ content: '${placeholder.failed}';
106
+ }
107
+ }
108
+ }
109
+ `,
110
+ );
111
+
112
+ const style = utils.getStyle(
113
+ () =>
114
+ css`
115
+ display: inline-block;
116
+ margin: 0 auto;
117
+ object-fit: contain;
118
+ width: 100%;
119
+ position: relative;
120
+ height: auto;
121
+ text-align: center;
122
+ `,
123
+ );
124
+
125
+ return {
126
+ id,
127
+ schema: {
128
+ inline: true,
129
+ group: 'inline',
130
+ draggable: true,
131
+ marks: '',
132
+ atom: true,
133
+ defining: true,
134
+ isolating: true,
135
+ attrs: {
136
+ src: { default: '' },
137
+ alt: { default: null },
138
+ title: { default: null },
139
+ failed: { default: false },
140
+ loading: { default: true },
141
+ width: { default: 0 },
142
+ },
143
+ parseDOM: [
144
+ {
145
+ tag: 'img[src]',
146
+ getAttrs: (dom) => {
147
+ if (!(dom instanceof HTMLElement)) {
148
+ throw new Error();
149
+ }
150
+ return {
151
+ failed: dom.classList.contains('failed'),
152
+ loading: dom.classList.contains('loading'),
153
+ src: dom.getAttribute('src') || '',
154
+ alt: dom.getAttribute('alt'),
155
+ title: dom.getAttribute('title') || dom.getAttribute('alt'),
156
+ width: dom.getAttribute('width') || 0,
157
+ };
158
+ },
159
+ },
160
+ ],
161
+ toDOM: (node) => {
162
+ return [
163
+ 'img',
164
+ {
165
+ ...node.attrs,
166
+ class: utils.getClassName(
167
+ node.attrs,
168
+ id,
169
+ node.attrs.failed ? 'failed' : '',
170
+ node.attrs.loading ? 'loading' : '',
171
+ style,
172
+ ),
173
+ },
174
+ ];
175
+ },
176
+ },
177
+ parser: {
178
+ match: ({ type }) => type === id,
179
+ runner: (state, node, type) => {
180
+ const url = node.url as string;
181
+ const alt = node.alt as string;
182
+ const title = node.title as string;
183
+ state.addNode(type, {
184
+ src: url,
185
+ alt,
186
+ title,
187
+ });
188
+ },
189
+ },
190
+ serializer: {
191
+ match: (node) => node.type.name === id,
192
+ runner: (state, node) => {
193
+ state.addNode('image', undefined, undefined, {
194
+ title: node.attrs.title,
195
+ url: node.attrs.src,
196
+ alt: node.attrs.alt,
197
+ });
198
+ },
199
+ },
200
+ commands: (nodeType) => [
201
+ createCmd(InsertImage, (src = '') => (state, dispatch) => {
202
+ if (!dispatch) return true;
203
+ const { tr } = state;
204
+ const node = nodeType.create({ src });
205
+ if (!node) {
206
+ return true;
207
+ }
208
+ const _tr = tr.replaceSelectionWith(node);
209
+ dispatch(_tr.scrollIntoView());
210
+ return true;
211
+ }),
212
+ createCmd(ModifyImage, (src = '') => (state, dispatch) => {
213
+ const node = findSelectedNodeOfType(state.selection, nodeType);
214
+ if (!node) return false;
215
+
216
+ const { tr } = state;
217
+ dispatch?.(
218
+ tr.setNodeMarkup(node.pos, undefined, { ...node.node.attrs, loading: true, src }).scrollIntoView(),
219
+ );
220
+
221
+ return true;
222
+ }),
223
+ ],
224
+ inputRules: (nodeType) => [
225
+ new InputRule(
226
+ /!\[(?<alt>.*?)]\((?<filename>.*?)\s*(?="|\))"?(?<title>[^"]+)?"?\)/,
227
+ (state, match, start, end) => {
228
+ const [okay, alt, src = '', title] = match;
229
+ const { tr } = state;
230
+ if (okay) {
231
+ tr.replaceWith(start, end, nodeType.create({ src, alt, title }));
232
+ }
233
+
234
+ return tr;
235
+ },
236
+ ),
237
+ ],
238
+ view: (_editor, nodeType, node, view, getPos) => {
239
+ const createIcon = utils.ctx.get(themeToolCtx).slots.icon;
240
+ const container = document.createElement('span');
241
+ container.className = utils.getClassName(node.attrs, id, containerStyle);
242
+ container.contentEditable = 'false';
243
+
244
+ const content = document.createElement('img');
245
+ content.contentEditable = 'true';
246
+
247
+ container.append(content);
248
+ let icon = createIcon('image');
249
+ const placeholder = document.createElement('span');
250
+ placeholder.classList.add('placeholder');
251
+ container.append(icon, placeholder);
252
+
253
+ const setIcon = (name: Icon) => {
254
+ const nextIcon = createIcon(name);
255
+ container.replaceChild(nextIcon, icon);
256
+ icon = nextIcon;
257
+ };
258
+
259
+ const loadImage = (src: string) => {
260
+ container.classList.add('system', 'loading');
261
+ setIcon('loading');
262
+ const img = document.createElement('img');
263
+ img.src = src;
264
+
265
+ img.onerror = () => {
266
+ const { tr } = view.state;
267
+ const _tr = tr.setNodeMarkup(getPos(), nodeType, {
268
+ ...node.attrs,
269
+ src,
270
+ loading: false,
271
+ failed: true,
272
+ });
273
+ view.dispatch(_tr);
274
+ };
275
+
276
+ img.onload = () => {
277
+ const { tr } = view.state;
278
+ const _tr = tr.setNodeMarkup(getPos(), nodeType, {
279
+ ...node.attrs,
280
+ width: img.width,
281
+ src,
282
+ loading: false,
283
+ failed: false,
284
+ });
285
+ view.dispatch(_tr);
286
+ };
287
+ };
288
+
289
+ const { src, loading, title, alt, width } = node.attrs;
290
+ content.src = src;
291
+ content.title = title || alt;
292
+ content.alt = alt;
293
+ content.width = width;
294
+
295
+ if (src.length === 0) {
296
+ container.classList.add('system', 'empty');
297
+ setIcon('image');
298
+ } else if (loading) {
299
+ loadImage(src);
300
+ }
301
+
302
+ return {
303
+ dom: container,
304
+ update: (updatedNode) => {
305
+ if (updatedNode.type.name !== id) return false;
306
+
307
+ const { src, alt, title, loading, failed, width } = updatedNode.attrs;
308
+ content.src = src;
309
+ content.alt = alt;
310
+ content.title = title || alt;
311
+ content.width = width;
312
+ if (loading) {
313
+ loadImage(src);
314
+ return true;
315
+ }
316
+ if (failed) {
317
+ container.classList.remove('loading', 'empty');
318
+ container.classList.add('system', 'failed');
319
+ setIcon('brokenImage');
320
+ return true;
321
+ }
322
+ if (src.length > 0) {
323
+ container.classList.remove('system', 'empty', 'loading');
324
+ return true;
325
+ }
326
+
327
+ container.classList.add('system', 'empty');
328
+ setIcon('image');
329
+ return true;
330
+ },
331
+ };
332
+ },
333
+ };
334
+ });
@@ -0,0 +1,41 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { blockquote } from './blockquote';
3
+ import { bulletList } from './bullet-list';
4
+ import { codeFence } from './code-fence';
5
+ import { doc } from './doc';
6
+ import { hardbreak } from './hardbreak';
7
+ import { heading } from './heading';
8
+ import { hr } from './hr';
9
+ import { image } from './image';
10
+ import { listItem } from './list-item';
11
+ import { orderedList } from './ordered-list';
12
+ import { paragraph } from './paragraph';
13
+ import { text } from './text';
14
+
15
+ export const nodes = [
16
+ doc(),
17
+ paragraph(),
18
+ hardbreak(),
19
+ blockquote(),
20
+ codeFence(),
21
+ bulletList(),
22
+ orderedList(),
23
+ listItem(),
24
+ heading(),
25
+ hr(),
26
+ image(),
27
+ text(),
28
+ ];
29
+
30
+ export * from './blockquote';
31
+ export * from './bullet-list';
32
+ export * from './code-fence';
33
+ export * from './doc';
34
+ export * from './hardbreak';
35
+ export * from './heading';
36
+ export * from './hr';
37
+ export * from './image';
38
+ export * from './list-item';
39
+ export * from './ordered-list';
40
+ export * from './paragraph';
41
+ export * from './text';
@@ -0,0 +1,73 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { css } from '@emotion/css';
3
+ import { createCmd, createCmdKey } from '@milkdown/core';
4
+ import { createNode, createShortcut } from '@milkdown/utils';
5
+ import { wrappingInputRule } from 'prosemirror-inputrules';
6
+ import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
7
+
8
+ import { SupportedKeys } from '../supported-keys';
9
+
10
+ type Keys = SupportedKeys['SinkListItem'] | SupportedKeys['LiftListItem'] | SupportedKeys['NextListItem'];
11
+
12
+ const id = 'list_item';
13
+
14
+ export const SplitListItem = createCmdKey();
15
+ export const SinkListItem = createCmdKey();
16
+ export const LiftListItem = createCmdKey();
17
+
18
+ export const listItem = createNode<Keys>((_, utils) => {
19
+ const style = utils.getStyle(
20
+ (themeTool) =>
21
+ css`
22
+ &,
23
+ & > * {
24
+ margin: 0.5rem 0;
25
+ }
26
+
27
+ &,
28
+ li {
29
+ &::marker {
30
+ color: ${themeTool.palette('primary')};
31
+ }
32
+ }
33
+ `,
34
+ );
35
+
36
+ return {
37
+ id,
38
+ schema: {
39
+ group: 'listItem',
40
+ content: 'paragraph block*',
41
+ defining: true,
42
+ parseDOM: [{ tag: 'li' }],
43
+ toDOM: (node) => ['li', { class: utils.getClassName(node.attrs, 'list-item', style) }, 0],
44
+ },
45
+ parser: {
46
+ match: ({ type, checked }) => type === 'listItem' && checked === null,
47
+ runner: (state, node, type) => {
48
+ state.openNode(type);
49
+ state.next(node.children);
50
+ state.closeNode();
51
+ },
52
+ },
53
+ serializer: {
54
+ match: (node) => node.type.name === id,
55
+ runner: (state, node) => {
56
+ state.openNode('listItem');
57
+ state.next(node.content);
58
+ state.closeNode();
59
+ },
60
+ },
61
+ inputRules: (nodeType) => [wrappingInputRule(/^\s*([-+*])\s$/, nodeType)],
62
+ commands: (nodeType) => [
63
+ createCmd(SplitListItem, () => splitListItem(nodeType)),
64
+ createCmd(SinkListItem, () => sinkListItem(nodeType)),
65
+ createCmd(LiftListItem, () => liftListItem(nodeType)),
66
+ ],
67
+ shortcuts: {
68
+ [SupportedKeys.NextListItem]: createShortcut(SplitListItem, 'Enter'),
69
+ [SupportedKeys.SinkListItem]: createShortcut(SinkListItem, 'Mod-]'),
70
+ [SupportedKeys.LiftListItem]: createShortcut(LiftListItem, 'Mod-['),
71
+ },
72
+ };
73
+ });
@@ -0,0 +1,70 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { createCmd, createCmdKey } from '@milkdown/core';
3
+ import { createNode, createShortcut } from '@milkdown/utils';
4
+ import { wrapIn } from 'prosemirror-commands';
5
+ import { wrappingInputRule } from 'prosemirror-inputrules';
6
+
7
+ import { SupportedKeys } from '../supported-keys';
8
+
9
+ type Keys = SupportedKeys['OrderedList'];
10
+
11
+ export const WrapInOrderedList = createCmdKey();
12
+
13
+ const id = 'ordered_list';
14
+ export const orderedList = createNode<Keys>((_, utils) => ({
15
+ id,
16
+ schema: {
17
+ content: 'listItem+',
18
+ group: 'block',
19
+ attrs: {
20
+ order: {
21
+ default: 1,
22
+ },
23
+ },
24
+ parseDOM: [
25
+ {
26
+ tag: 'ol',
27
+ getAttrs: (dom) => {
28
+ if (!(dom instanceof HTMLElement)) {
29
+ throw new Error();
30
+ }
31
+ return { order: dom.hasAttribute('start') ? Number(dom.getAttribute('start')) : 1 };
32
+ },
33
+ },
34
+ ],
35
+ toDOM: (node) => [
36
+ 'ol',
37
+ {
38
+ ...(node.attrs.order === 1 ? {} : node.attrs.order),
39
+ class: utils.getClassName(node.attrs, 'ordered-list'),
40
+ },
41
+ 0,
42
+ ],
43
+ },
44
+ parser: {
45
+ match: ({ type, ordered }) => type === 'list' && !!ordered,
46
+ runner: (state, node, type) => {
47
+ state.openNode(type).next(node.children).closeNode();
48
+ },
49
+ },
50
+ serializer: {
51
+ match: (node) => node.type.name === id,
52
+ runner: (state, node) => {
53
+ state.openNode('list', undefined, { ordered: true, start: 1 });
54
+ state.next(node.content);
55
+ state.closeNode();
56
+ },
57
+ },
58
+ inputRules: (nodeType) => [
59
+ wrappingInputRule(
60
+ /^(\d+)\.\s$/,
61
+ nodeType,
62
+ (match) => ({ order: Number(match[1]) }),
63
+ (match, node) => node.childCount + node.attrs.order === Number(match[1]),
64
+ ),
65
+ ],
66
+ commands: (nodeType) => [createCmd(WrapInOrderedList, () => wrapIn(nodeType))],
67
+ shortcuts: {
68
+ [SupportedKeys.OrderedList]: createShortcut(WrapInOrderedList, 'Mod-Shift-7'),
69
+ },
70
+ }));
@@ -0,0 +1,56 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { css } from '@emotion/css';
3
+ import { createCmd, createCmdKey } from '@milkdown/core';
4
+ import { createNode, createShortcut } from '@milkdown/utils';
5
+ import { setBlockType } from 'prosemirror-commands';
6
+
7
+ import { SupportedKeys } from '../supported-keys';
8
+
9
+ type Keys = SupportedKeys['Text'];
10
+
11
+ export const TurnIntoText = createCmdKey();
12
+
13
+ const id = 'paragraph';
14
+ export const paragraph = createNode<Keys>((options, utils) => {
15
+ const style = options?.headless
16
+ ? null
17
+ : css`
18
+ font-size: 1rem;
19
+ line-height: 1.5;
20
+ letter-spacing: 0.5px;
21
+ `;
22
+
23
+ return {
24
+ id,
25
+ schema: {
26
+ content: 'inline*',
27
+ group: 'block',
28
+ parseDOM: [{ tag: 'p' }],
29
+ toDOM: (node) => ['p', { class: utils.getClassName(node.attrs, id, style) }, 0],
30
+ },
31
+ parser: {
32
+ match: (node) => node.type === 'paragraph',
33
+ runner: (state, node, type) => {
34
+ state.openNode(type);
35
+ if (node.children) {
36
+ state.next(node.children);
37
+ } else {
38
+ state.addText(node.value as string);
39
+ }
40
+ state.closeNode();
41
+ },
42
+ },
43
+ serializer: {
44
+ match: (node) => node.type.name === 'paragraph',
45
+ runner: (state, node) => {
46
+ state.openNode('paragraph');
47
+ state.next(node.content);
48
+ state.closeNode();
49
+ },
50
+ },
51
+ commands: (nodeType) => [createCmd(TurnIntoText, () => setBlockType(nodeType))],
52
+ shortcuts: {
53
+ [SupportedKeys.Text]: createShortcut(TurnIntoText, 'Mod-Alt-0'),
54
+ },
55
+ };
56
+ });
@@ -0,0 +1,21 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { createNode } from '@milkdown/utils';
3
+
4
+ export const text = createNode(() => ({
5
+ id: 'text',
6
+ schema: {
7
+ group: 'inline',
8
+ },
9
+ parser: {
10
+ match: ({ type }) => type === 'text',
11
+ runner: (state, node) => {
12
+ state.addText(node.value as string);
13
+ },
14
+ },
15
+ serializer: {
16
+ match: (node) => node.type.name === 'text',
17
+ runner: (state, node) => {
18
+ state.addNode('text', undefined, node.text as string);
19
+ },
20
+ },
21
+ }));