@milkdown/preset-gfm 4.11.2 → 4.13.2

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # @milkdown/preset-commonmark
1
+ # @milkdown/preset-gfm
2
2
 
3
- Common mark preset for [milkdown](https://saul-mirone.github.io/milkdown/).
4
- Add support for commonmark.
3
+ Github flavored markdown preset for [milkdown](https://saul-mirone.github.io/milkdown/).
4
+ Add support for gfm.
5
5
 
6
6
  # Example Usage
7
7
 
@@ -9,16 +9,15 @@ Add support for commonmark.
9
9
  import { Editor } from '@milkdown/core';
10
10
  import { nord } from '@milkdown/theme-nord';
11
11
 
12
- import { commonmark } from '@milkdown/preset-commonmark';
13
- import '@milkdown/preset-commonmark/lib/style.css';
12
+ import { gfm } from '@milkdown/preset-gfm';
14
13
 
15
- Editor.make().use(nord).use(commonmark).create();
14
+ Editor.make().use(nord).use(gfm).create();
16
15
  ```
17
16
 
18
17
  ## Custom Keymap
19
18
 
20
19
  ```typescript
21
- import { commonmark, blockquote, SupportedKeys } from '@milkdown/preset-commonmark';
20
+ import { commonmark, blockquote, SupportedKeys } from '@milkdown/preset-gfm';
22
21
 
23
22
  const nodes = commonmark.configure(blockquote, {
24
23
  keymap: {
@@ -51,11 +50,14 @@ Keymap supported:
51
50
  - NextListItem
52
51
  - SinkListItem
53
52
  - LiftListItem
53
+ - NextCell
54
+ - PrevCell
55
+ - ExitTable
54
56
 
55
57
  ## Custom Style
56
58
 
57
59
  ```typescript
58
- import { commonmark, paragraph, heading } from '@milkdown/commonmark';
60
+ import { commonmark, paragraph, heading } from '@milkdown/preset-gfm';
59
61
 
60
62
  const nodes = commonmark
61
63
  .configure(paragraph, {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@milkdown/preset-gfm",
3
- "version": "4.11.2",
3
+ "version": "4.13.2",
4
4
  "main": "lib/index.js",
5
5
  "module": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "sideEffects": false,
8
8
  "license": "MIT",
9
9
  "files": [
10
- "lib"
10
+ "lib",
11
+ "src"
11
12
  ],
12
13
  "keywords": [
13
14
  "milkdown",
@@ -20,9 +21,9 @@
20
21
  },
21
22
  "dependencies": {
22
23
  "@emotion/css": "^11.1.3",
23
- "@milkdown/plugin-table": "4.11.2",
24
- "@milkdown/preset-commonmark": "4.11.2",
25
- "@milkdown/utils": "4.11.2",
24
+ "@milkdown/plugin-table": "4.13.2",
25
+ "@milkdown/preset-commonmark": "4.13.2",
26
+ "@milkdown/utils": "4.13.2",
26
27
  "@types/prosemirror-schema-list": "^1.0.3",
27
28
  "prosemirror-schema-list": "^1.1.4",
28
29
  "tslib": "^2.2.0"
@@ -0,0 +1,27 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { prosePluginFactory } from '@milkdown/core';
3
+ import { InputRule, inputRules } from 'prosemirror-inputrules';
4
+
5
+ const urlRegex =
6
+ /(https?:\/\/)?www\.[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)$/;
7
+
8
+ const proseUrlPlugin = () =>
9
+ inputRules({
10
+ rules: [
11
+ new InputRule(urlRegex, (state, match, start, end) => {
12
+ const { schema } = state;
13
+ const [text] = match;
14
+ if (!text) return null;
15
+
16
+ return state.tr
17
+ .replaceWith(start, end, schema.text(text))
18
+ .addMark(
19
+ start,
20
+ text.length + start,
21
+ schema.marks.link.create({ href: text.startsWith('www') ? `https://${text}` : text }),
22
+ );
23
+ }),
24
+ ],
25
+ });
26
+
27
+ export const urlPlugin = prosePluginFactory(proseUrlPlugin());
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { tableNodes, tablePlugins } from '@milkdown/plugin-table';
3
+ import { commands as commonmarkCommands, commonmarkNodes, commonmarkPlugins } from '@milkdown/preset-commonmark';
4
+ import { AtomList } from '@milkdown/utils';
5
+
6
+ import { urlPlugin } from './auto-link';
7
+ import { strikeThrough, ToggleStrikeThrough } from './strike-through';
8
+ import {
9
+ LiftTaskListItem,
10
+ SinkTaskListItem,
11
+ SplitTaskListItem,
12
+ taskListItem,
13
+ TurnIntoTaskList,
14
+ } from './task-list-item';
15
+
16
+ export * from './strike-through';
17
+ export { SupportedKeys } from './supported-keys';
18
+ export * from './task-list-item';
19
+ export {
20
+ BreakTable,
21
+ // command
22
+ createTable,
23
+ InsertTable,
24
+ NextCell,
25
+ PrevCell,
26
+ // gather
27
+ table,
28
+ tableNodes,
29
+ tablePlugins,
30
+ } from '@milkdown/plugin-table';
31
+ export {
32
+ blockquote,
33
+ bulletList,
34
+ codeFence,
35
+ codeInline,
36
+ commonmark,
37
+ // gather
38
+ commonmarkNodes,
39
+ commonmarkPlugins,
40
+ // node
41
+ doc,
42
+ em,
43
+ hardbreak,
44
+ heading,
45
+ hr,
46
+ image,
47
+ // command
48
+ InsertHardbreak,
49
+ InsertHr,
50
+ InsertImage,
51
+ LiftListItem,
52
+ link,
53
+ listItem,
54
+ ModifyImage,
55
+ ModifyLink,
56
+ orderedList,
57
+ paragraph,
58
+ SinkListItem,
59
+ SplitListItem,
60
+ strong,
61
+ text,
62
+ ToggleBold,
63
+ ToggleInlineCode,
64
+ ToggleItalic,
65
+ ToggleLink,
66
+ TurnIntoCodeFence,
67
+ TurnIntoHeading,
68
+ TurnIntoText,
69
+ WrapInBlockquote,
70
+ WrapInBulletList,
71
+ WrapInOrderedList,
72
+ } from '@milkdown/preset-commonmark';
73
+
74
+ export const gfmNodes = AtomList.create([...commonmarkNodes, ...tableNodes, strikeThrough(), taskListItem()]);
75
+ export const gfmPlugins = AtomList.create([...tablePlugins, ...commonmarkPlugins, urlPlugin]);
76
+ export const gfm = AtomList.create([...gfmNodes, ...gfmPlugins]);
77
+
78
+ export const commands = {
79
+ ...commonmarkCommands,
80
+ ToggleStrikeThrough,
81
+ TurnIntoTaskList,
82
+ SinkTaskListItem,
83
+ LiftTaskListItem,
84
+ SplitTaskListItem,
85
+ } as const;
86
+ export type Commands = typeof commands;
@@ -0,0 +1,54 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { css } from '@emotion/css';
3
+ import { createCmd, createCmdKey } from '@milkdown/core';
4
+ import { createMark, createShortcut, markRule } from '@milkdown/utils';
5
+ import { toggleMark } from 'prosemirror-commands';
6
+
7
+ import { SupportedKeys } from './supported-keys';
8
+
9
+ type Keys = SupportedKeys['StrikeThrough'];
10
+
11
+ export const ToggleStrikeThrough = createCmdKey();
12
+
13
+ export const strikeThrough = createMark<Keys>((_, utils) => {
14
+ const id = 'strike_through';
15
+ const style = utils.getStyle(
16
+ (themeTool) =>
17
+ css`
18
+ text-decoration-color: ${themeTool.palette('secondary')};
19
+ `,
20
+ );
21
+
22
+ return {
23
+ id,
24
+ schema: {
25
+ parseDOM: [
26
+ { tag: 'del' },
27
+ { style: 'text-decoration', getAttrs: (value) => (value === 'line-through') as false },
28
+ ],
29
+ toDOM: (mark) => ['del', { class: utils.getClassName(mark.attrs, 'strike-through', style) }],
30
+ },
31
+ parser: {
32
+ match: (node) => node.type === 'delete',
33
+ runner: (state, node, markType) => {
34
+ state.openMark(markType);
35
+ state.next(node.children);
36
+ state.closeMark(markType);
37
+ },
38
+ },
39
+ serializer: {
40
+ match: (mark) => mark.type.name === id,
41
+ runner: (state, mark) => {
42
+ state.withMark(mark, 'delete');
43
+ },
44
+ },
45
+ inputRules: (markType) => [
46
+ markRule(/(?:~~)([^~]+)(?:~~)$/, markType),
47
+ markRule(/(?:^|[^~])(~([^~]+)~)$/, markType),
48
+ ],
49
+ commands: (markType) => [createCmd(ToggleStrikeThrough, () => toggleMark(markType))],
50
+ shortcuts: {
51
+ [SupportedKeys.StrikeThrough]: createShortcut(ToggleStrikeThrough, 'Mod-Alt-x'),
52
+ },
53
+ };
54
+ });
@@ -0,0 +1,11 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { SupportedKeys as TableKeys } from '@milkdown/plugin-table';
3
+ import { SupportedKeys as CommonmarkKeys } from '@milkdown/preset-commonmark';
4
+
5
+ export const SupportedKeys = {
6
+ ...CommonmarkKeys,
7
+ ...TableKeys,
8
+ StrikeThrough: 'StrikeThrough',
9
+ TaskList: 'TaskList',
10
+ } as const;
11
+ export type SupportedKeys = typeof SupportedKeys;
@@ -0,0 +1,220 @@
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, createShortcut } from '@milkdown/utils';
6
+ import { wrapIn } from 'prosemirror-commands';
7
+ import { wrappingInputRule } from 'prosemirror-inputrules';
8
+ import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
9
+
10
+ import { SupportedKeys } from './supported-keys';
11
+
12
+ type Keys = Extract<keyof SupportedKeys, 'SinkListItem' | 'LiftListItem' | 'NextListItem' | 'TaskList'>;
13
+
14
+ export const SplitTaskListItem = createCmdKey();
15
+ export const SinkTaskListItem = createCmdKey();
16
+ export const LiftTaskListItem = createCmdKey();
17
+ export const TurnIntoTaskList = createCmdKey();
18
+
19
+ export const taskListItem = createNode<Keys>((options, utils) => {
20
+ const id = 'task_list_item';
21
+ const style = utils.getStyle(
22
+ ({ palette, size }) =>
23
+ css`
24
+ list-style-type: none;
25
+ position: relative;
26
+
27
+ & > div {
28
+ overflow: hidden;
29
+ padding: 0 2px;
30
+ }
31
+
32
+ label {
33
+ position: absolute;
34
+ top: 0;
35
+ left: -2rem;
36
+ display: inline-block;
37
+ width: 1.5rem;
38
+ height: 1.5rem;
39
+ margin: 0.5rem 0;
40
+ input {
41
+ visibility: hidden;
42
+ }
43
+ }
44
+ label:before {
45
+ position: absolute;
46
+ top: 0;
47
+ right: 0;
48
+ bottom: 0;
49
+ left: 0;
50
+ border-radius: ${size.radius};
51
+ }
52
+ label:hover:before {
53
+ background: ${palette('background')};
54
+ }
55
+ &[data-checked='true'] {
56
+ label {
57
+ color: ${palette('primary')};
58
+ }
59
+ }
60
+ &[data-checked='false'] {
61
+ label {
62
+ color: ${palette('solid', 0.87)};
63
+ }
64
+ }
65
+ .paragraph {
66
+ margin: 0.5rem 0;
67
+ }
68
+ `,
69
+ );
70
+
71
+ return {
72
+ id,
73
+ schema: {
74
+ group: 'listItem',
75
+ content: 'paragraph block*',
76
+ defining: true,
77
+ attrs: {
78
+ checked: {
79
+ default: false,
80
+ },
81
+ },
82
+ parseDOM: [
83
+ {
84
+ tag: 'li[data-type="task-list-item"]',
85
+ getAttrs: (dom) => {
86
+ if (!(dom instanceof HTMLElement)) {
87
+ throw new Error();
88
+ }
89
+ return { checked: dom.dataset.checked === 'true' };
90
+ },
91
+ },
92
+ ],
93
+ toDOM: (node) => [
94
+ 'li',
95
+ {
96
+ 'data-type': 'task-item',
97
+ 'data-checked': node.attrs.checked ? 'true' : 'false',
98
+ class: utils.getClassName(node.attrs, 'task-list-item', style),
99
+ },
100
+ 0,
101
+ ],
102
+ },
103
+ parser: {
104
+ match: ({ type, checked }) => {
105
+ return type === 'listItem' && checked !== null;
106
+ },
107
+ runner: (state, node, type) => {
108
+ state.openNode(type, { checked: node.checked as boolean });
109
+ state.next(node.children);
110
+ state.closeNode();
111
+ },
112
+ },
113
+ serializer: {
114
+ match: (node) => node.type.name === id,
115
+ runner: (state, node) => {
116
+ state.openNode('listItem', undefined, { checked: node.attrs.checked });
117
+ state.next(node.content);
118
+ state.closeNode();
119
+ },
120
+ },
121
+ inputRules: (nodeType) => [
122
+ wrappingInputRule(/^\s*(\[([ |x])\])\s$/, nodeType, (match) => ({
123
+ checked: match[match.length - 1] === 'x',
124
+ })),
125
+ ],
126
+ commands: (nodeType) => [
127
+ createCmd(SplitTaskListItem, () => splitListItem(nodeType)),
128
+ createCmd(SinkTaskListItem, () => sinkListItem(nodeType)),
129
+ createCmd(LiftTaskListItem, () => liftListItem(nodeType)),
130
+ createCmd(TurnIntoTaskList, () => wrapIn(nodeType)),
131
+ ],
132
+ shortcuts: {
133
+ [SupportedKeys.NextListItem]: createShortcut(SplitTaskListItem, 'Enter'),
134
+ [SupportedKeys.SinkListItem]: createShortcut(SinkTaskListItem, 'Mod-]'),
135
+ [SupportedKeys.LiftListItem]: createShortcut(LiftTaskListItem, 'Mod-['),
136
+ [SupportedKeys.TaskList]: createShortcut(TurnIntoTaskList, 'Mod-Alt-9'),
137
+ },
138
+ view: (editor, nodeType, node, view, getPos, decorations) => {
139
+ if (options?.view) {
140
+ return options.view(editor, nodeType, node, view, getPos, decorations);
141
+ }
142
+ const createIcon = utils.ctx.get(themeToolCtx).slots.icon;
143
+
144
+ const listItem = document.createElement('li');
145
+ const checkboxWrapper = document.createElement('label');
146
+ const checkboxStyler = document.createElement('span');
147
+ const checkbox = document.createElement('input');
148
+ const content = document.createElement('div');
149
+
150
+ let icon = createIcon('unchecked');
151
+ checkboxWrapper.appendChild(icon);
152
+ const setIcon = (name: Icon) => {
153
+ const nextIcon = createIcon(name);
154
+ checkboxWrapper.replaceChild(nextIcon, icon);
155
+ icon = nextIcon;
156
+ };
157
+
158
+ checkboxWrapper.contentEditable = 'false';
159
+ checkbox.type = 'checkbox';
160
+ const onChange = (event: Event) => {
161
+ const target = event.target;
162
+ if (!(target instanceof HTMLInputElement)) return;
163
+
164
+ if (!view.editable) {
165
+ checkbox.checked = !checkbox.checked;
166
+
167
+ return;
168
+ }
169
+
170
+ const { tr } = view.state;
171
+
172
+ view.dispatch(
173
+ tr.setNodeMarkup(getPos(), undefined, {
174
+ checked: target.checked,
175
+ }),
176
+ );
177
+ };
178
+ checkbox.addEventListener('change', onChange);
179
+
180
+ listItem.dataset.checked = node.attrs.checked;
181
+ if (node.attrs.checked) {
182
+ checkbox.setAttribute('checked', 'checked');
183
+ }
184
+
185
+ checkboxWrapper.append(checkbox, checkboxStyler);
186
+ listItem.append(checkboxWrapper, content);
187
+
188
+ const attributes = {
189
+ 'data-type': 'task-item',
190
+ 'data-checked': node.attrs.checked ? 'true' : 'false',
191
+ class: utils.getClassName(node.attrs, 'task-list-item', style),
192
+ };
193
+ Object.entries(attributes).forEach(([key, value]) => {
194
+ listItem.setAttribute(key, value);
195
+ });
196
+ setIcon(node.attrs.checked ? 'checked' : 'unchecked');
197
+
198
+ return {
199
+ dom: listItem,
200
+ contentDOM: content,
201
+ update: (updatedNode) => {
202
+ if (updatedNode.type.name !== id) return false;
203
+
204
+ listItem.dataset.checked = updatedNode.attrs.checked;
205
+ if (updatedNode.attrs.checked) {
206
+ checkbox.setAttribute('checked', 'checked');
207
+ } else {
208
+ checkbox.removeAttribute('checked');
209
+ }
210
+ setIcon(updatedNode.attrs.checked ? 'checked' : 'unchecked');
211
+
212
+ return true;
213
+ },
214
+ destroy: () => {
215
+ checkbox.removeEventListener('change', onChange);
216
+ },
217
+ };
218
+ },
219
+ };
220
+ });