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

官方示例:

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);

我的示例:

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));
};

使用方式:

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

官方示例:

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())
                  )
                ),
              };
        }
      };
    },
  })
);

我的示例:

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),
            };
      }
    };
  },
});

使用方式:

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

我这样写的几个好处是:

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