最近因为写ToDoList软件的时候需要一款MarkDown编辑器,尝试过很多种。都有些不尽人意的地方,后来热心群友推荐了Milkdown这款编辑器,当时一看到界面就被它的颜值吸引到了,在体验到他便捷的语法后当即决定使用这款编辑器。
但是使用过程中,并不是很顺畅,文档有些感觉就像是话说了一半的样子,网上的文档也不是很多。所以这里将进行一些使用过程中遇见的问题记录。方便自己帮助他人。

官方示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {
createCmdKey,
MilkdownPlugin,
CommandsReady,
commandsCtx,
schemaCtx,
} from "@milkdown/core";
import { wrapIn } from "prosemirror-commands";

export const WrapInBlockquote = createCmdKey();
const plugin: MilkdownPlugin = () => async (ctx) => {
// wait for command manager ready
await ctx.wait(CommandsReady);

const commandManager = ctx.get(commandsCtx);
const schema = ctx.get(schemaCtx);

commandManager.create(WrapInBlockquote, () =>
wrapIn(schema.nodes.blockquote)
);
};

// call command
commandManager.call(WrapInBlockquote);

我的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
createCmdKey,
MilkdownPlugin,
CommandsReady,
commandsCtx,
schemaCtx,
} from "@milkdown/core";
import { wrapIn } from "prosemirror-commands";

export const taskList = createCmdKey("TaskList");
export const taskListPlugin: MilkdownPlugin = () => async (ctx) => {
// 等待命令管理器初始化完成
await ctx.wait(CommandsReady);

const commandManager = ctx.get(commandsCtx);
const schema = ctx.get(schemaCtx);

// 下方wrapIn(schema.nodes.blockquote)可以修改为自己所期望的行为
commandManager.create(taskList, () => wrapIn(schema.nodes.blockquote));
};

使用方式:

1
2
3
4
5
6
7
8
const { editor } = useEditor(
(root) =>
Editor.make()
.config((ctx) => {
ctx.set(rootCtx, root);
})
.use(taskListPlugin) // 你导出的插件名
);

该功能主要是自定义 markdown 中元素的样式,比如超链接的样子。唯一的坑点就是你引入@milkdown/preset-gfm这个包后,会和官方示例冲突,所以使用官方示例的时候需要注释掉@milkdown/preset-gfm这个插件的使用。即可解决冲突问题。

官方的解释:https://github.com/Saul-Mirone/milkdown/issues/294

官方示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import {
slashPlugin,
slash,
createDropdownItem,
defaultActions,
} from "@milkdown/plugin-slash";
import { themeManagerCtx, commandsCtx } from "@milkdown/core";

Editor.make().use(
slash.configure(slashPlugin, {
config: (ctx) => {
// Get default slash plugin items
const actions = defaultActions(ctx);

// Define a status builder
return ({ isTopLevel, content, parentNode }) => {
// You can only show something at root level
if (!isTopLevel) return null;

// Empty content ? Set your custom empty placeholder !
if (!content) {
return { placeholder: "Type / to use the slash commands..." };
}

// Define the placeholder & actions (dropdown items) you want to display depending on content
if (content.startsWith("/")) {
// Add some actions depending on your content's parent node
if (parentNode.type.name === "customNode") {
actions.push({
id: "custom",
dom: createDropdownItem(ctx.get(themeManagerCtx), "Custom", "h1"),
command: () =>
ctx.get(commandsCtx).call(/* Add custom command here */),
keyword: ["custom"],
typeName: "heading",
});
}

return content === "/"
? {
placeholder: "Type to filter...",
actions,
}
: {
actions: actions.filter(({ keyword }) =>
keyword.some((key) =>
key.includes(content.slice(1).toLocaleLowerCase())
)
),
};
}
};
},
})
);

我的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { commandsCtx, Ctx, schemaCtx, themeManagerCtx } from "@milkdown/core";
import {
createDropdownItem,
slash,
slashPlugin,
WrappedAction,
} from "@milkdown/plugin-slash";

// 自定义下拉菜单
const diyActions = (ctx: Ctx, input = "/"): WrappedAction[] => {
const { nodes } = ctx.get(schemaCtx);
const actions: Array<
WrappedAction & { keyword: string[]; typeName: string }
> = [
{
id: "h1",
dom: createDropdownItem(ctx.get(themeManagerCtx), "标签一", "h1"),
command: () => ctx.get(commandsCtx).call("TurnIntoHeading", 1),
keyword: ["h1", "large heading"],
typeName: "heading",
},
{
id: "h2",
dom: createDropdownItem(ctx.get(themeManagerCtx), "标签二", "h2"),
command: () => ctx.get(commandsCtx).call("TurnIntoHeading", 2),
keyword: ["h2", "medium heading"],
typeName: "heading",
},
{
id: "h3",
dom: createDropdownItem(ctx.get(themeManagerCtx), "标签三", "h3"),
command: () => ctx.get(commandsCtx).call("TurnIntoHeading", 3),
keyword: ["h3", "small heading"],
typeName: "heading",
},
{
id: "bulletList",
dom: createDropdownItem(
ctx.get(themeManagerCtx),
"无序列表",
"bulletList"
),
command: () => ctx.get(commandsCtx).call("WrapInBulletList"),
keyword: ["bullet list", "ul"],
typeName: "bullet_list",
},
{
id: "orderedList",
dom: createDropdownItem(
ctx.get(themeManagerCtx),
"有序列表",
"orderedList"
),
command: () => ctx.get(commandsCtx).call("WrapInOrderedList"),
keyword: ["ordered list", "ol"],
typeName: "ordered_list",
},
{
id: "taskList",
dom: createDropdownItem(ctx.get(themeManagerCtx), "任务列表", "taskList"),
command: () => ctx.get(commandsCtx).call("TurnIntoTaskList"),
keyword: ["task list", "task"],
typeName: "task_list_item",
},
{
id: "image",
dom: createDropdownItem(ctx.get(themeManagerCtx), "图片", "image"),
command: () => ctx.get(commandsCtx).call("InsertImage"),
keyword: ["image"],
typeName: "image",
},
{
id: "blockquote",
dom: createDropdownItem(ctx.get(themeManagerCtx), "引用", "quote"),
command: () => ctx.get(commandsCtx).call("WrapInBlockquote"),
keyword: ["quote", "blockquote"],
typeName: "blockquote",
},
{
id: "table",
dom: createDropdownItem(ctx.get(themeManagerCtx), "表格", "table"),
command: () => ctx.get(commandsCtx).call("InsertTable"),
keyword: ["table"],
typeName: "table",
},
{
id: "code",
dom: createDropdownItem(ctx.get(themeManagerCtx), "代码块", "code"),
command: () => ctx.get(commandsCtx).call("TurnIntoCodeFence"),
keyword: ["code"],
typeName: "fence",
},
{
id: "divider",
dom: createDropdownItem(ctx.get(themeManagerCtx), "分隔符", "divider"),
command: () => ctx.get(commandsCtx).call("InsertHr"),
keyword: ["divider", "hr"],
typeName: "hr",
},
{
id: "h4",
dom: createDropdownItem(ctx.get(themeManagerCtx), "测试", "h3"),
command: () => {
ctx.get(commandsCtx).call("TaskList");
},
keyword: ["h4", "Test"],
typeName: "heading",
},
];

const userInput = input.slice(1).toLocaleLowerCase();
return actions
.filter(
(action) =>
!!nodes[action.typeName] &&
action.keyword.some((keyword) => keyword.includes(userInput))
)
.map(({ keyword, typeName, ...action }) => action);
};

export const diySlash = slash.configure(slashPlugin, {
config: (ctx) => {
// 获取自定义的斜线命令
const actions: any = diyActions(ctx);
// Define a status builder
return ({ isTopLevel, content, parentNode }) => {
if (!isTopLevel) return null;
if (!content) {
return { placeholder: "输入/来使用斜杠命令..." };
}
if (content.startsWith("/")) {
return content === "/"
? {
placeholder: "请选择类型...",
actions: diyActions(ctx),
}
: {
actions: diyActions(ctx, content),
};
}
};
},
});

使用方式:

1
2
3
4
5
6
7
8
9
10
11
const { editor } = useEditor((root) => {
const editor: Editor undefined = Editor.make()
.config((ctx) => {
ctx.set(rootCtx, root);
})
// 自定义斜线命令
.use(diySlash)
// 自定义命令,这斜线命令使用了自定义的命令,所以这里也一定要加上。如何自定义指令
.use(taskListPlugin);
return editor;
});

我这样写的几个好处是:

  • 可以自定义列表的排序
  • 可以添加自定义指令,比如我的最后一个就是自定义的指令
  • 通俗易懂,比官方的看的更明白