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.
This commit is contained in:
2026-01-17 14:18:08 +08:00
parent 4de3dfdd8d
commit d9ab341223
381 changed files with 125356 additions and 0 deletions
@@ -0,0 +1,142 @@
# Announcing Telemetry Inspector
There's a lot of questions from community asking that how can they know what plugins are enabled.
From Milkdown@7.2, we've added telemetries for milkdown, it can be available by inspectors.
With this API, you can inspect editor inner status.
You can even use visualizer to visualize the data. We create a simple example on [our playground](/playground).
![Milkdown Inspector](/blogs/announcing-telemetry-inspector/milkdown-inspector.gif)
## Get Started
Inspector will be a top-level API in Milkdown. You can use it like this:
```ts
import { Editor } from "@milkdown/core";
import { Telemetry } from "@milkdown/ctx";
const editor = await Editor.make()
// Inspector is disabled by default considering performance. You need to enable it manually.
.enableInspector()
// ...
.create();
const telemetry: Telemetry[] = editor.inspect();
```
The `Telemetry` interface will have the following fields:
```ts
interface Telemetry {
// User defined information for the plugin.
metadata: Meta;
// The slices and their current value defined by the plugin.
injectedSlices: { name: string; value: unknown }[];
// The slices and their current value consumed by the plugin.
consumedSlices: { name: string; value: unknown }[];
// The timers and their duration defined by the plugin.
recordedTimers: { name: string; duration: number; status: TimerStatus }[];
// The timers and their duration consumed by the plugin.
// Generally, the plugin will wait for them.
waitTimers: { name: string; duration: number; status: TimerStatus }[];
}
type TimerStatus = "pending" | "resolved" | "rejected";
interface Meta {
displayName: string;
description?: string;
package: string;
group?: string;
additional?: Record<string, any>;
}
```
For every plugin, it'll have a telemetry if it has metadata declared.
With the data, you'll know the sequence of the plugins loaded, the slices and timers they defined and consumed.
For example:
```ts
[
{
metadata: {
displayName: "Config",
package: "@milkdown/core",
group: "System",
},
injectedSlices: [],
consumedSlices: [
/* ... */
],
recordedTimers: [
{
name: "ConfigReady",
duration: 3,
status: "resolved",
},
],
waitTimers: [],
},
{
metadata: {
displayName: "Init",
package: "@milkdown/core",
group: "System",
},
injectedSlices: [],
consumedSlices: [
/* ... */
],
recordedTimers: [
{
name: "InitReady",
duration: 5,
status: "resolved",
},
],
waitTimers: [
{
name: "ConfigReady",
duration: 5,
status: "resolved",
},
],
},
];
```
From above information, we can know that the `Init` plugin wait for `Config` plugin to be ready.
We can build a sequence diagram from the data.
![Timer Sequence](/blogs/announcing-telemetry-inspector/timer-sequence.gif)
## Add Metadata for Plugin
For plugin maintainers, you can add metadata to your plugin to make it more friendly to the inspector.
```ts
import { MilkdownPlugin } from "@milkdown/ctx";
const yourMilkdownPlugin: MilkdownPlugin = () => {
/* your implementation */
};
yourMilkdownPlugin.metadata = {
displayName: "Your Plugin",
package: "your-plugin-package",
description: "Your plugin description",
group: "If you have a lot of plugins in your package, you can group them.",
addtitional: {
/* You can add any additional information here. */
version: "1.0.0",
authror: "Mike",
},
};
```
With metadata, your plugin will report telemetry correctly to the inspector.
@@ -0,0 +1,247 @@
# Build Your Own Milkdown Copilot
OpenAI introduced ChatGPT in 2020, which is a chatbot that can generate natural language responses to user input.
Which brings us a new way to interact with devices and applications.
Nowadays, there are more and more tools that are powered by AI. Such as Notion, GitHub and even Microsoft 365.
Since OpenAI also released the [API](https://openai.com/blog/openai-api) of it. And Milkdown is composed by plugins.
I think it's possible to build a Milkdown Copilot Plugin that can help you write documents. So I did it.
Let's see the result.
![Milkdown Copilot](/blogs/build-your-own-milkdown-copilot/milkdown-copilot.gif)
Looks cool, right? But how does it work? I'll explain it in the following sections.
## Prepare a Backend
**Before we start, you need to have a OpenAI API Key.** You'll need to get one [here](https://platform.openai.com/account/api-keys).
I'll not explain how to get it. You can find the details in their [official docs](https://platform.openai.com/).
I'll use Node.js to build the backend. You can use any language you like.
The backend is very simple. It just calls the OpenAI API and returns the result.
```ts
import { Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
// Get your API key from env variable
apiKey: process.env.OPENAPI_KEY,
});
const openai = new OpenAIApi(configuration);
export const handler = async (req, res, next) => {
if (req.path === "/api/copilot" && req.method === "POST") {
const buffers = [];
// Get the body of the request.
const body = JSON.parse(req.body);
// Get prompt from the body.
const { prompt } = body;
const completion = await openai.createCompletion({
// Pick a model you like
model: "text-davinci-003",
prompt,
});
const hint = completion.data.choices[0].text;
return res.end(JSON.stringify({ hint }));
}
next();
return;
};
```
We watch the `/api/copilot` route and call the OpenAI API when we receive a POST request.
The post request should contain a `prompt` field which is the text that we want to complete.
To call our API, we just need one single helper in browser environment:
```ts
async function fetchAIHint(prompt: string) {
const data: Record<string, string> = { prompt };
const response = await fetch("/api/copilot", {
method: "POST",
body: JSON.stringify(data),
});
const res = (await response.json()) as { hint: string };
return res.hint;
}
```
## Build a Milkdown Plugin
Now let's focus on the Milkdown Copilot Plugin.
Basically I want to implement two things:
1. When the user types `<Enter>` or `<Space>`, they will get a hint from the copilot.
2. When the user types `<Tab>`, they will apply the content from the hint to the editor.
### Overview
To build a bridge between the copilot and the editor,
we can build a prosemirror plugin and use the `onKeyDown` hook to listen to the keydown event.
```ts
function keyDownHandler(ctx: Ctx, event: Event) {
if (event.key === "Enter" || event.code === "Space") {
getHint(ctx);
return;
}
if (event.key === "Tab") {
// prevent the browser from focusing on the next element.
event.preventDefault();
applyHint(ctx);
return;
}
hideHint(ctx);
}
```
When the user types `<Enter>` or `<Space>`, we will call the `getHint` function to get a hint from the copilot.
And when the user types `<Tab>`, we will call the `applyHint` function to apply the hint to the editor.
If user types other keys, we will hide the hint.
And we also need a component to render the hint. Here I choose to use a simple [widget decoration in prosemirror](https://prosemirror.net/docs/ref/#view.Decoration^widget).
```ts
function renderHint(message: string) {
const dom = document.createElement("pre");
dom.className = "copilot-hint";
dom.innerHTML = message;
return dom;
}
```
So our component looks like:
```ts
import { Plugin, PluginKey } from "@milkdown/prose/state";
import { Decoration, DecorationSet } from "@milkdown/prose/view";
import { $prose } from "@milkdown/utils";
const initialState = {
deco: DecorationSet.empty,
message: "",
};
export const copilotPluginKey = new PluginKey("milkdown-copilot");
export const copilotPlugin = $prose(
(ctx) =>
new Plugin({
key: copilotPluginKey,
props: {
handleKeyDwon(view, event) {
keydownHandler(ctx, event);
},
decorations(state) {
return copilotPluginKey.getState(state).deco;
},
},
state: {
init() {
return { ...initialState };
},
apply(tr, value, _prevState, state) {
const message = tr.getMeta(copilotPluginKey);
if (typeof message !== "string") return value;
if (message.length === 0) {
return { ...initialState };
}
const { to } = tr.selection;
const widget = Decoration.widget(to + 1, () => renderHint(message));
return {
deco: DecorationSet.create(state.doc, [widget]),
message,
};
},
},
}),
);
```
### Get Hint
To get a hint from the copilot, we need to get the text before the cursor.
```ts
function getHint(ctx: Ctx) {
const view = ctx.get(editorViewCtx);
const { state } = view;
const { tr, schema } = state;
const { from } = tr.selection;
const slice = tr.doc.slice(0, from);
const serializer = ctx.get(serializerCtx);
const doc = schema.topNodeType.createAndFill(undefined, slice.content);
if (!doc) return;
const markdown = serializer(doc);
fetchAIHint(markdown).then((hint) => {
const tr = view.state.tr;
view.dispatch(tr.setMeta(copilotPluginKey, hint));
});
}
```
1. First of all, we get the `selection` from the `state` of the editor.
2. Then we get a `slice` of the document from the start to the cursor.
3. Then we use the `serializer` to convert the slice to markdown.
4. After that, we call the `fetchAIHint` function to get a hint from the copilot.
5. Finally, we dispatch a transaction with the hint message we get to update the state of the editor.
### Hide Hint
To hide the hint, we just need to dispatch a transaction with an empty message.
```ts
function hideHint(ctx: Ctx) {
const view = ctx.get(editorViewCtx);
const { state } = view;
const { tr } = state;
view.dispatch(tr.setMeta(copilotPluginKey, ""));
}
```
### Apply Hint
Since we pass markdown to the OpenAI API. It may return a markdown snippet.
So, before we apply the hint to the editor, we need to convert the markdown snippet to prosemirror node.
```ts
function applyHint(ctx: Ctx) {
const view = ctx.get(editorViewCtx);
const { state } = view;
const { tr, schema } = state;
const { message } = copilotPluginKey.getState(state);
const parser = ctx.get(parserCtx);
const slice = parser(message);
const dom = DOMSerializer.fromSchema(schema).serializeFragment(slice.content);
const node = DOMParser.fromSchema(schema).parseSlice(dom);
// Reset the hint since it's applied
tr.setMeta(copilotPluginKey, "")
// Replace the selection with the hint
.replaceSelection(node);
view.dispatch(tr);
}
```
1. First of all, we get the hint message from the state of the editor.
2. Then we use the `parser` to convert the markdown snippet to prosemirror node.
3. Finally, we dispatch a transaction to replace the selection with the hint.
## Conclusion
In this article, we have built a really simple Copilot plugin for Milkdown.
The plugin is not perfect, but it's a good start to help you build your own.
The source code is available on [Milkdown/examples/vanilla-openapi](https://github.com/Milkdown/examples/tree/main/vanilla-openai).
I hope it can give you some inspiration.
@@ -0,0 +1,151 @@
# Introducing Milkdown@7
It's been almost one year since the release of [milkdown](https://milkdown.dev) V6.
It helped a lot of users to build their own markdown based applications.
It has 13k downloads per month and I feel so grateful that users like that.
However, we noticed that there're some problems cannot be resolved if we don't make a new major version.
What big changes did we made? I'll introduce them to you in this blog.
## TL;DR
- The editor becomes a first-class headless component.
- Factory plugins are fully replaced by **composable plugins**.
- Runtime plugin toggling is supported.
- Universal widget plugins.
- Better Vue and React support.
- API documentation is provided.
## Why Headless?
In the past, milkdown had a lot of internal styles to make sure the editor can work out of box and the themes are easy to create.
However, I found it limits the users to design their own editor.
Even worse, if you have an well designed application,
it is really hard to keep the style of the milkdown editor same with the rest of the application.
You'll need to override lots of styles everywhere.
It stops a log of users from using milkdown.
If we think about why users need an editor,
the most important thing is always the functionality of the editor.
Users just want a component that can provide smooth editing experience.
Style will always be the second thing.
So, why not remove all the internal styles and make the editor a headless component?
The users can easily integrate the editor into their own application.
They can use their own styles and even use their own components to render the editor.
We just care about the functionality of the editor. Make sure it works well.
## Composable Plugins
Although the composable plugins have been existed in milkdown for a long time,
we use factory plugins to create most of the official plugins in V6.
But, the problem is that factory plugins limit the possibility of the plugins.
The factory plugins handle a bunch of complex logic and it is hard to extend.
So for users who want to create a plugin in a easy way, they must follow the factory plugin's way.
```ts
const nodePlugin = createPlugin(() => ({
id: 'node',
schema: someSchema,
inputRules: someInputRules
commands: someCommands
}))
```
See? You can define a lot of things inside the factory plugin.
But if you want to use some part of them in another plugin, it's really hard to do that.
However, the milkdown's plugin system is designed to be flexible and composable.
We want to let users to control the data flow entirely.
So, we decided to remove all the factory plugins and use composable plugins to replace them.
The composable plugins can keep the atomicity of the plugins and make the plugin system more flexible.
They also make the plugin system easier to maintain.
```ts
const nodeSchema = $node("node", someSchema);
const nodeInputRules = $inputRules(someInputRules);
const nodeCommands = $commands(someCommands);
```
If you want to reuse them, it also will be very easy.
```ts
const anotherCommand = $commands(() => {
return setBlockType(nodeSchema.type());
});
```
## Runtime Plugin Toggling
In the past, once you register a plugin, you cannot remove it.
In V7, we support runtime plugin toggling by providing two new API: `editor.remove` and `editor.removeConfig`.
They can let users remove the plugins and configs at runtime.
```ts
import { Editor } from "@milkdown/core";
import { someMilkdownPlugin } from "some-milkdown-plugin";
const editor = await Editor.config(configForPlugin)
.use(someMilkdownPlugin)
.create();
// remove plugin
await editor.remove(someMilkdownPlugin);
// remove config
editor.removeConfig(configForPlugin);
// add another plugin
editor.use(anotherMilkdownPlugin);
// Recreate the editor to apply changes.
await editor.create();
```
Also, if you call the `editor.create` method after the editor is created,
it will recreate the editor and apply all the changes.
## Universal Widget Plugins
We have 4 official widget plugins in V6: _slash_, _tooltip_, _block_ and _menu_.
They are all well designed and easy to use.
But if you want to customize them, what you can do is really limited.
Also, it's hard to reuse their logic even if you want to create something similar to them.
For example, if you want to create a mention plugin which will show a list of users when you type `@`,
you need to create a new plugin from scratch.
So, in V7, we make _slash_, _tooltip_ and _block_ plugins universal.
You can use them to build you features easily.
For example, if you want to create a mention plugin, you can use the new slash plugin to do that.
Another example is that you can also create tooltips for different types of nodes.
Display a tooltip with input when you focus on an image node, or display a tooltip with buttons when you select some text.
What about the _menu_ plugin? We removed it because we think it's easy to create a menu plugin by yourself.
We've already done that in the [official playground](https://milkdown.dev/playground).
And, trust me, [it won't need much code](https://github.com/milkdown/website/blob/main/src/component/Playground/Milkdown/index.tsx#L57).
## Better Vue and React Support
Thanks to the [Saul-Mirone/prosemirror-adapter project](https://github.com/Saul-Mirone/prosemirror-adapter).
In milkdown V7. We allow users to use vue and react to render lots of parts of the editor.
For example, you can use them to render your own code block, drag handle or even small icons.
- React Example: [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/Milkdown/examples/tree/main/react-custom-component)
- Vue Example: [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/Milkdown/examples/tree/main/vue-custom-component)
## API Documentation
What's the hardest thing to do when maintaining an open source project?
Keep the documentation up to date.
Thanks to the [marijnh/builddocs project](https://github.com/marijnh/builddocs),
we can generate the API documentation automatically from the source code.
We also redesigned the documentation website, provide a more powerful playground and lots of examples.
@@ -0,0 +1,172 @@
# Understanding Headless Slash Plugin
In the old Milkdown versions. The slash plugin can be used to display a list of commands when users type `/` in the editor.
It provides a way to insert nodes and commands into the editor, and it's really easy to use.
![legacy slash plugin](/blogs/understanding-headless-slash-plugin/legacy-slash-plugin.png)
However, it's hard to extend the slash plugin to support more commands, or if you want to change the UI of the slash plugin, you have to rewrite the whole plugin.
But, write a new plugin is always a hard work. You have to understand a lot of context and APIs of both ProseMirror and Milkdown.
## User Story
So, why don't we provide the slash plugin as a headless plugin?
In most cases, developers just want to make sure that when users type a special character, a dropdown menu will be displayed.
But the trigger character and the UI of the dropdown menu are different in different cases.
For example:
- When user type `/`, the menu contains a list of **commands**.
- When user type `:`, the menu contains a list of **emoji**.
- When user type `@`, the menu contains a list of **users**.
That's the story behind the headless slash plugin. We provide the plugin to solve a single problem: **display a dropdown menu when users input satisfy a condition**.
## How to use
In the new slash plugin, you'll need to control when to display the dropdown menu by yourself.
And you'll also need to provide the UI of the dropdown menu.
So, you'll need to create a `SlashProvider` instance.
```ts
import { slashPlugin, SlashProvider } from "@milkdown/plugin-slash";
const slashProvider = new SlashProvider({
content: YourDropdownUI,
shouldShow(this: SlashProvider, view: EditorView) {
const currentText = this.getContent(view);
if (currentText === "") {
return false;
}
// Display the menu if the last character is `/`.
if (currentText.endsWith("/")) {
return true;
}
return false;
},
});
```
Then, you can use the slash provider in your plugin view.
```ts
import { EditorState } from "@milkdown/prose/state";
import { EditorView, PluginView } from "@milkdown/prose/view";
function yourSlashView(): PluginView {
return {
update: (view: EditorView, prevState: EditorState) => {
slashProvider.update(view, prevState);
},
destroy: () => {
slashProvider.destroy();
},
};
}
```
Last, you'll need to add the slash plugin to your editor.
```ts
import { Editor } from "@milkdown/core";
import { slashFactory } from "@milkdown/plugin-slash";
const slash = slashFactory("my-slash");
Editor.make()
.config((ctx) => {
ctx.set(slash.key, {
view: slashPluginView,
});
})
.use(slash)
.create();
```
## Use with Prosemirror Adapter
If you're using milkdown with UI frameworks like React,
I recommend you to use the [Prosemirror Adapter](https://github.com/Saul-Mirone/prosemirror-adapter).
It can help you build prosemirror UI components with your favorite UI framework.
For example, if you're using React:
```tsx
import { SlashProvider } from "@milkdown/plugin-slash";
import { useInstance } from "@milkdown/react";
import { usePluginViewContext } from "@prosemirror-adapter/react";
export const DropdownMenu = () => {
const { view, prevState } = usePluginViewContext();
const slashProvider = useRef<SlashProvider>();
const divRef = useRef<HTMLDivElement>(null);
const [loading] = useInstance();
useEffect(() => {
if (!ref.current || loading) return;
slashProvider.current ??= new SlashProvider({
content: divRef.current,
// ...
});
return () => {
slashProvider.current?.destroy();
slashProvider.current = undefined;
};
}, [loading, root, setOpened, setSearch, setSelected]);
useEffect(() => {
slashProvider.current?.update(view, prevState);
});
// Add a wrapper `div` to hide the dropdown menu when initializing.
return (
<div className="hidden">
<div role="tooltip" ref={divRef}>
<h1>Hi! I'm a dropdown menu.</h1>
</div>
</div>
);
};
```
And in your editor component:
```ts
import { usePluginViewFactory } from "@prosemirror-adapter/react";
export const YourEditor = () => {
const pluginViewFactory = usePluginViewFactory();
useEditor((editor) => {
return Editor.make()
.config((ctx) => {
ctx.set(slash.key, {
view: pluginViewFactory({
component: DopdownMenu,
}),
});
})
.use(slash);
});
// ...
};
```
## Real World Example
In [milkdown playground](/playground), you can type `/` to display a dropdown menu.
![command dropdown](/blogs/understanding-headless-slash-plugin/command-dropdown.png)
You can also type `:(\S)+` (for example: `:mil`) to display a list of emojis.
![emoji dropdown](/blogs/understanding-headless-slash-plugin/emoji-dropdown.png)
You can find the source code of them in [Milkdown website](https://github.com/Milkdown/website).
I hope you enjoy the new slash plugin.