Files
llm-in-text/milkdown-docs/plugin/plugins-101.md
ydy0615 d9ab341223 Add documentation for using Milkdown with various frameworks
- Created a new document for using components in Milkdown.
- Added a guide for using plugins in Milkdown, including toggling plugins programmatically and listing official plugins.
- Introduced a recipe for integrating Milkdown with Angular, including installation steps and component creation.
- Added a recipe for using Milkdown with Next.js, detailing installation and component setup.
- Created a guide for integrating Milkdown with NuxtJS, including installation and component creation.
- Added a comprehensive guide for using Milkdown with React, covering both Crepe and core Milkdown usage.
- Introduced a recipe for SolidJS integration with Milkdown, including installation and component creation.
- Added a guide for using Milkdown with Svelte, detailing installation and component setup.
- Created a comprehensive guide for integrating Milkdown with Vue, covering both Crepe and core Milkdown usage.
- Added a recipe for using Milkdown with Vue2, including installation and component creation.
2026-01-17 14:18:08 +08:00

5.5 KiB

Plugins 101

In this section we will show you the basic information of the plugin. In most cases, you will not need to write plugins without helpers. But it can help you understand the plugin system and what happens under the hood.

Structure Overview

Generally speaking, a plugin will have following structure:

import { MilkdownPlugin } from "@milkdown/kit/ctx";

const myPlugin: MilkdownPlugin = (ctx) => {
  // #1 prepare plugin
  return async () => {
    // #2 run plugin
    return async () => {
      // #3 clean up plugin
    };
  };
};

Each plugin is composed by three parts:

  1. Prepare: this part will be executed when plugin is registered in milkdown by .use method.
  2. Run: this part will be executed when plugin is actually loaded.
  3. Post: this part will be executed when plugin is removed by .remove method or editor is destroyed.

Timer

Timer can be used to decide when to load the current plugin and how current plugin can influence other plugin's loading status.

You can use ctx.wait to wait a timer to finish.

import { MilkdownPlugin, Complete } from "@milkdown/kit/core";

const myPlugin: MilkdownPlugin = (ctx) => {
  return async () => {
    const start = Date.now();

    await ctx.wait(Complete);

    const end = Date.now();

    console.log("Milkdown load duration: ", end - start);
  };
};

You can also create your own timer and influence other plugins load time. For example, let's create a plugin that will fetch markdown content from remote server as editor's default value.

import {
  MilkdownPlugin,
  editorStateTimerCtx,
  defaultValueCtx,
  createTimer,
} from "@milkdown/kit/core";

const RemoteTimer = createTimer("RemoteTimer");

const remotePlugin: MilkdownPlugin = (ctx) => {
  // register timer
  ctx.record(RemoteTimer);

  return async () => {
    // the editorState plugin will wait for this timer to finish before initialize editor state.
    ctx.update(editorStateTimerCtx, (timers) => timers.concat(RemoteTimer));

    const defaultMarkdown = await fetchMarkdownAPI();
    ctx.set(defaultValueCtx, defaultMarkdown);

    // mark timer as complete
    ctx.done(RemoteTimer);

    return async () => {
      await SomeAPI();

      // remove timer when plugin is removed
      ctx.clearTimer(RemoteTimer);
    };
  };
};

It has following steps:

  1. We use createTimer to create a timer, and use pre.record to register it into milkdown.
  2. We update editorStateTimerCtx to tell the internal editorState plugin that before initialize editor state, it should wait our remote fetch process finished.
  3. After we get value from fetchMarkdownAPI, we set it as defaultValue and use ctx.done to mark a timer as complete.

Ctx

We have used ctx several times in the above example, now we can try to understand what it is.

Ctx is a data container which is shared in the entire editor instance. It's composed by a lot of slices. Every slice has a unique key and a value. You can change the value of a slice by ctx.set and ctx.update. And you can get the value of a slice by ctx.get with the slice key or name. Last but not least, you can remove a slice by post.remove.

import { MilkdownPlugin, createSlice } from "@milkdown/kit/ctx";

const counterCtx = createSlice(0, "counter");

const counterPlugin: MilkdownPlugin = (ctx) => {
  ctx.inject(counterCtx);

  return () => {
    // count is 0
    const count0 = ctx.get(counterCtx);

    // set count to 1
    ctx.set(counterCtx, 1);

    // now count is 1
    const count1 = ctx.get(counterCtx);

    // set count to n + 2
    ctx.update(counterCtx, (prev) => prev + 2);

    // now count is 3
    const count2 = ctx.get(counterCtx);
    // we can also get value by the slice name
    const count3 = ctx.get("counter");

    return () => {
      // remove the slice
      ctx.remove(counterCtx);
    };
  };
};

We can use createSlice to create a ctx, and use pre.inject to inject the ctx into the editor.

And when plugin processing, ctx.get can get the value of a ctx, ctx.set can set the value of a ctx, and ctx.update can update a ctx using callback function.

So, we can use ctx combine with timer to decide when should a plugin be processed.

import {
  MilkdownPlugin,
  SchemaReady,
  Timer,
  createSlice,
} from "@milkdown/kit/core";

const examplePluginTimersCtx = createSlice<Timer[]>([], "example-timer");

const examplePlugin: MilkdownPlugin = (ctx) => {
  ctx.inject(examplePluginTimersCtx, [SchemaReady]);
  return async () => {
    await Promise.all(
      ctx.get(examplePluginTimersCtx).map((timer) => ctx.wait(timer)),
    );
    // or we can use a simplified syntax sugar
    await ctx.waitTimers(examplePluginTimersCtx);

    // do something
  };
};

With this pattern, if other plugins want to delay the process of examplePlugin, all they need to do is just add a timer into examplePluginTimersCtx with ctx.update.

Summary

Now let's go back to the plugin structure. Since we have the knowledge of timer and ctx, we can understand what we should do in each part of a plugin.

  1. In prepare stage of the plugin, we can use ctx.record to register a timer, and use ctx.inject to inject a slice.
  2. In run stage of the plugin, we can use ctx.wait to wait a timer to finish, and use ctx.get to get the value of a slice. We can also change values of slices by ctx.set and ctx.update. And we can use ctx.done to mark a timer as complete.
  3. In post stage of the plugin, we can use ctx.clearTimer to clear a timer, and use ctx.remove to remove a slice.