@milkdown/preset-commonmark 6.1.4 → 6.3.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 (61) hide show
  1. package/lib/index.d.ts +2 -6
  2. package/lib/index.d.ts.map +1 -1
  3. package/lib/index.es.js +1012 -1065
  4. package/lib/index.es.js.map +1 -1
  5. package/lib/mark/code-inline.d.ts +1 -5
  6. package/lib/mark/code-inline.d.ts.map +1 -1
  7. package/lib/mark/em.d.ts +1 -5
  8. package/lib/mark/em.d.ts.map +1 -1
  9. package/lib/mark/index.d.ts +1 -1
  10. package/lib/mark/index.d.ts.map +1 -1
  11. package/lib/mark/link.d.ts +1 -5
  12. package/lib/mark/link.d.ts.map +1 -1
  13. package/lib/mark/strong.d.ts +1 -5
  14. package/lib/mark/strong.d.ts.map +1 -1
  15. package/lib/node/blockquote.d.ts +1 -5
  16. package/lib/node/blockquote.d.ts.map +1 -1
  17. package/lib/node/bullet-list.d.ts +1 -5
  18. package/lib/node/bullet-list.d.ts.map +1 -1
  19. package/lib/node/code-fence.d.ts +1 -5
  20. package/lib/node/code-fence.d.ts.map +1 -1
  21. package/lib/node/doc.d.ts +1 -5
  22. package/lib/node/doc.d.ts.map +1 -1
  23. package/lib/node/hardbreak.d.ts +1 -5
  24. package/lib/node/hardbreak.d.ts.map +1 -1
  25. package/lib/node/heading.d.ts +8 -6
  26. package/lib/node/heading.d.ts.map +1 -1
  27. package/lib/node/hr.d.ts +1 -5
  28. package/lib/node/hr.d.ts.map +1 -1
  29. package/lib/node/image.d.ts +1 -5
  30. package/lib/node/image.d.ts.map +1 -1
  31. package/lib/node/index.d.ts +2 -3
  32. package/lib/node/index.d.ts.map +1 -1
  33. package/lib/node/list-item.d.ts +1 -5
  34. package/lib/node/list-item.d.ts.map +1 -1
  35. package/lib/node/ordered-list.d.ts +1 -5
  36. package/lib/node/ordered-list.d.ts.map +1 -1
  37. package/lib/node/paragraph.d.ts +1 -5
  38. package/lib/node/paragraph.d.ts.map +1 -1
  39. package/lib/node/text.d.ts +1 -5
  40. package/lib/node/text.d.ts.map +1 -1
  41. package/lib/plugin/add-order-in-list.d.ts +3 -0
  42. package/lib/plugin/add-order-in-list.d.ts.map +1 -0
  43. package/lib/plugin/index.d.ts +1 -1
  44. package/lib/plugin/index.d.ts.map +1 -1
  45. package/lib/supported-keys.d.ts +1 -0
  46. package/lib/supported-keys.d.ts.map +1 -1
  47. package/package.json +7 -5
  48. package/src/mark/link.ts +55 -9
  49. package/src/node/code-fence.ts +7 -6
  50. package/src/node/hardbreak.ts +16 -2
  51. package/src/node/heading.ts +264 -117
  52. package/src/node/hr.ts +4 -2
  53. package/src/node/image.ts +4 -3
  54. package/src/node/index.ts +3 -1
  55. package/src/node/list-item.ts +93 -3
  56. package/src/node/ordered-list.ts +2 -1
  57. package/src/node/paragraph.ts +12 -2
  58. package/src/plugin/add-order-in-list.ts +19 -0
  59. package/src/plugin/index.ts +2 -1
  60. package/src/plugin/inline-nodes-cursor.ts +1 -1
  61. package/src/supported-keys.ts +1 -0
@@ -1,10 +1,13 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
- import { createCmd, createCmdKey, editorViewCtx } from '@milkdown/core';
2
+ import { createCmd, createCmdKey, Ctx, editorViewCtx, getPalette, schemaCtx } from '@milkdown/core';
3
+ import { expectDomTypeError } from '@milkdown/exception';
4
+ import { cloneTr } from '@milkdown/prose';
3
5
  import { setBlockType } from '@milkdown/prose/commands';
4
6
  import { textblockTypeInputRule } from '@milkdown/prose/inputrules';
5
- import { Node } from '@milkdown/prose/model';
7
+ import { Fragment, Node, NodeType } from '@milkdown/prose/model';
6
8
  import { EditorState, Plugin, PluginKey, Transaction } from '@milkdown/prose/state';
7
- import { createNode, createShortcut } from '@milkdown/utils';
9
+ import { Decoration, DecorationSet } from '@milkdown/prose/view';
10
+ import { createNode, createShortcut, Utils } from '@milkdown/utils';
8
11
 
9
12
  import { SupportedKeys } from '../supported-keys';
10
13
 
@@ -18,11 +21,15 @@ type Keys =
18
21
  | SupportedKeys['H3']
19
22
  | SupportedKeys['H4']
20
23
  | SupportedKeys['H5']
21
- | SupportedKeys['H6'];
24
+ | SupportedKeys['H6']
25
+ | SupportedKeys['DowngradeHeading'];
22
26
 
23
27
  export const TurnIntoHeading = createCmdKey<number>('TurnIntoHeading');
28
+ export const DowngradeHeading = createCmdKey('DowngradeHeading');
29
+
30
+ export const headingIdPluginKey = new PluginKey('MILKDOWN_HEADING_ID');
31
+ export const headingHashPluginKey = new PluginKey('MILKDOWN_HEADING_HASH');
24
32
 
25
- export const headingPluginKey = new PluginKey('MILKDOWN_ID');
26
33
  const createId = (node: Node) =>
27
34
  node.textContent
28
35
  .replace(/[\p{P}\p{S}]/gu, '')
@@ -30,132 +37,272 @@ const createId = (node: Node) =>
30
37
  .toLowerCase()
31
38
  .trim();
32
39
 
33
- export const heading = createNode<Keys>((utils) => {
34
- const id = 'heading';
35
-
36
- return {
37
- id,
38
- schema: () => ({
39
- content: 'inline*',
40
- group: 'block',
41
- defining: true,
42
- attrs: {
43
- id: {
44
- default: '',
40
+ const headingIdPlugin = (ctx: Ctx, type: NodeType, getId: (node: Node) => string): Plugin => {
41
+ let lock = false;
42
+ const walkThrough = (state: EditorState, callback: (tr: Transaction) => void) => {
43
+ const tr = state.tr;
44
+ state.doc.descendants((node, pos) => {
45
+ if (node.type === type && !lock) {
46
+ if (node.textContent.trim().length === 0) {
47
+ return;
48
+ }
49
+ const attrs = node.attrs;
50
+ const id = getId(node);
51
+
52
+ if (attrs['id'] !== id) {
53
+ tr.setMeta(headingIdPluginKey, true).setNodeMarkup(pos, undefined, {
54
+ ...attrs,
55
+ id,
56
+ });
57
+ }
58
+ }
59
+ });
60
+ callback(tr);
61
+ };
62
+ return new Plugin({
63
+ key: headingIdPluginKey,
64
+ props: {
65
+ handleDOMEvents: {
66
+ compositionstart: () => {
67
+ lock = true;
68
+ return false;
45
69
  },
46
- level: {
47
- default: 1,
70
+ compositionend: () => {
71
+ lock = false;
72
+ const view = ctx.get(editorViewCtx);
73
+ setTimeout(() => {
74
+ walkThrough(view.state, (tr) => view.dispatch(tr));
75
+ }, 0);
76
+ return false;
48
77
  },
49
78
  },
50
- parseDOM: headingIndex.map((x) => ({
51
- tag: `h${x}`,
52
- getAttrs: (node) => {
53
- if (!(node instanceof HTMLElement)) {
54
- throw new Error();
79
+ },
80
+ appendTransaction: (transactions, _, nextState) => {
81
+ let tr: Transaction | null = null;
82
+
83
+ if (
84
+ transactions.every((transaction) => !transaction.getMeta(headingIdPluginKey)) &&
85
+ transactions.some((transaction) => transaction.docChanged)
86
+ ) {
87
+ walkThrough(nextState, (t) => {
88
+ tr = t;
89
+ });
90
+ }
91
+
92
+ return tr;
93
+ },
94
+ view: (view) => {
95
+ const doc = view.state.doc;
96
+ let tr = view.state.tr;
97
+ doc.descendants((node, pos) => {
98
+ if (node.type.name === 'heading' && node.attrs['level']) {
99
+ if (!node.attrs['id']) {
100
+ tr = tr.setNodeMarkup(pos, undefined, {
101
+ ...node.attrs,
102
+ id: getId(node),
103
+ });
55
104
  }
56
- return { level: x, id: node.id };
57
- },
58
- })),
59
- toDOM: (node) => {
60
- return [
61
- `h${node.attrs['level']}`,
62
- {
63
- id: node.attrs['id'] || createId(node),
64
- class: utils.getClassName(node.attrs, `heading h${node.attrs['level']}`),
65
- },
66
- 0,
67
- ];
105
+ }
106
+ });
107
+ view.dispatch(tr);
108
+ return {};
109
+ },
110
+ });
111
+ };
112
+
113
+ const headingHashPlugin = (ctx: Ctx, type: NodeType, utils: Utils): Plugin => {
114
+ return new Plugin({
115
+ key: headingHashPluginKey,
116
+ state: {
117
+ init: () => {
118
+ return DecorationSet.empty;
68
119
  },
69
- parseMarkdown: {
70
- match: ({ type }) => type === id,
71
- runner: (state, node, type) => {
72
- const depth = node['depth'] as number;
73
- state.openNode(type, { level: depth });
74
- state.next(node.children);
75
- state.closeNode();
76
- },
120
+ apply: (tr) => {
121
+ const view = ctx.get(editorViewCtx);
122
+ if (!view.hasFocus || !view.editable) return DecorationSet.empty;
123
+
124
+ const { $from } = tr.selection;
125
+ const node = $from.node();
126
+ if (node.type !== type) {
127
+ return DecorationSet.empty;
128
+ }
129
+
130
+ const level = node.attrs['level'];
131
+ const getHashes = (level: number) => {
132
+ return Array(level)
133
+ .fill(0)
134
+ .map((_) => `#`)
135
+ .join('');
136
+ };
137
+ const widget = document.createElement('span');
138
+ widget.textContent = getHashes(level);
139
+ widget.contentEditable = 'false';
140
+ utils.themeManager.onFlush(() => {
141
+ const style = utils.getStyle(({ css }) => {
142
+ const palette = getPalette(utils.themeManager);
143
+ return css`
144
+ margin-right: 4px;
145
+ color: ${palette('primary')};
146
+ `;
147
+ });
148
+ if (style) {
149
+ widget.className = style;
150
+ }
151
+ });
152
+
153
+ const deco = Decoration.widget($from.before() + 1, widget, { side: -1 });
154
+ return DecorationSet.create(tr.doc, [deco]);
77
155
  },
78
- toMarkdown: {
79
- match: (node) => node.type.name === id,
80
- runner: (state, node) => {
81
- state.openNode('heading', undefined, { depth: node.attrs['level'] });
82
- state.next(node.content);
83
- state.closeNode();
156
+ },
157
+ props: {
158
+ handleDOMEvents: {
159
+ focus: (view) => {
160
+ const tr = cloneTr(view.state.tr);
161
+ view.dispatch(tr);
162
+ return false;
84
163
  },
85
164
  },
86
- }),
87
- inputRules: (type) =>
88
- headingIndex.map((x) =>
89
- textblockTypeInputRule(new RegExp(`^(#{1,${x}})\\s$`), type, () => ({
90
- level: x,
91
- })),
92
- ),
93
- commands: (type) => [createCmd(TurnIntoHeading, (level = 1) => setBlockType(type, { level }))],
94
- shortcuts: {
95
- [SupportedKeys.H1]: createShortcut(TurnIntoHeading, 'Mod-Alt-1', 1),
96
- [SupportedKeys.H2]: createShortcut(TurnIntoHeading, 'Mod-Alt-2', 2),
97
- [SupportedKeys.H3]: createShortcut(TurnIntoHeading, 'Mod-Alt-3', 3),
98
- [SupportedKeys.H4]: createShortcut(TurnIntoHeading, 'Mod-Alt-4', 4),
99
- [SupportedKeys.H5]: createShortcut(TurnIntoHeading, 'Mod-Alt-5', 5),
100
- [SupportedKeys.H6]: createShortcut(TurnIntoHeading, 'Mod-Alt-6', 6),
165
+ decorations(this: Plugin, state) {
166
+ return this.getState(state);
167
+ },
101
168
  },
102
- prosePlugins: (type, ctx) => {
103
- let lock = false;
104
- const walkThrough = (state: EditorState, callback: (tr: Transaction) => void) => {
105
- const tr = state.tr;
106
- state.doc.descendants((node, pos) => {
107
- if (node.type === type && !lock) {
108
- if (node.textContent.trim().length === 0) {
109
- return;
110
- }
111
- const attrs = node.attrs;
112
- const id = createId(node);
169
+ });
170
+ };
113
171
 
114
- if (attrs['id'] !== id) {
115
- tr.setMeta(headingPluginKey, true).setNodeMarkup(pos, undefined, {
116
- ...attrs,
117
- id,
118
- });
172
+ export const heading = createNode<Keys, { getId: (node: Node) => string; displayHashtag: boolean }>(
173
+ (utils, options) => {
174
+ const id = 'heading';
175
+
176
+ const getId = options?.getId ?? createId;
177
+ const displayHashtag = options?.displayHashtag ?? true;
178
+
179
+ return {
180
+ id,
181
+ schema: () => ({
182
+ content: 'inline*',
183
+ group: 'block',
184
+ defining: true,
185
+ attrs: {
186
+ id: {
187
+ default: '',
188
+ },
189
+ level: {
190
+ default: 1,
191
+ },
192
+ },
193
+ parseDOM: headingIndex.map((x) => ({
194
+ tag: `h${x}`,
195
+ getAttrs: (node) => {
196
+ if (!(node instanceof HTMLElement)) {
197
+ throw expectDomTypeError(node);
119
198
  }
120
- }
121
- });
122
- callback(tr);
123
- };
124
- return [
125
- new Plugin({
126
- key: headingPluginKey,
127
- props: {
128
- handleDOMEvents: {
129
- compositionstart: () => {
130
- lock = true;
131
- return false;
132
- },
133
- compositionend: () => {
134
- lock = false;
135
- const view = ctx.get(editorViewCtx);
136
- setTimeout(() => {
137
- walkThrough(view.state, (tr) => view.dispatch(tr));
138
- }, 0);
139
- return false;
140
- },
199
+ return { level: x, id: node.id };
200
+ },
201
+ })),
202
+ toDOM: (node) => {
203
+ return [
204
+ `h${node.attrs['level']}`,
205
+ {
206
+ id: node.attrs['id'] || getId(node),
207
+ class: utils.getClassName(node.attrs, `heading h${node.attrs['level']}`),
141
208
  },
209
+ 0,
210
+ ];
211
+ },
212
+ parseMarkdown: {
213
+ match: ({ type }) => type === id,
214
+ runner: (state, node, type) => {
215
+ const depth = node['depth'] as number;
216
+ state.openNode(type, { level: depth });
217
+ state.next(node.children);
218
+ state.closeNode();
142
219
  },
143
- appendTransaction: (transactions, _, nextState) => {
144
- let tr: Transaction | null = null;
145
-
146
- if (
147
- transactions.every((transaction) => !transaction.getMeta(headingPluginKey)) &&
148
- transactions.some((transaction) => transaction.docChanged)
149
- ) {
150
- walkThrough(nextState, (t) => {
151
- tr = t;
220
+ },
221
+ toMarkdown: {
222
+ match: (node) => node.type.name === id,
223
+ runner: (state, node) => {
224
+ state.openNode('heading', undefined, { depth: node.attrs['level'] });
225
+ const lastIsHardbreak = node.childCount >= 1 && node.lastChild?.type.name === 'hardbreak';
226
+ if (lastIsHardbreak) {
227
+ const contentArr: Node[] = [];
228
+ node.content.forEach((n, _, i) => {
229
+ if (i === node.childCount - 1) {
230
+ return;
231
+ }
232
+ contentArr.push(n);
152
233
  });
234
+ state.next(Fragment.fromArray(contentArr));
235
+ } else {
236
+ state.next(node.content);
153
237
  }
154
-
155
- return tr;
238
+ state.closeNode();
156
239
  },
240
+ },
241
+ }),
242
+ inputRules: (type, ctx) =>
243
+ headingIndex.map((x) =>
244
+ textblockTypeInputRule(new RegExp(`^(#{1,${x}})\\s$`), type, () => {
245
+ const view = ctx.get(editorViewCtx);
246
+ const { $from } = view.state.selection;
247
+ const node = $from.node();
248
+ if (node.type.name === 'heading') {
249
+ let level = Number(node.attrs['level']) + Number(x);
250
+ if (level > 6) {
251
+ level = 6;
252
+ }
253
+ return {
254
+ level,
255
+ };
256
+ }
257
+ return {
258
+ level: x,
259
+ };
260
+ }),
261
+ ),
262
+ commands: (type, ctx) => [
263
+ createCmd(TurnIntoHeading, (level = 1) => {
264
+ if (level < 1) {
265
+ return setBlockType(level === 0 ? ctx.get(schemaCtx).nodes['paragraph'] || type : type);
266
+ }
267
+ return setBlockType(level === 0 ? ctx.get(schemaCtx).nodes['paragraph'] || type : type, { level });
157
268
  }),
158
- ];
159
- },
160
- };
161
- });
269
+ createCmd(DowngradeHeading, () => {
270
+ return (state, dispatch, view) => {
271
+ const { $from } = state.selection;
272
+ const node = $from.node();
273
+ if (node.type !== type || !state.selection.empty || $from.parentOffset !== 0) return false;
274
+
275
+ const level = node.attrs['level'] - 1;
276
+ if (!level) {
277
+ return setBlockType(ctx.get(schemaCtx).nodes['paragraph'] || type)(state, dispatch, view);
278
+ }
279
+
280
+ dispatch?.(
281
+ state.tr.setNodeMarkup(state.selection.$from.before(), undefined, {
282
+ ...node.attrs,
283
+ level,
284
+ }),
285
+ );
286
+ return true;
287
+ };
288
+ }),
289
+ ],
290
+ shortcuts: {
291
+ [SupportedKeys.H1]: createShortcut(TurnIntoHeading, 'Mod-Alt-1', 1),
292
+ [SupportedKeys.H2]: createShortcut(TurnIntoHeading, 'Mod-Alt-2', 2),
293
+ [SupportedKeys.H3]: createShortcut(TurnIntoHeading, 'Mod-Alt-3', 3),
294
+ [SupportedKeys.H4]: createShortcut(TurnIntoHeading, 'Mod-Alt-4', 4),
295
+ [SupportedKeys.H5]: createShortcut(TurnIntoHeading, 'Mod-Alt-5', 5),
296
+ [SupportedKeys.H6]: createShortcut(TurnIntoHeading, 'Mod-Alt-6', 6),
297
+ [SupportedKeys.DowngradeHeading]: createShortcut(DowngradeHeading, ['Backspace', 'Delete']),
298
+ },
299
+ prosePlugins: (type, ctx) => {
300
+ const plugins = [headingIdPlugin(ctx, type, getId)];
301
+ if (displayHashtag) {
302
+ plugins.push(headingHashPlugin(ctx, type, utils));
303
+ }
304
+ return plugins;
305
+ },
306
+ };
307
+ },
308
+ );
package/src/node/hr.ts CHANGED
@@ -40,13 +40,15 @@ export const hr = createNode((utils) => {
40
40
  commands: (type, ctx) => [
41
41
  createCmd(InsertHr, () => (state, dispatch) => {
42
42
  if (!dispatch) return true;
43
+
44
+ const paragraph = ctx.get(schemaCtx).node('paragraph');
43
45
  const { tr, selection } = state;
44
- const from = selection.from;
46
+ const { from } = selection;
45
47
  const node = type.create();
46
48
  if (!node) {
47
49
  return true;
48
50
  }
49
- const _tr = tr.replaceSelectionWith(node).insert(from, ctx.get(schemaCtx).node('paragraph'));
51
+ const _tr = tr.replaceSelectionWith(node).insert(from, paragraph);
50
52
  const sel = Selection.findFrom(_tr.doc.resolve(from), 1, true);
51
53
  if (!sel) {
52
54
  return true;
package/src/node/image.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { commandsCtx, createCmd, createCmdKey, ThemeImageType, ThemeInputChipType } from '@milkdown/core';
3
+ import { expectDomTypeError } from '@milkdown/exception';
3
4
  import { findSelectedNodeOfType } from '@milkdown/prose';
4
5
  import { InputRule } from '@milkdown/prose/inputrules';
5
6
  import { Plugin, PluginKey } from '@milkdown/prose/state';
6
- import { EditorView } from '@milkdown/prose/view';
7
+ import { EditorView, NodeView } from '@milkdown/prose/view';
7
8
  import { createNode } from '@milkdown/utils';
8
9
 
9
10
  export const ModifyImage = createCmdKey<string>('ModifyImage');
@@ -41,7 +42,7 @@ export const image = createNode<string, ImageOptions>((utils, options) => {
41
42
  tag: 'img[src]',
42
43
  getAttrs: (dom) => {
43
44
  if (!(dom instanceof HTMLElement)) {
44
- throw new Error();
45
+ throw expectDomTypeError(dom);
45
46
  }
46
47
  return {
47
48
  src: dom.getAttribute('src') || '',
@@ -134,7 +135,7 @@ export const image = createNode<string, ImageOptions>((utils, options) => {
134
135
  });
135
136
 
136
137
  if (!renderer) {
137
- return {};
138
+ return {} as NodeView;
138
139
  }
139
140
 
140
141
  const { dom, onUpdate } = renderer;
package/src/node/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
+ import { AtomPlugin } from '@milkdown/utils';
3
+
2
4
  import { blockquote } from './blockquote';
3
5
  import { bulletList } from './bullet-list';
4
6
  import { codeFence } from './code-fence';
@@ -12,7 +14,7 @@ import { orderedList } from './ordered-list';
12
14
  import { paragraph } from './paragraph';
13
15
  import { text } from './text';
14
16
 
15
- export const nodes = [
17
+ export const nodes: AtomPlugin[] = [
16
18
  doc(),
17
19
  paragraph(),
18
20
  hardbreak(),
@@ -1,7 +1,11 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { createCmd, createCmdKey } from '@milkdown/core';
3
+ import { expectDomTypeError } from '@milkdown/exception';
4
+ import { getNodeFromSchema } from '@milkdown/prose';
3
5
  import { wrappingInputRule } from '@milkdown/prose/inputrules';
6
+ import { NodeType } from '@milkdown/prose/model';
4
7
  import { liftListItem, sinkListItem, splitListItem } from '@milkdown/prose/schema-list';
8
+ import { EditorState, Plugin, PluginKey, Transaction } from '@milkdown/prose/state';
5
9
  import { createNode, createShortcut } from '@milkdown/utils';
6
10
 
7
11
  import { SupportedKeys } from '../supported-keys';
@@ -14,18 +18,103 @@ export const SplitListItem = createCmdKey('SplitListItem');
14
18
  export const SinkListItem = createCmdKey('SinkListItem');
15
19
  export const LiftListItem = createCmdKey('LiftListItem');
16
20
 
21
+ const keepListOrderPluginKey = new PluginKey('MILKDOWN_KEEP_LIST_ORDER');
22
+
23
+ const createKeepListOrderPlugin = (type: NodeType) => {
24
+ const walkThrough = (state: EditorState, callback: (tr: Transaction) => void) => {
25
+ const orderedListType = getNodeFromSchema('ordered_list', state.schema);
26
+ let tr = state.tr;
27
+ state.doc.descendants((node, pos, parent, index) => {
28
+ if (node.type === type && parent?.type === orderedListType) {
29
+ let changed = false;
30
+ const attrs = { ...node.attrs };
31
+ if (node.attrs['listType'] !== 'ordered') {
32
+ attrs['listType'] = 'ordered';
33
+ changed = true;
34
+ }
35
+
36
+ const prev = parent?.maybeChild(index - 1);
37
+ if (prev && prev.type === type && prev.attrs['listType'] === 'ordered') {
38
+ const label = prev.attrs['label'];
39
+ attrs['label'] = `${Number(label.slice(0, -1)) + 1}.`;
40
+ changed = true;
41
+ }
42
+
43
+ if (node.attrs['label'] === '•') {
44
+ attrs['label'] = `${index + 1}.`;
45
+ changed = true;
46
+ }
47
+
48
+ if (changed) {
49
+ tr = tr.setNodeMarkup(pos, undefined, attrs);
50
+ }
51
+ }
52
+ });
53
+ callback(tr);
54
+ };
55
+ return new Plugin({
56
+ key: keepListOrderPluginKey,
57
+ appendTransaction: (transactions, _oldState, nextState) => {
58
+ let tr: Transaction | null = null;
59
+ if (transactions.some((transaction) => transaction.docChanged)) {
60
+ walkThrough(nextState, (t) => {
61
+ tr = t;
62
+ });
63
+ }
64
+
65
+ return tr;
66
+ },
67
+ });
68
+ };
69
+
17
70
  export const listItem = createNode<Keys>((utils) => ({
18
71
  id,
19
72
  schema: () => ({
20
73
  group: 'listItem',
21
74
  content: 'paragraph block*',
75
+ attrs: {
76
+ label: {
77
+ default: '•',
78
+ },
79
+ listType: {
80
+ default: 'bullet',
81
+ },
82
+ },
22
83
  defining: true,
23
- parseDOM: [{ tag: 'li' }],
24
- toDOM: (node) => ['li', { class: utils.getClassName(node.attrs, 'list-item') }, 0],
84
+ parseDOM: [
85
+ {
86
+ tag: 'li.list-item',
87
+ getAttrs: (dom) => {
88
+ if (!(dom instanceof HTMLElement)) {
89
+ throw expectDomTypeError(dom);
90
+ }
91
+ return {
92
+ label: dom.dataset['label'],
93
+ listType: dom.dataset['list-type'],
94
+ };
95
+ },
96
+ contentElement: 'div.list-item_body',
97
+ },
98
+ { tag: 'li' },
99
+ ],
100
+ toDOM: (node) => {
101
+ return [
102
+ 'li',
103
+ {
104
+ class: utils.getClassName(node.attrs, 'list-item'),
105
+ 'data-label': node.attrs['label'],
106
+ 'data-list-type': node.attrs['listType'],
107
+ },
108
+ ['div', { class: utils.getClassName(node.attrs, 'list-item_label') }, node.attrs['label']],
109
+ ['div', { class: utils.getClassName(node.attrs, 'list-item_body') }, 0],
110
+ ];
111
+ },
25
112
  parseMarkdown: {
26
113
  match: ({ type, checked }) => type === 'listItem' && checked === null,
27
114
  runner: (state, node, type) => {
28
- state.openNode(type);
115
+ const label = node['label'] != null ? `${node['label']}.` : '•';
116
+ const listType = node['label'] != null ? 'ordered' : 'bullet';
117
+ state.openNode(type, { label, listType });
29
118
  state.next(node.children);
30
119
  state.closeNode();
31
120
  },
@@ -50,4 +139,5 @@ export const listItem = createNode<Keys>((utils) => ({
50
139
  [SupportedKeys.SinkListItem]: createShortcut(SinkListItem, 'Mod-]'),
51
140
  [SupportedKeys.LiftListItem]: createShortcut(LiftListItem, 'Mod-['),
52
141
  },
142
+ prosePlugins: (nodeType) => [createKeepListOrderPlugin(nodeType)],
53
143
  }));
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { createCmd, createCmdKey } from '@milkdown/core';
3
+ import { expectDomTypeError } from '@milkdown/exception';
3
4
  import { wrapIn } from '@milkdown/prose/commands';
4
5
  import { wrappingInputRule } from '@milkdown/prose/inputrules';
5
6
  import { createNode, createShortcut } from '@milkdown/utils';
@@ -26,7 +27,7 @@ export const orderedList = createNode<Keys>((utils) => ({
26
27
  tag: 'ol',
27
28
  getAttrs: (dom) => {
28
29
  if (!(dom instanceof HTMLElement)) {
29
- throw new Error();
30
+ throw expectDomTypeError(dom);
30
31
  }
31
32
  return { order: dom.hasAttribute('start') ? Number(dom.getAttribute('start')) : 1 };
32
33
  },
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { createCmd, createCmdKey } from '@milkdown/core';
3
3
  import { setBlockType } from '@milkdown/prose/commands';
4
+ import { Fragment, Node } from '@milkdown/prose/model';
4
5
  import { createNode, createShortcut } from '@milkdown/utils';
5
6
 
6
7
  import { SupportedKeys } from '../supported-keys';
@@ -34,8 +35,17 @@ export const paragraph = createNode<Keys>((utils) => {
34
35
  match: (node) => node.type.name === 'paragraph',
35
36
  runner: (state, node) => {
36
37
  state.openNode('paragraph');
37
- const onlyHardbreak = node.childCount === 1 && node.firstChild?.type.name === 'hardbreak';
38
- if (!onlyHardbreak) {
38
+ const lastIsHardbreak = node.childCount >= 1 && node.lastChild?.type.name === 'hardbreak';
39
+ if (lastIsHardbreak) {
40
+ const contentArr: Node[] = [];
41
+ node.content.forEach((n, _, i) => {
42
+ if (i === node.childCount - 1) {
43
+ return;
44
+ }
45
+ contentArr.push(n);
46
+ });
47
+ state.next(Fragment.fromArray(contentArr));
48
+ } else {
39
49
  state.next(node.content);
40
50
  }
41
51
  state.closeNode();