@milkdown/preset-commonmark 6.2.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milkdown/preset-commonmark",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.es.js",
6
6
  "types": "./lib/index.d.ts",
@@ -17,8 +17,8 @@
17
17
  "commonmark"
18
18
  ],
19
19
  "devDependencies": {
20
- "@milkdown/core": "6.2.0",
21
- "@milkdown/prose": "6.2.0",
20
+ "@milkdown/core": "6.3.0",
21
+ "@milkdown/prose": "6.3.0",
22
22
  "@types/unist": "^2.0.6"
23
23
  },
24
24
  "peerDependencies": {
@@ -26,9 +26,11 @@
26
26
  "@milkdown/prose": "^6.0.1"
27
27
  },
28
28
  "dependencies": {
29
- "@milkdown/utils": "6.2.0",
29
+ "@milkdown/utils": "6.3.0",
30
+ "@milkdown/exception": "6.3.0",
31
+ "unist-util-visit": "^4.0.0",
30
32
  "remark-inline-links": "^6.0.0",
31
- "tslib": "^2.3.1"
33
+ "tslib": "^2.4.0"
32
34
  },
33
35
  "nx": {
34
36
  "targets": {
package/src/mark/link.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { commandsCtx, createCmd, createCmdKey, schemaCtx, ThemeInputChipType } from '@milkdown/core';
3
+ import { expectDomTypeError, missingRootElement } from '@milkdown/exception';
3
4
  import { calculateTextPosition } from '@milkdown/prose';
4
5
  import { toggleMark } from '@milkdown/prose/commands';
5
6
  import { InputRule } from '@milkdown/prose/inputrules';
@@ -33,7 +34,7 @@ export const link = createMark<string, LinkOptions>((utils, options) => {
33
34
  tag: 'a[href]',
34
35
  getAttrs: (dom) => {
35
36
  if (!(dom instanceof HTMLElement)) {
36
- throw new Error();
37
+ throw expectDomTypeError(dom);
37
38
  }
38
39
  return { href: dom.getAttribute('href'), title: dom.getAttribute('title') };
39
40
  },
@@ -132,7 +133,7 @@ export const link = createMark<string, LinkOptions>((utils, options) => {
132
133
  calculateTextPosition(view, input, (start, end, target, parent) => {
133
134
  const $editor = view.dom.parentElement;
134
135
  if (!$editor) {
135
- throw new Error();
136
+ throw missingRootElement();
136
137
  }
137
138
 
138
139
  const selectionWidth = end.left - start.left;
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2021, Milkdown by Mirone. */
2
2
  import { createCmd, createCmdKey, editorViewCtx, ThemeCodeFenceType } from '@milkdown/core';
3
+ import { expectDomTypeError } from '@milkdown/exception';
3
4
  import { setBlockType } from '@milkdown/prose/commands';
4
5
  import { textblockTypeInputRule } from '@milkdown/prose/inputrules';
5
6
  import { Fragment } from '@milkdown/prose/model';
@@ -60,13 +61,13 @@ export const codeFence = createNode<Keys, { languageList?: string[] }>((utils, o
60
61
  preserveWhitespace: 'full',
61
62
  getAttrs: (dom) => {
62
63
  if (!(dom instanceof HTMLElement)) {
63
- throw new Error('Parse DOM error.');
64
+ throw expectDomTypeError(dom);
64
65
  }
65
66
  return { language: dom.querySelector('pre')?.dataset['language'] };
66
67
  },
67
68
  getContent: (dom, schema) => {
68
69
  if (!(dom instanceof HTMLElement)) {
69
- throw new Error('Parse DOM error.');
70
+ throw expectDomTypeError(dom);
70
71
  }
71
72
  const textNode = schema.text(dom.querySelector('pre')?.textContent ?? '');
72
73
  return Fragment.from(textNode);
@@ -77,7 +78,7 @@ export const codeFence = createNode<Keys, { languageList?: string[] }>((utils, o
77
78
  preserveWhitespace: 'full',
78
79
  getAttrs: (dom) => {
79
80
  if (!(dom instanceof HTMLElement)) {
80
- throw new Error('Parse DOM error.');
81
+ throw expectDomTypeError(dom);
81
82
  }
82
83
  return { language: dom.dataset['language'] };
83
84
  },
@@ -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 { Fragment, 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,14 @@ 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');
24
29
 
25
- export const headingPluginKey = new PluginKey('MILKDOWN_ID');
30
+ export const headingIdPluginKey = new PluginKey('MILKDOWN_HEADING_ID');
31
+ export const headingHashPluginKey = new PluginKey('MILKDOWN_HEADING_HASH');
26
32
 
27
33
  const createId = (node: Node) =>
28
34
  node.textContent
@@ -31,162 +37,272 @@ const createId = (node: Node) =>
31
37
  .toLowerCase()
32
38
  .trim();
33
39
 
34
- export const heading = createNode<Keys, { getId: (node: Node) => string }>((utils, options) => {
35
- const id = 'heading';
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);
36
51
 
37
- const getId = options?.getId ?? createId;
38
-
39
- return {
40
- id,
41
- schema: () => ({
42
- content: 'inline*',
43
- group: 'block',
44
- defining: true,
45
- attrs: {
46
- id: {
47
- default: '',
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;
48
69
  },
49
- level: {
50
- 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;
51
77
  },
52
78
  },
53
- parseDOM: headingIndex.map((x) => ({
54
- tag: `h${x}`,
55
- getAttrs: (node) => {
56
- if (!(node instanceof HTMLElement)) {
57
- 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
+ });
58
104
  }
59
- return { level: x, id: node.id };
60
- },
61
- })),
62
- toDOM: (node) => {
63
- return [
64
- `h${node.attrs['level']}`,
65
- {
66
- id: node.attrs['id'] || getId(node),
67
- class: utils.getClassName(node.attrs, `heading h${node.attrs['level']}`),
68
- },
69
- 0,
70
- ];
71
- },
72
- parseMarkdown: {
73
- match: ({ type }) => type === id,
74
- runner: (state, node, type) => {
75
- const depth = node['depth'] as number;
76
- state.openNode(type, { level: depth });
77
- state.next(node.children);
78
- state.closeNode();
79
- },
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;
80
119
  },
81
- toMarkdown: {
82
- match: (node) => node.type.name === id,
83
- runner: (state, node) => {
84
- state.openNode('heading', undefined, { depth: node.attrs['level'] });
85
- const lastIsHardbreak = node.childCount >= 1 && node.lastChild?.type.name === 'hardbreak';
86
- if (lastIsHardbreak) {
87
- const contentArr: Node[] = [];
88
- node.content.forEach((n, _, i) => {
89
- if (i === node.childCount - 1) {
90
- return;
91
- }
92
- contentArr.push(n);
93
- });
94
- state.next(Fragment.fromArray(contentArr));
95
- } else {
96
- state.next(node.content);
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;
97
150
  }
98
- state.closeNode();
151
+ });
152
+
153
+ const deco = Decoration.widget($from.before() + 1, widget, { side: -1 });
154
+ return DecorationSet.create(tr.doc, [deco]);
155
+ },
156
+ },
157
+ props: {
158
+ handleDOMEvents: {
159
+ focus: (view) => {
160
+ const tr = cloneTr(view.state.tr);
161
+ view.dispatch(tr);
162
+ return false;
99
163
  },
100
164
  },
101
- }),
102
- inputRules: (type) =>
103
- headingIndex.map((x) =>
104
- textblockTypeInputRule(new RegExp(`^(#{1,${x}})\\s$`), type, () => ({
105
- level: x,
106
- })),
107
- ),
108
- commands: (type) => [createCmd(TurnIntoHeading, (level = 1) => setBlockType(type, { level }))],
109
- shortcuts: {
110
- [SupportedKeys.H1]: createShortcut(TurnIntoHeading, 'Mod-Alt-1', 1),
111
- [SupportedKeys.H2]: createShortcut(TurnIntoHeading, 'Mod-Alt-2', 2),
112
- [SupportedKeys.H3]: createShortcut(TurnIntoHeading, 'Mod-Alt-3', 3),
113
- [SupportedKeys.H4]: createShortcut(TurnIntoHeading, 'Mod-Alt-4', 4),
114
- [SupportedKeys.H5]: createShortcut(TurnIntoHeading, 'Mod-Alt-5', 5),
115
- [SupportedKeys.H6]: createShortcut(TurnIntoHeading, 'Mod-Alt-6', 6),
165
+ decorations(this: Plugin, state) {
166
+ return this.getState(state);
167
+ },
116
168
  },
117
- prosePlugins: (type, ctx) => {
118
- let lock = false;
119
- const walkThrough = (state: EditorState, callback: (tr: Transaction) => void) => {
120
- const tr = state.tr;
121
- state.doc.descendants((node, pos) => {
122
- if (node.type === type && !lock) {
123
- if (node.textContent.trim().length === 0) {
124
- return;
125
- }
126
- const attrs = node.attrs;
127
- const id = getId(node);
169
+ });
170
+ };
128
171
 
129
- if (attrs['id'] !== id) {
130
- tr.setMeta(headingPluginKey, true).setNodeMarkup(pos, undefined, {
131
- ...attrs,
132
- id,
133
- });
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);
134
198
  }
135
- }
136
- });
137
- callback(tr);
138
- };
139
- return [
140
- new Plugin({
141
- key: headingPluginKey,
142
- props: {
143
- handleDOMEvents: {
144
- compositionstart: () => {
145
- lock = true;
146
- return false;
147
- },
148
- compositionend: () => {
149
- lock = false;
150
- const view = ctx.get(editorViewCtx);
151
- setTimeout(() => {
152
- walkThrough(view.state, (tr) => view.dispatch(tr));
153
- }, 0);
154
- return false;
155
- },
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']}`),
156
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();
157
219
  },
158
- appendTransaction: (transactions, _, nextState) => {
159
- let tr: Transaction | null = null;
160
-
161
- if (
162
- transactions.every((transaction) => !transaction.getMeta(headingPluginKey)) &&
163
- transactions.some((transaction) => transaction.docChanged)
164
- ) {
165
- walkThrough(nextState, (t) => {
166
- 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);
167
233
  });
234
+ state.next(Fragment.fromArray(contentArr));
235
+ } else {
236
+ state.next(node.content);
168
237
  }
169
-
170
- return tr;
238
+ state.closeNode();
171
239
  },
172
- view: (view) => {
173
- const doc = view.state.doc;
174
- let tr = view.state.tr;
175
- doc.descendants((node, pos) => {
176
- if (node.type.name === 'heading' && node.attrs['level']) {
177
- if (!node.attrs['id']) {
178
- tr = tr.setNodeMarkup(pos, undefined, {
179
- ...node.attrs,
180
- id: getId(node),
181
- });
182
- }
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;
183
252
  }
184
- });
185
- view.dispatch(tr);
186
- return {};
187
- },
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 });
188
268
  }),
189
- ];
190
- },
191
- };
192
- });
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/image.ts CHANGED
@@ -1,5 +1,6 @@
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';
@@ -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') || '',