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:
@@ -0,0 +1,199 @@
|
||||
# Architecture Overview
|
||||
|
||||
Milkdown is built with a modular, layered architecture that provides flexibility and extensibility. This document explains the core architectural concepts and how they work together.
|
||||
|
||||

|
||||
|
||||
## Core Architecture Layers
|
||||
|
||||
Milkdown's architecture is built upon four distinct layers, each providing specific functionality and extensibility:
|
||||
|
||||
### 🥛 Core Layer
|
||||
|
||||
The foundation of Milkdown that provides:
|
||||
|
||||
- Plugin loading and management system
|
||||
- Core editor concepts and interfaces
|
||||
- Base document model integration
|
||||
- Essential utilities and helpers
|
||||
|
||||
### 🧇 Plugin Layer
|
||||
|
||||
A comprehensive collection of modular plugins that extend the editor's functionality:
|
||||
|
||||
- Syntax plugins (Markdown parsing, GFM, etc.)
|
||||
- UI plugins (toolbar, menu, etc.)
|
||||
- Feature plugins (image upload, table, etc.)
|
||||
- Utility plugins (history, clipboard, etc.)
|
||||
|
||||
### 🍮 Component Layer
|
||||
|
||||
Headless UI components that serve as building blocks:
|
||||
|
||||
- Toolbar components
|
||||
- Slash menu components
|
||||
- Table components
|
||||
|
||||
### 🍰 Editor Layer
|
||||
|
||||
Ready-to-use, user-friendly editors:
|
||||
|
||||
- Crepe editor
|
||||
- Custom editor implementations
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
This layered approach provides several key benefits:
|
||||
|
||||
1. **Modularity**: Each layer can be used independently
|
||||
2. **Flexibility**: Mix and match components as needed
|
||||
3. **Extensibility**: Create custom implementations at any layer
|
||||
4. **Maintainability**: Clear separation of concerns
|
||||
5. **Reusability**: Components can be shared across implementations
|
||||
|
||||
## Markdown Transformation
|
||||
|
||||

|
||||
|
||||
Milkdown's transformation system handles the conversion between Markdown and the editor's internal document model:
|
||||
|
||||
### Parsing Process
|
||||
|
||||
1. Markdown text → Remark AST
|
||||
2. Remark AST → ProseMirror Schema
|
||||
3. Schema → ProseMirror Document
|
||||
|
||||
### Serialization Process
|
||||
|
||||
1. ProseMirror Document → ProseMirror Schema
|
||||
2. Schema → Remark AST
|
||||
3. Remark AST → Markdown text
|
||||
|
||||
This transformation system ensures:
|
||||
|
||||
- Accurate Markdown parsing
|
||||
- Consistent document structure
|
||||
- Reliable serialization
|
||||
- Extensible transformation pipeline
|
||||
|
||||
## Context System
|
||||
|
||||
The Context System is a powerful state management and dependency coordination system that enables plugins to work together seamlessly.
|
||||
|
||||

|
||||
|
||||
### Core Concepts
|
||||
|
||||
#### 1. Context (Ctx)
|
||||
|
||||
The main interface for plugins to interact with the system:
|
||||
|
||||
```typescript
|
||||
interface Ctx {
|
||||
get: <T>(slice: Slice<T>) => T;
|
||||
set: <T>(slice: Slice<T>, value: T) => void;
|
||||
wait: (timer: Timer) => Promise<void>;
|
||||
done: (timer: Timer) => void;
|
||||
inject: <T>(slice: Slice<T>, value: T) => void;
|
||||
remove: <T>(slice: Slice<T>) => void;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Slices
|
||||
|
||||
State containers that can be shared between plugins:
|
||||
|
||||
```typescript
|
||||
// Create a slice with initial value and name
|
||||
const themeSlice = createSlice("light", "theme");
|
||||
|
||||
// Use in a plugin
|
||||
const themePlugin: MilkdownPlugin = (ctx) => {
|
||||
return () => {
|
||||
// Read current theme
|
||||
const theme = ctx.get(themeSlice);
|
||||
|
||||
// Update theme
|
||||
ctx.set(themeSlice, "dark");
|
||||
|
||||
// React to theme changes
|
||||
ctx.watch(themeSlice, (newTheme) => {
|
||||
// Handle theme change
|
||||
});
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Timers
|
||||
|
||||
Dependency management system for plugin coordination:
|
||||
|
||||
```typescript
|
||||
// Define a timer
|
||||
const dataReady = createTimer("DataReady");
|
||||
|
||||
// Use in a plugin
|
||||
const dataPlugin: MilkdownPlugin = (ctx) => {
|
||||
ctx.record(dataReady);
|
||||
|
||||
return async () => {
|
||||
// Wait for dependencies
|
||||
await ctx.wait(SchemaReady);
|
||||
|
||||
// Do work
|
||||
// ...
|
||||
|
||||
// Mark as ready
|
||||
ctx.done(dataReady);
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
Plugins follow a consistent lifecycle pattern:
|
||||
|
||||
```typescript
|
||||
const examplePlugin: MilkdownPlugin = (ctx) => {
|
||||
// 1. Setup Phase
|
||||
ctx.inject(mySlice, defaultValue);
|
||||
ctx.record(myTimer);
|
||||
|
||||
return async () => {
|
||||
// 2. Initialization Phase
|
||||
await ctx.wait(RequiredTimer);
|
||||
|
||||
// 3. Runtime Phase
|
||||
const value = ctx.get(mySlice);
|
||||
ctx.set(mySlice, newValue);
|
||||
|
||||
// 4. Cleanup Phase
|
||||
return () => {
|
||||
ctx.remove(mySlice);
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **State Management**
|
||||
- Use slices for shared state
|
||||
- Keep state minimal and focused
|
||||
- Watch for state changes when needed
|
||||
|
||||
2. **Dependency Management**
|
||||
- Use timers for coordination
|
||||
- Wait for required dependencies
|
||||
- Mark completion appropriately
|
||||
|
||||
3. **Plugin Organization**
|
||||
- Follow the lifecycle pattern
|
||||
- Clean up resources properly
|
||||
- Document dependencies clearly
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Start to [use Crepe editor](/docs/guide/using-crepe)
|
||||
- Learn more about [writing plugins](/docs/plugin/plugins-101)
|
||||
- Explore [available plugins](/docs/plugin/using-plugins)
|
||||
@@ -0,0 +1,160 @@
|
||||
# Code Highlighting
|
||||
|
||||
Milkdown supports syntax highlighting for code blocks through the `@milkdown/plugin-highlight` plugin. This plugin provides several options for highlighting code with different syntax highlighters.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @milkdown/plugin-highlight
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The highlight plugin requires a parser to be configured. Here's a basic example using the Shiki parser:
|
||||
|
||||
```typescript
|
||||
import { Editor } from "@milkdown/core";
|
||||
import { commonmark } from "@milkdown/preset-commonmark";
|
||||
import { highlight, highlightPluginConfig } from "@milkdown/plugin-highlight";
|
||||
import { createParser } from "@milkdown/plugin-highlight/shiki";
|
||||
|
||||
const editor = Editor.make()
|
||||
.config(async (ctx) => {
|
||||
const parser = await createParser({
|
||||
theme: "github-light",
|
||||
langs: ["javascript", "typescript", "python", "html", "css"],
|
||||
});
|
||||
ctx.set(highlightPluginConfig.key, { parser });
|
||||
})
|
||||
.use(commonmark)
|
||||
.use(highlight)
|
||||
.create();
|
||||
```
|
||||
|
||||
## Available Parsers
|
||||
|
||||
The plugin supports multiple syntax highlighting libraries:
|
||||
|
||||
### Shiki
|
||||
|
||||
Provides high-quality syntax highlighting with VS Code themes. Learn more at [Shiki](https://shiki.style/):
|
||||
|
||||
```typescript
|
||||
import { createParser } from "@milkdown/plugin-highlight/shiki";
|
||||
|
||||
const parser = await createParser({
|
||||
theme: "github-light",
|
||||
langs: ["javascript", "typescript", "python"],
|
||||
});
|
||||
ctx.set(highlightPluginConfig.key, { parser });
|
||||
```
|
||||
|
||||
### Lowlight
|
||||
|
||||
Based on [highlight.js](https://highlightjs.org/), supports many languages:
|
||||
|
||||
```typescript
|
||||
import { createParser } from "@milkdown/plugin-highlight/lowlight";
|
||||
import { common } from "lowlight";
|
||||
|
||||
const parser = createParser({ common });
|
||||
ctx.set(highlightPluginConfig.key, { parser });
|
||||
```
|
||||
|
||||
Learn more about Lowlight at [lowlight](https://github.com/wooorm/lowlight).
|
||||
|
||||
### Refractor
|
||||
|
||||
Based on [Prism.js](https://prismjs.com/):
|
||||
|
||||
```typescript
|
||||
import { createParser } from "@milkdown/plugin-highlight/refractor";
|
||||
import { refractor } from "refractor";
|
||||
|
||||
const parser = createParser({ refractor });
|
||||
ctx.set(highlightPluginConfig.key, { parser });
|
||||
```
|
||||
|
||||
Learn more about Refractor at [refractor](https://github.com/wooorm/refractor).
|
||||
|
||||
### Sugar High
|
||||
|
||||
A lightweight and fast syntax highlighter. Learn more at [Sugar High](https://github.com/huozhi/sugar-high):
|
||||
|
||||
```typescript
|
||||
import { createParser } from "@milkdown/plugin-highlight/sugar-high";
|
||||
|
||||
const parser = createParser();
|
||||
ctx.set(highlightPluginConfig.key, { parser });
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The highlighted code will have CSS classes applied based on the chosen parser. You'll need to include appropriate CSS to style the highlighted tokens.
|
||||
|
||||
### Sugar High Classes
|
||||
|
||||
Sugar High uses classes like:
|
||||
|
||||
- `sh__token--identifier`
|
||||
- `sh__token--string`
|
||||
- `sh__token--keyword`
|
||||
- `sh__token--sign`
|
||||
- `sh__token--property`
|
||||
|
||||
You can style these using CSS variables:
|
||||
|
||||
```css
|
||||
.sh__token--identifier {
|
||||
color: var(--sh-identifier);
|
||||
}
|
||||
.sh__token--string {
|
||||
color: var(--sh-string);
|
||||
}
|
||||
.sh__token--keyword {
|
||||
color: var(--sh-keyword);
|
||||
}
|
||||
```
|
||||
|
||||
### Other Parsers
|
||||
|
||||
For Lowlight, Refractor, and Shiki, refer to their respective documentation for styling information.
|
||||
|
||||
## Example
|
||||
|
||||
Here's a complete example with Shiki:
|
||||
|
||||
```typescript
|
||||
import { Editor } from "@milkdown/core";
|
||||
import { commonmark } from "@milkdown/preset-commonmark";
|
||||
import { highlight, highlightPluginConfig } from "@milkdown/plugin-highlight";
|
||||
import { createParser } from "@milkdown/plugin-highlight/shiki";
|
||||
|
||||
async function createHighlightedEditor() {
|
||||
const parser = await createParser({
|
||||
theme: "github-light",
|
||||
langs: ["javascript", "typescript", "python", "html", "css", "json"],
|
||||
});
|
||||
|
||||
const editor = Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.set(highlightPluginConfig.key, { parser });
|
||||
})
|
||||
.use(commonmark)
|
||||
.use(highlight);
|
||||
|
||||
await editor.create();
|
||||
return editor;
|
||||
}
|
||||
```
|
||||
|
||||
With this setup, your code blocks will be automatically highlighted:
|
||||
|
||||
````markdown
|
||||
```javascript
|
||||
console.log("Hello, world!");
|
||||
const greeting = (name) => `Hello, ${name}!`;
|
||||
```
|
||||
````
|
||||
|
||||
The code above will render with syntax highlighting applied to keywords, strings, and other language constructs.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Collaborative Editing
|
||||
|
||||
Milkdown supports collaborative editing powered by [Y.js](https://docs.yjs.dev/).
|
||||
We provide the [@milkdown/plugin-collab](/docs/api/plugin-collab) plugin to help you use milkdown with yjs easily.
|
||||
This plugin includes basic collaborative editing features like:
|
||||
|
||||
- Sync between clients.
|
||||
- Remote cursor support.
|
||||
- Undo/Redo support.
|
||||
|
||||
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/vanilla-collab"}
|
||||
|
||||
## Configure Plugin
|
||||
|
||||
First you need to install the plugin and yjs through npm:
|
||||
|
||||
```bash
|
||||
npm install @milkdown/plugin-collab
|
||||
|
||||
npm install yjs y-protocols y-prosemirror
|
||||
```
|
||||
|
||||
And you also need to choose a [provider for yjs](https://docs.yjs.dev/ecosystem/connection-provider), here we use [y-websocket](https://docs.yjs.dev/ecosystem/connection-provider/y-websocket) as an example.
|
||||
|
||||
After the installation, you can configure your editor:
|
||||
|
||||
```typescript
|
||||
// ...import other plugins
|
||||
import { collab, collabServiceCtx } from "@milkdown/plugin-collab";
|
||||
|
||||
async function setup() {
|
||||
const editor = await Editor.make()
|
||||
.config(nord)
|
||||
.use(commonmark)
|
||||
.use(collab)
|
||||
.create();
|
||||
|
||||
const doc = new Doc();
|
||||
const wsProvider = new WebsocketProvider("<YOUR_WS_HOST>", "milkdown", doc);
|
||||
|
||||
editor.action((ctx) => {
|
||||
const collabService = ctx.get(collabServiceCtx);
|
||||
|
||||
collabService
|
||||
// bind doc and awareness
|
||||
.bindDoc(doc)
|
||||
.setAwareness(wsProvider.awareness)
|
||||
// connect yjs with milkdown
|
||||
.connect();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Now your editor can support collaborative editing. Isn't it easy?
|
||||
|
||||
## Connect and Disconnect
|
||||
|
||||
You may want to control the connect status of the editor manually.
|
||||
|
||||
```typescript
|
||||
editor.action((ctx) => {
|
||||
const collabService = ctx.get(collabServiceCtx);
|
||||
const doc = new Doc();
|
||||
const wsProvider = new WebsocketProvider("<YOUR_WS_HOST>", "milkdown", doc);
|
||||
|
||||
collabService.bindDoc(doc).setAwareness(wsProvider.awareness);
|
||||
|
||||
document.getElementById("connect").onclick = () => {
|
||||
wsProvider.connect();
|
||||
collabService.connect();
|
||||
};
|
||||
|
||||
document.getElementById("disconnect").onclick = () => {
|
||||
wsProvider.disconnect();
|
||||
collabService.disconnect();
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## Default Template
|
||||
|
||||
By default, the editor will show a empty document. You may want to use a template to show a document.
|
||||
|
||||
```typescript
|
||||
const template = `# Heading`;
|
||||
|
||||
editor.action((ctx) => {
|
||||
const collabService = ctx.get(collabServiceCtx);
|
||||
const doc = new Doc();
|
||||
const wsProvider = new WebsocketProvider("<YOUR_WS_HOST>", "milkdown", doc);
|
||||
|
||||
collabService.bindDoc(doc).setAwareness(wsProvider.awareness);
|
||||
|
||||
wsProvider.once("synced", async (isSynced: boolean) => {
|
||||
if (isSynced) {
|
||||
collabService
|
||||
// apply your template
|
||||
.applyTemplate(markdown)
|
||||
// don't forget connect
|
||||
.connect();
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Keep in mind that applying a template multiple times may cause some unexpected behavior, such as duplicate content.
|
||||
Because of this you need to make sure **the template is applied only once**.
|
||||
|
||||
By default, the template will only be applied if _document get from remote server is empty_.
|
||||
You can control this behavior through passing second parameter to `applyTemplate`:
|
||||
|
||||
```typescript
|
||||
collabService
|
||||
.applyTemplate(markdown, (remoteNode, templateNode) => {
|
||||
// return true to apply template
|
||||
})
|
||||
// don't forget connect
|
||||
.connect();
|
||||
```
|
||||
|
||||
Here the nodes we get are [prosemirror nodes](https://prosemirror.net/docs/ref/#model.Node).
|
||||
You should return `true` if the template should be applied, and `false` if not.
|
||||
@@ -0,0 +1,250 @@
|
||||
# Commands
|
||||
|
||||
Commands are a powerful way to programmatically modify editor content. The command system in Milkdown provides a flexible and type-safe way to create, manage, and execute commands.
|
||||
|
||||
## Command Manager
|
||||
|
||||
---
|
||||
|
||||
The command manager is the central place for handling all editor commands. It provides methods to:
|
||||
|
||||
- Register new commands
|
||||
- Execute commands
|
||||
- Chain multiple commands together
|
||||
- Handle command arguments
|
||||
|
||||
## Run a Command
|
||||
|
||||
---
|
||||
|
||||
You can execute commands using the command manager through the editor's action system:
|
||||
|
||||
```typescript
|
||||
import { Editor, commandsCtx } from "@milkdown/kit/core";
|
||||
import {
|
||||
commonmark,
|
||||
toggleEmphasisCommand,
|
||||
} from "@milkdown/kit/preset/commonmark";
|
||||
|
||||
async function setup() {
|
||||
const editor = await Editor.make().use(commonmark).create();
|
||||
|
||||
const toggleItalic = () =>
|
||||
editor.action((ctx) => {
|
||||
// get command manager
|
||||
const commandManager = ctx.get(commandsCtx);
|
||||
|
||||
// call command
|
||||
commandManager.call(toggleEmphasisCommand.key);
|
||||
});
|
||||
|
||||
// get markdown string:
|
||||
$button.onClick = toggleItalic;
|
||||
}
|
||||
```
|
||||
|
||||
## Command Chaining
|
||||
|
||||
---
|
||||
|
||||
You can chain multiple commands together using the command manager's `chain` method. Commands in the chain will be executed in order until one of them returns `true`:
|
||||
|
||||
```typescript
|
||||
import { Editor, commandsCtx } from "@milkdown/kit/core";
|
||||
import {
|
||||
commonmark,
|
||||
toggleEmphasisCommand,
|
||||
toggleStrongCommand,
|
||||
} from "@milkdown/kit/preset/commonmark";
|
||||
|
||||
const editor = await Editor.make().use(commonmark).create();
|
||||
|
||||
editor.action((ctx) => {
|
||||
const commandManager = ctx.get(commandsCtx);
|
||||
|
||||
// Chain multiple commands
|
||||
commandManager
|
||||
.chain()
|
||||
.pipe(toggleEmphasisCommand.key) // Try to toggle emphasis
|
||||
.pipe(toggleStrongCommand.key) // If emphasis fails, try to toggle strong
|
||||
.run();
|
||||
});
|
||||
```
|
||||
|
||||
You can also mix inline commands with registered commands:
|
||||
|
||||
```typescript
|
||||
import { chainCommands } from "@milkdown/prose/commands";
|
||||
|
||||
editor.action((ctx) => {
|
||||
const commandManager = ctx.get(commandsCtx);
|
||||
|
||||
commandManager
|
||||
.chain()
|
||||
.inline(someInlineCommand) // Add an inline command
|
||||
.pipe(toggleEmphasisCommand.key) // Add a registered command
|
||||
.run();
|
||||
});
|
||||
```
|
||||
|
||||
## Create a Command
|
||||
|
||||
---
|
||||
|
||||
To create a command, use the `$command` utility from `@milkdown/utils`. Commands should be [prosemirror commands](https://prosemirror.net/docs/guide/#commands).
|
||||
|
||||
### Example: Command without argument
|
||||
|
||||
```typescript
|
||||
import { Editor } from "@milkdown/kit/core";
|
||||
import { blockquoteSchema } from "@milkdown/kit/preset/commonmark";
|
||||
import { wrapIn } from "@milkdown/kit/prose/commands";
|
||||
import { $command, callCommand } from "@milkdown/kit/utils";
|
||||
|
||||
const wrapInBlockquoteCommand = $command(
|
||||
"WrapInBlockquote",
|
||||
(ctx) => () => wrapIn(blockquoteSchema.type(ctx)),
|
||||
);
|
||||
|
||||
// register the command when creating the editor
|
||||
const editor = Editor().make().use(wrapInBlockquoteCommand).create();
|
||||
|
||||
// call command
|
||||
editor.action(callCommand(wrapInBlockquoteCommand.key));
|
||||
```
|
||||
|
||||
### Example: Command with argument
|
||||
|
||||
Commands can accept arguments of any type:
|
||||
|
||||
```typescript
|
||||
import { headingSchema } from "@milkdown/kit/preset/commonmark";
|
||||
import { setBlockType } from "@milkdown/kit/prose/commands";
|
||||
import { $command, callCommand } from "@milkdown/kit/utils";
|
||||
|
||||
// use number as the type of argument
|
||||
export const WrapInHeading = createCmdKey<number>();
|
||||
const wrapInHeadingCommand = $command(
|
||||
"WrapInHeading",
|
||||
(ctx) =>
|
||||
(level = 1) =>
|
||||
setBlockType(headingSchema.type(ctx), { level }),
|
||||
);
|
||||
|
||||
// call command
|
||||
editor.action(callCommand(wrapInHeadingCommand.key)); // turn to h1 by default
|
||||
editor.action(callCommand(wrapInHeadingCommand.key, 2)); // turn to h2
|
||||
```
|
||||
|
||||
### Example: Command with Multiple Arguments
|
||||
|
||||
```typescript
|
||||
interface TableConfig {
|
||||
rows: number;
|
||||
cols: number;
|
||||
withHeader: boolean;
|
||||
}
|
||||
|
||||
const insertTableCommand = $command(
|
||||
"InsertTable",
|
||||
(ctx) => (config: TableConfig) => {
|
||||
// Implementation for inserting a table
|
||||
return (state, dispatch) => {
|
||||
// ... table insertion logic
|
||||
return true;
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Usage
|
||||
editor.action(
|
||||
callCommand(insertTableCommand.key, {
|
||||
rows: 3,
|
||||
cols: 3,
|
||||
withHeader: true,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
---
|
||||
|
||||
1. **Command Naming**
|
||||
- Use clear, descriptive names
|
||||
- Follow the pattern: `[Action][Target]Command`
|
||||
- Example: `toggleEmphasisCommand`, `insertTableCommand`
|
||||
|
||||
2. **Command Organization**
|
||||
- Group related commands together
|
||||
- Use namespaces for command keys
|
||||
- Keep commands focused and single-purpose
|
||||
|
||||
3. **Error Handling**
|
||||
- Always check if the command can be executed
|
||||
- Return `false` if the command cannot be executed
|
||||
- Handle edge cases gracefully
|
||||
|
||||
4. **Performance**
|
||||
- Keep commands lightweight
|
||||
- Avoid unnecessary state updates
|
||||
- Use command chaining for complex operations
|
||||
|
||||
5. **Type Safety**
|
||||
- Use TypeScript for command arguments
|
||||
- Define clear interfaces for command payloads
|
||||
- Use generics for type-safe command keys
|
||||
|
||||
## Common Patterns
|
||||
|
||||
---
|
||||
|
||||
### Toggle Commands
|
||||
|
||||
```typescript
|
||||
const toggleCommand = $command(
|
||||
"ToggleFeature",
|
||||
(ctx) => () => (state, dispatch) => {
|
||||
const isActive = checkIfActive(state);
|
||||
return isActive
|
||||
? removeFeature(state, dispatch)
|
||||
: addFeature(state, dispatch);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Insert Commands
|
||||
|
||||
```typescript
|
||||
const insertCommand = $command(
|
||||
"InsertContent",
|
||||
(ctx) => (content: string) => (state, dispatch) => {
|
||||
const { selection } = state;
|
||||
if (!selection) return false;
|
||||
|
||||
const tr = state.tr.insertText(content, selection.from);
|
||||
dispatch?.(tr);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Transform Commands
|
||||
|
||||
```typescript
|
||||
const transformCommand = $command(
|
||||
"TransformContent",
|
||||
(ctx) => (transform: (node: ProseNode) => ProseNode) => (state, dispatch) => {
|
||||
const { selection } = state;
|
||||
if (!selection) return false;
|
||||
|
||||
const tr = state.tr.replaceWith(
|
||||
selection.from,
|
||||
selection.to,
|
||||
transform(state.doc.nodeAt(selection.from)!),
|
||||
);
|
||||
dispatch?.(tr);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# FAQ
|
||||
|
||||
This page lists answers of FAQ.
|
||||
|
||||
---
|
||||
|
||||
### How can I change contents programmatically?
|
||||
|
||||
You should use `editor.action` to change the contents.
|
||||
We provide two macros for that allow you to change content in milkdown, `insert` and `replaceAll`.
|
||||
|
||||
```typescript
|
||||
import { insert, replaceAll } from "@milkdown/kit/utils";
|
||||
|
||||
const editor = await Editor.make()
|
||||
// .use(<All Your Plugins>)
|
||||
.create();
|
||||
|
||||
editor.action(insert("# New Heading"));
|
||||
|
||||
editor.action(replaceAll("# New Document"));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### How to configure remark?
|
||||
|
||||
```typescript
|
||||
import { remarkStringifyOptionsCtx } from "@milkdown/kit/core";
|
||||
|
||||
editor.config((ctx) => {
|
||||
ctx.set(remarkStringifyOptionsCtx, {
|
||||
// some options, for example:
|
||||
bullet: "*",
|
||||
fences: true,
|
||||
incrementListMarker: false,
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,165 @@
|
||||
# Getting Started with Milkdown
|
||||
|
||||
Milkdown is a powerful WYSIWYG markdown editor that combines the simplicity of markdown with the flexibility of a modern editor. It's designed to be lightweight yet extensible, making it perfect for both simple and complex editing needs.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The fastest way to get started is using `@milkdown/crepe`:
|
||||
|
||||
```bash
|
||||
npm install @milkdown/crepe
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Crepe } from "@milkdown/crepe";
|
||||
import "@milkdown/crepe/theme/common/style.css";
|
||||
import "@milkdown/crepe/theme/frame.css";
|
||||
|
||||
const crepe = new Crepe({
|
||||
root: "#app",
|
||||
defaultValue: "Hello, Milkdown!",
|
||||
});
|
||||
|
||||
crepe.create();
|
||||
```
|
||||
|
||||
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/editor-crepe"}
|
||||
|
||||
## Core Concepts
|
||||
|
||||
Milkdown consists of two main parts:
|
||||
|
||||
1. **Core Package** (`@milkdown/core`)
|
||||
- Plugin loader
|
||||
- Internal plugins
|
||||
|
||||
2. **Additional Plugins**
|
||||
- Syntax support
|
||||
- Commands
|
||||
- UI components
|
||||
- Custom features
|
||||
|
||||
This modular architecture allows you to enable or disable features as needed, from basic markdown support to advanced features like tables, LaTeX equations, and collaborative editing.
|
||||
|
||||
## Key Features
|
||||
|
||||
- 📝 **WYSIWYG Markdown** - Write markdown in an elegant way
|
||||
- 🎨 **Themable** - Create your own theme and publish it as an npm package
|
||||
- 🎮 **Hackable** - Create your own plugin to support your awesome idea
|
||||
- 🦾 **Reliable** - Built on top of [prosemirror](https://prosemirror.net/) and [remark](https://github.com/remarkjs/remark)
|
||||
- ⚡ **Slash & Tooltip** - Write faster than ever, enabled by a plugin
|
||||
- 🧮 **Math** - LaTeX math equations support via math plugin
|
||||
- 📊 **Table** - Table support with fluent ui, via table plugin
|
||||
- 🍻 **Collaborate** - Shared editing support with [yjs](https://docs.yjs.dev/)
|
||||
- 💾 **Clipboard** - Support copy and paste markdown, via clipboard plugin
|
||||
- 👍 **Emoji** - Support emoji shortcut and picker, via emoji plugin
|
||||
|
||||
## Tech Stack
|
||||
|
||||
Milkdown is built on top of these powerful libraries:
|
||||
|
||||
- [Prosemirror](https://prosemirror.net/) - A toolkit for building rich-text editors on the web
|
||||
- [Remark](https://github.com/remarkjs/remark) - Markdown parser done right
|
||||
- [TypeScript](https://www.typescriptlang.org/) - For type safety and better developer experience
|
||||
|
||||
## Creating Your First Editor
|
||||
|
||||
Milkdown provides two distinct approaches to create an editor, each suited for different needs:
|
||||
|
||||
### 1. 🍼 Using `@milkdown/kit` (Build from Scratch)
|
||||
|
||||
This approach gives you complete control over your editor. Use this if you want to:
|
||||
|
||||
- Build a custom editor from the ground up
|
||||
- Have full control over which features to include
|
||||
- Create a highly customized editing experience
|
||||
- Integrate with specific frameworks or requirements
|
||||
|
||||
First, install the required packages:
|
||||
|
||||
```bash
|
||||
npm install @milkdown/kit
|
||||
```
|
||||
|
||||
Create a basic editor with commonmark syntax:
|
||||
|
||||
```typescript
|
||||
import { Editor } from "@milkdown/kit/core";
|
||||
import { commonmark } from "@milkdown/kit/preset/commonmark";
|
||||
// This is the must have css for prosemirror
|
||||
import "@milkdown/kit/prose/view/style/prosemirror.css";
|
||||
|
||||
Editor.make().use(commonmark).create();
|
||||
```
|
||||
|
||||
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/vanilla-commonmark"}
|
||||
|
||||
Add undo & redo support:
|
||||
|
||||
```typescript
|
||||
import { Editor } from "@milkdown/kit/core";
|
||||
import { history } from "@milkdown/kit/plugin/history";
|
||||
import { commonmark } from "@milkdown/kit/preset/commonmark";
|
||||
import { nord } from "@milkdown/theme-nord";
|
||||
import "@milkdown/theme-nord/style.css";
|
||||
|
||||
const milkdown = Editor.make()
|
||||
.config(nord)
|
||||
.use(commonmark)
|
||||
.use(history)
|
||||
.create()
|
||||
.then(() => {
|
||||
console.log("Editor created");
|
||||
});
|
||||
|
||||
// To destroy the editor
|
||||
milkdown.destroy();
|
||||
```
|
||||
|
||||
> **Note**: `<Mod>` is `<Cmd>` for macOS and `<Ctrl>` for other platforms.
|
||||
|
||||
### 2. 🥞 Using `@milkdown/crepe` (Ready to Use)
|
||||
|
||||
This is the quickest way to get started with a fully-featured editor. Use this if you want to:
|
||||
|
||||
- Get up and running quickly
|
||||
- Have a well-designed editor out of the box
|
||||
- Focus on content rather than configuration
|
||||
- Have a production-ready solution with minimal setup
|
||||
|
||||
```bash
|
||||
npm install @milkdown/crepe
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Crepe } from "@milkdown/crepe";
|
||||
import "@milkdown/crepe/theme/common/style.css";
|
||||
/**
|
||||
* Available themes:
|
||||
* frame, classic, nord
|
||||
* frame-dark, classic-dark, nord-dark
|
||||
*/
|
||||
import "@milkdown/crepe/theme/frame.css";
|
||||
|
||||
const crepe = new Crepe({
|
||||
root: "#app",
|
||||
defaultValue: "Hello, Milkdown!",
|
||||
});
|
||||
|
||||
crepe.create().then(() => {
|
||||
console.log("Editor created");
|
||||
});
|
||||
|
||||
// To destroy the editor
|
||||
crepe.destroy();
|
||||
```
|
||||
|
||||
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/editor-crepe"}
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn more about [overview](/guide/architecture-overview)
|
||||
- Explore [available plugins](/plugins/using-plugins)
|
||||
- Check out [theming](/guide/theming)
|
||||
|
||||
> 🍼 Fun fact: This documentation is rendered by Milkdown itself!
|
||||
@@ -0,0 +1,398 @@
|
||||
# Interacting with Editor
|
||||
|
||||
This guide covers the essential ways to interact with the Milkdown editor, including initialization, content management, and editor lifecycle.
|
||||
|
||||
## Using Crepe Editor
|
||||
|
||||
---
|
||||
|
||||
Crepe is a high-level wrapper around Milkdown that provides a simpler API for common editor operations. Here's how to use it:
|
||||
|
||||
```typescript
|
||||
import { Crepe } from "@milkdown/crepe";
|
||||
|
||||
// Create a new editor instance
|
||||
const editor = new Crepe({
|
||||
// Optional: specify root element (DOM node or selector)
|
||||
root: "#editor",
|
||||
|
||||
// Optional: set default content, supports markdown, json and dom.
|
||||
defaultValue: "# Hello Crepe!",
|
||||
});
|
||||
|
||||
// Create the editor
|
||||
await editor.create();
|
||||
|
||||
// Get markdown content
|
||||
const markdown = editor.getMarkdown();
|
||||
|
||||
// Set readonly mode
|
||||
editor.setReadonly(true);
|
||||
|
||||
// Register event listeners
|
||||
editor.on((listener) => {
|
||||
listener.markdownUpdated((ctx, markdown) => {
|
||||
console.log("Content updated:", markdown);
|
||||
});
|
||||
|
||||
listener.focus((ctx) => {
|
||||
console.log("Editor focused");
|
||||
});
|
||||
|
||||
listener.blur((ctx) => {
|
||||
console.log("Editor blurred");
|
||||
});
|
||||
|
||||
listener.selectionUpdated((ctx, selection, prevSelection) => {
|
||||
console.log("Selection updated:", selection);
|
||||
});
|
||||
|
||||
listener.updated((ctx, doc, prevDoc) => {
|
||||
console.log("Document updated:", doc);
|
||||
});
|
||||
});
|
||||
|
||||
// Destroy the editor when done
|
||||
await editor.destroy();
|
||||
```
|
||||
|
||||
## Register to DOM
|
||||
|
||||
---
|
||||
|
||||
By default, milkdown will create editor on the `document.body`. Alternatively, you can also point out which dom node you want it to load into:
|
||||
|
||||
```typescript
|
||||
import { rootCtx } from "@milkdown/kit/core";
|
||||
|
||||
Editor.make().config((ctx) => {
|
||||
ctx.set(rootCtx, document.querySelector("#editor"));
|
||||
});
|
||||
```
|
||||
|
||||
It's also possible to just pass a selector to `rootCtx`:
|
||||
|
||||
> The selector will be passed to `document.querySelector` to get the dom.
|
||||
|
||||
```typescript
|
||||
import { rootCtx } from "@milkdown/kit/core";
|
||||
|
||||
Editor.make().config((ctx) => {
|
||||
ctx.set(rootCtx, "#editor");
|
||||
});
|
||||
```
|
||||
|
||||
## Setting Default Value
|
||||
|
||||
---
|
||||
|
||||
We support three types of default values:
|
||||
|
||||
- Markdown strings
|
||||
- HTML DOM
|
||||
- Prosemirror documentation JSON
|
||||
|
||||
### Markdown
|
||||
|
||||
You can set a markdown string as the default value of the editor.
|
||||
|
||||
```typescript
|
||||
import { defaultValueCtx } from "@milkdown/kit/core";
|
||||
|
||||
const defaultValue = "# Hello milkdown";
|
||||
Editor.make().config((ctx) => {
|
||||
ctx.set(defaultValueCtx, defaultValue);
|
||||
});
|
||||
```
|
||||
|
||||
### Dom
|
||||
|
||||
You can also use HTML as default value.
|
||||
|
||||
Let's assume that we have the following html snippets:
|
||||
|
||||
```html
|
||||
<div id="pre">
|
||||
<h1>Hello milkdown!</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
Then we can use it as a defaultValue with a `type` specification:
|
||||
|
||||
```typescript
|
||||
import { defaultValueCtx } from "@milkdown/kit/core";
|
||||
|
||||
const defaultValue = {
|
||||
type: "html",
|
||||
dom: document.querySelector("#pre"),
|
||||
};
|
||||
Editor.make().config((ctx) => {
|
||||
ctx.set(defaultValueCtx, defaultValue);
|
||||
});
|
||||
```
|
||||
|
||||
### JSON
|
||||
|
||||
We can also use a JSON object as a default value.
|
||||
|
||||
This JSON object can be obtained by a listener through the [listener-plugin](https://www.npmjs.com/package/@milkdown/plugin-listener), for example:
|
||||
|
||||
```typescript
|
||||
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
||||
|
||||
let jsonOutput;
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.get(listenerCtx).updated((ctx, doc, prevDoc) => {
|
||||
jsonOutput = doc.toJSON();
|
||||
});
|
||||
})
|
||||
.use(listener);
|
||||
```
|
||||
|
||||
Then we can use this `jsonOutput` as default Value:
|
||||
|
||||
```typescript
|
||||
import { defaultValueCtx } from "@milkdown/kit/core";
|
||||
|
||||
const defaultValue = {
|
||||
type: "json",
|
||||
value: jsonOutput,
|
||||
};
|
||||
Editor.make().config((ctx) => {
|
||||
ctx.set(defaultValueCtx, defaultValue);
|
||||
});
|
||||
```
|
||||
|
||||
## Inspecting Editor Status
|
||||
|
||||
---
|
||||
|
||||
You can inspect the editor's status through the `status` property.
|
||||
|
||||
```typescript
|
||||
import { Editor, EditorStatus } from "@milkdown/kit/core";
|
||||
|
||||
const editor = Editor.make().use(/* some plugins */);
|
||||
|
||||
assert(editor.status === EditorStatus.Idle);
|
||||
|
||||
editor.create().then(() => {
|
||||
assert(editor.status === EditorStatus.Created);
|
||||
});
|
||||
|
||||
assert(editor.status === EditorStatus.OnCreate);
|
||||
|
||||
editor.destroy().then(() => {
|
||||
assert(editor.status === EditorStatus.Destroyed);
|
||||
});
|
||||
|
||||
assert(editor.status === EditorStatus.OnDestroyed);
|
||||
```
|
||||
|
||||
You can also listen to the status changes:
|
||||
|
||||
```typescript
|
||||
import { Editor, EditorStatus } from "@milkdown/kit/core";
|
||||
|
||||
const editor = Editor.make().use(/* some plugins */);
|
||||
|
||||
editor.onStatusChange((status: EditorStatus) => {
|
||||
console.log(status);
|
||||
});
|
||||
```
|
||||
|
||||
### Status Lifecycle
|
||||
|
||||
1. `Idle`: Initial state
|
||||
2. `OnCreate`: During creation
|
||||
3. `Created`: Successfully created
|
||||
4. `OnDestroyed`: During destruction
|
||||
5. `Destroyed`: Successfully destroyed
|
||||
|
||||
## Adding Listeners
|
||||
|
||||
---
|
||||
|
||||
As mentioned above, you can add a listener to the editor, in order to get its value when needed.
|
||||
You can add as many listeners as you want, all the listeners will be triggered at once.
|
||||
|
||||
### Markdown Listener
|
||||
|
||||
You can add markdown listener to get the editor's contents as a markdown string.
|
||||
|
||||
> ⚠️ Markdown listener will influence the performance for large documents, please use it carefully.
|
||||
> If you have a large document, I suggest you to only `parse` and `serialize` the document when needed.
|
||||
|
||||
```typescript
|
||||
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
||||
|
||||
let output = "";
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.get(listenerCtx).markdownUpdated((ctx, markdown, prevMarkdown) => {
|
||||
output = markdown;
|
||||
});
|
||||
})
|
||||
.use(listener);
|
||||
```
|
||||
|
||||
### Doc Listener
|
||||
|
||||
You can also listen to the [raw prosemirror document node](https://prosemirror.net/docs/ref/#model.Node), and do things you want from there.
|
||||
|
||||
```typescript
|
||||
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
||||
|
||||
let jsonOutput;
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.get(listenerCtx).updated((ctx, doc, prevDoc) => {
|
||||
jsonOutput = doc.toJSON();
|
||||
});
|
||||
})
|
||||
.use(listener);
|
||||
```
|
||||
|
||||
### Selection Listener
|
||||
|
||||
You can track changes to the editor's selection using the `selectionUpdated` event. This is useful for implementing features like:
|
||||
|
||||
- Custom toolbars that update based on selection
|
||||
- Context menus
|
||||
- Selection-based formatting controls
|
||||
|
||||
```typescript
|
||||
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
||||
import { Selection, TextSelection } from "@milkdown/prose/state";
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.get(listenerCtx).selectionUpdated((ctx, selection, prevSelection) => {
|
||||
if (selection instanceof TextSelection) {
|
||||
// Get selection range
|
||||
const { from, to } = selection;
|
||||
|
||||
// Example: Update toolbar based on selection
|
||||
updateToolbar({
|
||||
hasSelection: from !== to,
|
||||
selectionStart: from,
|
||||
selectionEnd: to,
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.use(listener);
|
||||
```
|
||||
|
||||
The selection listener will be triggered when the selection is changed.
|
||||
So you don't need to compare them manually.
|
||||
|
||||
For more details about listeners, please check [Using Listeners](/docs/api/plugin-listener).
|
||||
|
||||
## Readonly Mode
|
||||
|
||||
---
|
||||
|
||||
You can set the editor to readonly mode by setting the `editable` property.
|
||||
|
||||
```typescript
|
||||
import { editorViewOptionsCtx } from "@milkdown/kit/core";
|
||||
|
||||
let readonly = false;
|
||||
|
||||
const editable = () => !readonly;
|
||||
|
||||
Editor.make().config((ctx) => {
|
||||
ctx.update(editorViewOptionsCtx, (prev) => ({
|
||||
...prev,
|
||||
editable,
|
||||
}));
|
||||
});
|
||||
|
||||
// set to readonly after 5 secs.
|
||||
setTimeout(() => {
|
||||
readonly = true;
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
### Use Cases for Readonly Mode
|
||||
|
||||
- Preview mode
|
||||
- Document review
|
||||
- Print-friendly views
|
||||
- Mobile device optimization
|
||||
|
||||
## Using Actions
|
||||
|
||||
---
|
||||
|
||||
You can use an action to get the context value in a running editor on demand.
|
||||
|
||||
For example, to get the markdown string by running an action:
|
||||
|
||||
```typescript
|
||||
import { Editor, editorViewCtx, serializerCtx } from "@milkdown/kit/core";
|
||||
|
||||
async function playWithEditor() {
|
||||
const editor = await Editor.make().use(commonmark).create();
|
||||
|
||||
const getMarkdown = () =>
|
||||
editor.action((ctx) => {
|
||||
const editorView = ctx.get(editorViewCtx);
|
||||
const serializer = ctx.get(serializerCtx);
|
||||
return serializer(editorView.state.doc);
|
||||
});
|
||||
|
||||
// get markdown string:
|
||||
getMarkdown();
|
||||
}
|
||||
```
|
||||
|
||||
We provide some macros out of the box, you can use them as actions:
|
||||
|
||||
```typescript
|
||||
import { insert } from "@milkdown/kit/utils";
|
||||
|
||||
editor.action(insert("# Hello milkdown"));
|
||||
```
|
||||
|
||||
### Common Actions
|
||||
|
||||
- Insert content
|
||||
- Get current selection
|
||||
- Apply formatting
|
||||
- Execute commands
|
||||
|
||||
For more details about macros, please check [macros](/docs/guide/macros).
|
||||
|
||||
## Destroying
|
||||
|
||||
---
|
||||
|
||||
You can call `editor.destroy` to destroy an existing editor. You can create a new editor again with `editor.create`.
|
||||
|
||||
```typescript
|
||||
await editor.destroy();
|
||||
|
||||
// Then create again
|
||||
await editor.create();
|
||||
```
|
||||
|
||||
If you just want to recreate the editor, you can use `editor.create`, it will **destroy the old editor and create a new one**.
|
||||
|
||||
```typescript
|
||||
await editor.create();
|
||||
|
||||
// This equals to call `editor.destroy` and `editor.create` again.
|
||||
await editor.create();
|
||||
```
|
||||
|
||||
If you want to **clear the plugins and configs for the editor** when calling `editor.destroy`, you can pass `true` to `editor.destroy`.
|
||||
|
||||
```typescript
|
||||
await editor.destroy(true);
|
||||
```
|
||||
@@ -0,0 +1,252 @@
|
||||
# Keyboard Shortcuts
|
||||
|
||||
Keyboard shortcuts are a crucial part of the editor's user experience. Milkdown provides a flexible system for configuring keyboard shortcuts through presets and plugins.
|
||||
|
||||
## Default Shortcuts
|
||||
|
||||
---
|
||||
|
||||
Milkdown comes with a set of default keyboard shortcuts from both presets and plugins. Here's a comprehensive list of all internal shortcuts:
|
||||
|
||||
> #### 💡 Note
|
||||
>
|
||||
> `Mod` represents the platform-specific modifier key:
|
||||
>
|
||||
> - Windows/Linux: `Ctrl`
|
||||
> - macOS: `Command`
|
||||
|
||||
### Commonmark Preset Shortcuts
|
||||
|
||||
#### Headings
|
||||
|
||||
| Shortcut | Description |
|
||||
| -------------------- | ----------------------- |
|
||||
| `Mod-Alt-1` | Turn block into h1 |
|
||||
| `Mod-Alt-2` | Turn block into h2 |
|
||||
| `Mod-Alt-3` | Turn block into h3 |
|
||||
| `Mod-Alt-4` | Turn block into h4 |
|
||||
| `Mod-Alt-5` | Turn block into h5 |
|
||||
| `Mod-Alt-6` | Turn block into h6 |
|
||||
| `Delete`/`Backspace` | Downgrade heading level |
|
||||
|
||||
#### Block Elements
|
||||
|
||||
| Shortcut | Description |
|
||||
| ------------- | ---------------------------- |
|
||||
| `Mod-Shift-b` | Wrap selection in blockquote |
|
||||
| `Mod-Shift-8` | Wrap in bullet list |
|
||||
| `Mod-Shift-7` | Wrap in ordered list |
|
||||
| `Mod-Shift-c` | Wrap in code block |
|
||||
| `Shift-Enter` | Insert hard break |
|
||||
| `Mod-Alt-0` | Wrap in paragraph |
|
||||
|
||||
#### Text Formatting
|
||||
|
||||
| Shortcut | Description |
|
||||
| -------- | ------------------ |
|
||||
| `Mod-b` | Toggle bold |
|
||||
| `Mod-i` | Toggle italic |
|
||||
| `Mod-e` | Toggle inline code |
|
||||
|
||||
### GFM Preset Shortcuts
|
||||
|
||||
#### Text Formatting
|
||||
|
||||
| Shortcut | Description |
|
||||
| ----------- | -------------------- |
|
||||
| `Mod-Alt-x` | Toggle strikethrough |
|
||||
|
||||
#### Tables
|
||||
|
||||
| Shortcut | Description |
|
||||
| ------------------- | -------------------------------- |
|
||||
| `Mod-]` | Move to next cell |
|
||||
| `Mod-[` | Move to previous cell |
|
||||
| `Mod-Enter`/`Enter` | Exit table and break if possible |
|
||||
|
||||
## Configuring Shortcuts
|
||||
|
||||
---
|
||||
|
||||
You can customize keyboard shortcuts by configuring the keymap in the editor setup:
|
||||
|
||||
```typescript
|
||||
import { blockquoteKeymap, commonmark } from "@milkdown/kit/preset/commonmark";
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.set(blockquoteKeymap.key, {
|
||||
WrapInBlockquote: "Mod-Shift-b",
|
||||
// or you may want to bind multiple keys:
|
||||
WrapInBlockquote: ["Mod-Shift-b", "Mod-b"],
|
||||
});
|
||||
})
|
||||
.use(commonmark);
|
||||
```
|
||||
|
||||
## Defining Keymaps
|
||||
|
||||
---
|
||||
|
||||
Keymaps in Milkdown are defined using the `$useKeymap` utility. Here's how to define keymaps for different features:
|
||||
|
||||
### Heading Keymap Example
|
||||
|
||||
```typescript
|
||||
import { $useKeymap } from "@milkdown/utils";
|
||||
import { commandsCtx } from "@milkdown/core";
|
||||
|
||||
export const headingKeymap = $useKeymap("headingKeymap", {
|
||||
TurnIntoH1: {
|
||||
shortcuts: "Mod-Alt-1",
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(wrapInHeadingCommand.key, 1);
|
||||
},
|
||||
},
|
||||
TurnIntoH2: {
|
||||
shortcuts: "Mod-Alt-2",
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(wrapInHeadingCommand.key, 2);
|
||||
},
|
||||
},
|
||||
// ... more heading levels
|
||||
DowngradeHeading: {
|
||||
shortcuts: ["Delete", "Backspace"],
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(downgradeHeadingCommand.key);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Strong (Bold) Keymap Example
|
||||
|
||||
```typescript
|
||||
import { $useKeymap } from "@milkdown/utils";
|
||||
import { commandsCtx } from "@milkdown/core";
|
||||
|
||||
export const strongKeymap = $useKeymap("strongKeymap", {
|
||||
ToggleBold: {
|
||||
shortcuts: ["Mod-b"],
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(toggleStrongCommand.key);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Keymap Structure
|
||||
|
||||
Each keymap definition follows this structure:
|
||||
|
||||
```typescript
|
||||
$useKeymap('keymapName', {
|
||||
CommandName: {
|
||||
shortcuts: string | string[], // Single shortcut or array of shortcuts
|
||||
priority?: number, // (Optional) Priority of the shortcut
|
||||
command: (ctx) => () => { // Command to execute
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(commandKey, ...args);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Creating Custom Shortcuts
|
||||
|
||||
---
|
||||
|
||||
If you need to add custom shortcuts, you can create a keymap plugin:
|
||||
|
||||
```typescript
|
||||
import { $useKeymap } from "@milkdown/utils";
|
||||
import { commandsCtx } from "@milkdown/core";
|
||||
|
||||
const customKeymap = $useKeymap("customKeymap", {
|
||||
CustomCommand: {
|
||||
shortcuts: "F1",
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(someCommand.key);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Usage
|
||||
Editor.make().use(customKeymap).use(commonmark);
|
||||
```
|
||||
|
||||
### Example: Custom Command with Shortcut
|
||||
|
||||
```typescript
|
||||
import { $command, $useKeymap } from "@milkdown/utils";
|
||||
import { commandsCtx } from "@milkdown/core";
|
||||
|
||||
// Create a custom command
|
||||
const customCommand = $command("CustomCommand", (ctx) => () => {
|
||||
return (state, dispatch) => {
|
||||
// Command implementation
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
// Create a keymap
|
||||
const customKeymap = $useKeymap("customKeymap", {
|
||||
CustomCommand: {
|
||||
shortcuts: ["F1", "Mod-F1"], // Multiple shortcuts
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => commands.call(customCommand.key);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Usage
|
||||
Editor.make().use(customCommand).use(customKeymap);
|
||||
```
|
||||
|
||||
## Shortcut Priority
|
||||
|
||||
You can control the order in which shortcuts are handled by specifying a `priority` property. Shortcuts with higher priority values are handled before those with lower values. This is useful if you want your custom shortcut to override or take precedence over other shortcuts that use the same key combination.
|
||||
|
||||
When multiple shortcuts are registered for the same key, they are executed in order of priority. If a shortcut command returns `false`, the next shortcut with the same key will be tried. If it returns `true`, no further commands for that key will be run. This allows you to chain or override shortcut behaviors as needed.
|
||||
|
||||
- The default priority is **50**.
|
||||
- Normal priority values should be between **1** and **100**.
|
||||
- Use higher numbers to ensure your shortcut is registered before others with the same key.
|
||||
|
||||
#### Example: Using Priority
|
||||
|
||||
```typescript
|
||||
import { $useKeymap } from "@milkdown/utils";
|
||||
import { commandsCtx } from "@milkdown/core";
|
||||
|
||||
export const customKeymap = $useKeymap("customKeymap", {
|
||||
CustomBold: {
|
||||
shortcuts: "Mod-b",
|
||||
priority: 100, // Highest in the normal range, so this runs first
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => {
|
||||
// Custom bold logic
|
||||
return true;
|
||||
};
|
||||
},
|
||||
},
|
||||
CustomAnotherBold: {
|
||||
shortcuts: "Mod-b",
|
||||
priority: 75, // Lower priority, will run only if CustomBold returns false
|
||||
command: (ctx) => {
|
||||
const commands = ctx.get(commandsCtx);
|
||||
return () => {
|
||||
// Custom italic logic
|
||||
return true;
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,278 @@
|
||||
# Macros
|
||||
|
||||
Macros are helper functions that provide a convenient way to interact with the editor. They take a payload (or nothing) as parameters and return a callback function that takes the `ctx` of milkdown as a parameter. When called with `ctx`, they apply the specified action to the editor.
|
||||
|
||||
## Usage
|
||||
|
||||
There are two main ways to use macros:
|
||||
|
||||
```typescript
|
||||
import { insert } from "@milkdown/kit/utils";
|
||||
import { listenerCtx } from "@milkdown/plugin-listener";
|
||||
|
||||
// Method 1: Using editor.action()
|
||||
editor.action(insert("# Hello Macro"));
|
||||
|
||||
// Method 2: Using listener
|
||||
editor.config((ctx) => {
|
||||
ctx.get(listenerCtx).mounted(insert("# Default Title"));
|
||||
});
|
||||
```
|
||||
|
||||
## Available Macros
|
||||
|
||||
### Content Manipulation
|
||||
|
||||
#### `insert`
|
||||
|
||||
Inserts content at the current cursor position. The macro accepts two parameters:
|
||||
|
||||
- `markdown`: The markdown string to insert
|
||||
- `inline`: Optional boolean flag (default: false) that determines how the content is inserted
|
||||
|
||||
```typescript
|
||||
import { insert } from "@milkdown/kit/utils";
|
||||
|
||||
// Insert as block content (default)
|
||||
editor.action(insert("# Hello World"));
|
||||
|
||||
// Insert as inline content
|
||||
editor.action(insert("inline text", true));
|
||||
```
|
||||
|
||||
The behavior differs based on the `inline` parameter:
|
||||
|
||||
- When `inline` is `false` (default):
|
||||
- Replaces the current selection with the parsed markdown content
|
||||
- Maintains the selection's open start/end positions
|
||||
- Scrolls the view to show the inserted content
|
||||
- When `inline` is `true`:
|
||||
- Attempts to insert the content as inline text
|
||||
- If the content is text-only, replaces the selection with a text node
|
||||
- Otherwise, replaces the selection with the parsed content
|
||||
|
||||
#### `insertPos`
|
||||
|
||||
Inserts markdown at a given position. The macro accepts two parameters:
|
||||
|
||||
- `markdown`: The markdown string to insert
|
||||
- `pos`: The position to insert the content at
|
||||
|
||||
```typescript
|
||||
import { insertPos } from "@milkdown/kit/utils";
|
||||
|
||||
// Insert "Hello" at the beginning of the document
|
||||
editor.action(insertPos("Hello", 0));
|
||||
```
|
||||
|
||||
#### `replaceAll`
|
||||
|
||||
Replaces all content in the editor. The macro accepts two parameters:
|
||||
|
||||
- `markdown`: The markdown string to replace the current content with
|
||||
- `flush`: Optional boolean flag (default: false) that determines how the replacement is performed
|
||||
|
||||
```typescript
|
||||
import { replaceAll } from "@milkdown/kit/utils";
|
||||
|
||||
// Replace content without flushing state
|
||||
editor.action(replaceAll("# New Content"));
|
||||
|
||||
// Replace content and flush editor state
|
||||
editor.action(replaceAll("# New Content", true));
|
||||
```
|
||||
|
||||
The behavior differs based on the `flush` parameter:
|
||||
|
||||
- When `flush` is `false` (default):
|
||||
- Replaces the entire document content with the new markdown
|
||||
- Maintains the current editor state
|
||||
- More efficient for simple content replacements
|
||||
- When `flush` is `true`:
|
||||
- Creates a new editor state with the new content
|
||||
- Reinitializes all plugins
|
||||
- Useful when you need a completely fresh editor state
|
||||
|
||||
#### `replaceRange`
|
||||
|
||||
Replaces the content of the given range with a markdown string.
|
||||
|
||||
```typescript
|
||||
import { replaceRange } from "@milkdown/kit/utils";
|
||||
|
||||
// Replace content from position 0 to 5 with "Hello"
|
||||
editor.action(replaceRange("Hello", { from: 0, to: 5 }));
|
||||
```
|
||||
|
||||
### Content Retrieval
|
||||
|
||||
#### `getMarkdown`
|
||||
|
||||
Gets the current content as markdown. If a range is provided, it will return the markdown for that range; otherwise, it will return the markdown for the entire document.
|
||||
|
||||
```typescript
|
||||
import { getMarkdown } from "@milkdown/kit/utils";
|
||||
|
||||
// Get markdown for the entire document
|
||||
const markdown = editor.action(getMarkdown());
|
||||
|
||||
// Get markdown for a specific range
|
||||
const selectionMarkdown = editor.action(getMarkdown({ from: 0, to: 5 }));
|
||||
```
|
||||
|
||||
#### `getHTML`
|
||||
|
||||
Gets the current content as HTML.
|
||||
|
||||
```typescript
|
||||
import { getHTML } from "@milkdown/kit/utils";
|
||||
|
||||
const html = editor.action(getHTML());
|
||||
```
|
||||
|
||||
### Editor State
|
||||
|
||||
#### `forceUpdate`
|
||||
|
||||
Forces the editor to update its state.
|
||||
|
||||
```typescript
|
||||
import { forceUpdate } from "@milkdown/kit/utils";
|
||||
|
||||
editor.action(forceUpdate());
|
||||
```
|
||||
|
||||
#### `setAttr`
|
||||
|
||||
Sets attributes for a node at a specific position. The macro accepts two parameters:
|
||||
|
||||
- `pos`: The position of the node to update
|
||||
- `update`: A function that takes the previous attributes and returns the new attributes
|
||||
|
||||
```typescript
|
||||
import { setAttr } from "@milkdown/kit/utils";
|
||||
|
||||
// Update node attributes at position 10
|
||||
editor.action(
|
||||
setAttr(10, (prevAttrs) => ({
|
||||
...prevAttrs,
|
||||
class: "custom-class",
|
||||
})),
|
||||
);
|
||||
|
||||
// Example: Update heading level
|
||||
editor.action(
|
||||
setAttr(10, (prevAttrs) => ({
|
||||
...prevAttrs,
|
||||
level: 2,
|
||||
})),
|
||||
);
|
||||
```
|
||||
|
||||
The macro:
|
||||
|
||||
- Takes a specific position in the document
|
||||
- Retrieves the node at that position
|
||||
- Applies the update function to modify the node's attributes
|
||||
- Dispatches the changes to update the editor state
|
||||
|
||||
Note: The position must be valid and contain a node, otherwise the operation will be ignored.
|
||||
|
||||
### Navigation
|
||||
|
||||
#### `outline`
|
||||
|
||||
Gets the outline of the document.
|
||||
|
||||
```typescript
|
||||
import { outline } from "@milkdown/kit/utils";
|
||||
|
||||
const docOutline = editor.action(outline());
|
||||
```
|
||||
|
||||
### Command Execution
|
||||
|
||||
#### `callCommand`
|
||||
|
||||
Calls a registered command with optional payload. The macro has two overloads:
|
||||
|
||||
Examples:
|
||||
|
||||
```typescript
|
||||
import { callCommand } from "@milkdown/kit/utils";
|
||||
import { wrapInHeadingCommand } from "@milkdown/plugin-heading";
|
||||
|
||||
// Using command key
|
||||
editor.action(callCommand(wrapInHeadingCommand.key, 1));
|
||||
|
||||
// With complex payload
|
||||
editor.action(
|
||||
callCommand("CustomCommand", {
|
||||
type: "heading",
|
||||
level: 1,
|
||||
content: "New Heading",
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
The macro:
|
||||
|
||||
- Takes a command key
|
||||
- Optionally accepts a payload parameter
|
||||
- Returns a boolean indicating whether the command was successful
|
||||
|
||||
Note: The command must be registered in the editor's command context before it can be called.
|
||||
|
||||
### Utility Macros
|
||||
|
||||
#### `markdownToSlice`
|
||||
|
||||
Converts a markdown string to a [slice](https://prosemirror.net/docs/ref/#model.Slice). This is useful when you need to manipulate the content before inserting it into the editor.
|
||||
|
||||
```typescript
|
||||
import { markdownToSlice } from "@milkdown/kit/utils";
|
||||
|
||||
const slice = editor.action(markdownToSlice("# Hello Slice"));
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Adding Content
|
||||
|
||||
```typescript
|
||||
import { insert } from "@milkdown/kit/utils";
|
||||
import { listenerCtx } from "@milkdown/plugin-listener";
|
||||
|
||||
editor.config((ctx) => {
|
||||
ctx.get(listenerCtx).mounted(insert("# Welcome\nStart editing..."));
|
||||
});
|
||||
```
|
||||
|
||||
### Saving Content
|
||||
|
||||
```typescript
|
||||
import { getMarkdown } from "@milkdown/kit/utils";
|
||||
|
||||
editor.config((ctx) => {
|
||||
ctx.get(listenerCtx).updated(() => {
|
||||
const content = getMarkdown()(ctx);
|
||||
localStorage.setItem("editor-content", content);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Command with Macro
|
||||
|
||||
```typescript
|
||||
import { callCommand } from "@milkdown/kit/utils";
|
||||
|
||||
editor.action(
|
||||
callCommand("customCommand", {
|
||||
type: "heading",
|
||||
level: 1,
|
||||
content: "New Heading",
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
For more details about each macro's parameters and return types, check the [API Reference](/docs/api/utils#macros).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Prosemirror API
|
||||
|
||||
Milkdown is built on top of prosemirror. Which means you can use the entire prosemirror API in Milkdown.
|
||||
To access the prosemirror API, you can use the `@milkdown/prose` package. It re-exports all of the prosemirror API.
|
||||
Using this package you can make sure that you are using the same version of prosemirror as Milkdown.
|
||||
|
||||
## Installation
|
||||
|
||||
To access a certain API in the `prosemirror-x` package, you need to import them from `@milkdown/kit/prose/x`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// Originally in prosemirror-state
|
||||
import { EditorState } from "@milkdown/kit/prose/state";
|
||||
// Originally in prosemirror-view
|
||||
import { EditorView } from "@milkdown/kit/prose/view";
|
||||
```
|
||||
|
||||
## List of packages
|
||||
|
||||
The following is a list of all the re-exported prosemirror API.
|
||||
|
||||
- `@milkdown/kit/prose/changeset`
|
||||
- `@milkdown/kit/prose/commands`
|
||||
- `@milkdown/kit/prose/dropcursor`
|
||||
- `@milkdown/kit/prose/gapcursor`
|
||||
- `@milkdown/kit/prose/history`
|
||||
- `@milkdown/kit/prose/inputrules`
|
||||
- `@milkdown/kit/prose/keymap`
|
||||
- `@milkdown/kit/prose/model`
|
||||
- `@milkdown/kit/prose/schema-list`
|
||||
- `@milkdown/kit/prose/state`
|
||||
- `@milkdown/kit/prose/transform`
|
||||
- `@milkdown/kit/prose/view`
|
||||
- `@milkdown/kit/prose/tables`
|
||||
|
||||
You can find the documentation of the prosemirror API [here](https://prosemirror.net/docs/ref/).
|
||||
@@ -0,0 +1,215 @@
|
||||
# Styling Guide
|
||||
|
||||
Milkdown is a headless editor, which means it doesn't come with any default styles. This gives you complete control over the appearance of your editor. You can either use existing themes or create your own custom styling solution.
|
||||
|
||||
# Styling Crepe Theme
|
||||
|
||||
---
|
||||
|
||||
Crepe is a collection of themes for Milkdown that provides both light and dark variants. The theme structure is organized as follows:
|
||||
|
||||
```
|
||||
theme/
|
||||
├── common/ # Shared styles and utilities
|
||||
├── crepe/ # Light theme variant
|
||||
├── crepe-dark/ # Dark theme variant
|
||||
├── frame/ # Frame theme (light)
|
||||
├── frame-dark/ # Frame theme (dark)
|
||||
├── nord/ # Nord theme (light)
|
||||
└── nord-dark/ # Nord theme (dark)
|
||||
```
|
||||
|
||||
## Using Crepe Theme
|
||||
|
||||
To use the Crepe theme in your project:
|
||||
|
||||
```ts
|
||||
// Import base styles first
|
||||
import "@milkdown/crepe/theme/common/style.css";
|
||||
|
||||
// Choose the theme you want to use
|
||||
import "@milkdown/crepe/theme/crepe.css";
|
||||
```
|
||||
|
||||
## Theme Variables
|
||||
|
||||
Crepe theme uses CSS variables for consistent styling. Here are all the available variables:
|
||||
|
||||
### Colors
|
||||
|
||||
```css
|
||||
.milkdown {
|
||||
/* Background Colors */
|
||||
--crepe-color-background: #fffdfb; /* Main background color */
|
||||
--crepe-color-surface: #fff8f4; /* Surface color for cards/panels */
|
||||
--crepe-color-surface-low: #fff1e5; /* Lower surface color for depth */
|
||||
|
||||
/* Text Colors */
|
||||
--crepe-color-on-background: #1f1b16; /* Text color on background */
|
||||
--crepe-color-on-surface: #201b13; /* Text color on surface */
|
||||
--crepe-color-on-surface-variant: #4f4539; /* Secondary text color */
|
||||
|
||||
/* Accent Colors */
|
||||
--crepe-color-primary: #805610; /* Primary brand color */
|
||||
--crepe-color-secondary: #fbdebc; /* Secondary accent color */
|
||||
--crepe-color-on-secondary: #271904; /* Text color on secondary */
|
||||
|
||||
/* UI Colors */
|
||||
--crepe-color-outline: #817567; /* Border/outline color */
|
||||
--crepe-color-inverse: #362f27; /* Inverse color for contrast */
|
||||
--crepe-color-on-inverse: #fcefe2; /* Text color on inverse */
|
||||
--crepe-color-inline-code: #ba1a1a; /* Inline code color */
|
||||
--crepe-color-error: #ba1a1a; /* Error state color */
|
||||
|
||||
/* Interactive Colors */
|
||||
--crepe-color-hover: #f9ecdf; /* Hover state color */
|
||||
--crepe-color-selected: #ede0d4; /* Selected state color */
|
||||
--crepe-color-inline-area: #e4d8cc; /* Inline editing area color */
|
||||
}
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
```css
|
||||
.milkdown {
|
||||
/* Font Families */
|
||||
--crepe-font-title: Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--crepe-font-default: "Open Sans", Arial, Helvetica, sans-serif;
|
||||
--crepe-font-code:
|
||||
Fira Code, Menlo, Monaco, "Courier New", Courier, monospace;
|
||||
}
|
||||
```
|
||||
|
||||
### Shadows
|
||||
|
||||
```css
|
||||
.milkdown {
|
||||
/* Small Shadow */
|
||||
--crepe-shadow-1:
|
||||
0px 1px 3px 1px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||
/* Large Shadow */
|
||||
--crepe-shadow-2:
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
```
|
||||
|
||||
## Customizing Crepe Theme
|
||||
|
||||
You can customize the Crepe theme by overriding its variables:
|
||||
|
||||
```css
|
||||
/* custom-overrides.css */
|
||||
.crepe .milkdown {
|
||||
/* Override colors */
|
||||
--crepe-color-primary: #your-primary-color;
|
||||
--crepe-color-background: #your-background-color;
|
||||
|
||||
/* Override typography */
|
||||
--crepe-font-default: "Your Font", sans-serif;
|
||||
|
||||
/* Override shadows */
|
||||
--crepe-shadow-1: your-shadow-value;
|
||||
}
|
||||
```
|
||||
|
||||
# Styling Milkdown
|
||||
|
||||
---
|
||||
|
||||
## Basic Styling
|
||||
|
||||
The editor is rendered within a container that has the class `.milkdown`, and the editable content area is wrapped in a container with the class `.editor`. You can use these classes to scope your styles:
|
||||
|
||||
```css
|
||||
/* Basic styling example */
|
||||
.milkdown .editor {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.milkdown .editor p {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
```
|
||||
|
||||
## Node and Mark Classes
|
||||
|
||||
Milkdown provides default class names for each node and mark. Here are some common examples:
|
||||
|
||||
```css
|
||||
/* Paragraph styling */
|
||||
.milkdown .editor .paragraph {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Heading styling */
|
||||
.milkdown .editor .heading {
|
||||
font-weight: 600;
|
||||
margin: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
/* List styling */
|
||||
.milkdown .editor .bullet-list {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.milkdown .editor .ordered-list {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Attributes
|
||||
|
||||
You can add custom attributes to nodes and marks, which is particularly useful when working with CSS frameworks like Tailwind CSS.
|
||||
|
||||
```typescript
|
||||
import { Editor, editorViewOptionsCtx } from "@milkdown/kit/core";
|
||||
import {
|
||||
commonmark,
|
||||
headingAttr,
|
||||
paragraphAttr,
|
||||
} from "@milkdown/kit/preset/commonmark";
|
||||
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
// Add attributes to the editor container
|
||||
ctx.update(editorViewOptionsCtx, (prev) => ({
|
||||
...prev,
|
||||
attributes: {
|
||||
class: "milkdown-editor mx-auto outline-hidden",
|
||||
spellcheck: "false",
|
||||
},
|
||||
}));
|
||||
|
||||
// Add attributes to nodes and marks
|
||||
ctx.set(headingAttr.key, (node) => {
|
||||
const level = node.attrs.level;
|
||||
return {
|
||||
class: `heading-${level} font-bold`,
|
||||
"data-level": level,
|
||||
};
|
||||
});
|
||||
|
||||
ctx.set(paragraphAttr.key, () => ({
|
||||
class: "text-base leading-relaxed",
|
||||
}));
|
||||
})
|
||||
.use(commonmark);
|
||||
```
|
||||
|
||||
# Best Practices
|
||||
|
||||
---
|
||||
|
||||
1. **Use CSS Variables**: Define your theme's colors and spacing using CSS variables for easy customization.
|
||||
2. **Responsive Design**: Ensure your editor styles work well on different screen sizes.
|
||||
3. **Dark Mode Support**: Consider adding dark mode support using CSS variables and media queries.
|
||||
4. **Accessibility**: Maintain good contrast ratios and readable font sizes.
|
||||
5. **Performance**: Keep your CSS selectors specific and avoid overly complex rules.
|
||||
|
||||
For more examples and inspiration, check out:
|
||||
|
||||
- [@milkdown/theme-nord](https://github.com/Milkdown/milkdown/tree/main/packages/theme-nord)
|
||||
- [@milkdown/crepe/theme](https://github.com/Milkdown/milkdown/tree/main/packages/crepe/src/theme)
|
||||
@@ -0,0 +1,234 @@
|
||||
# Using Crepe Editor
|
||||
|
||||
Crepe is a powerful, feature-rich Markdown editor built on top of Milkdown. It provides a complete editing experience with a beautiful UI and extensive customization options.
|
||||
|
||||
## Why Choose Crepe?
|
||||
|
||||
---
|
||||
|
||||
- 🚀 **Ready to Use**: Works out of the box with sensible defaults
|
||||
- 🎨 **Beautiful UI**: Modern design with multiple theme options
|
||||
- 🔧 **Highly Customizable**: Extensive configuration options
|
||||
- 📦 **Feature Complete**: Includes all essential Markdown editing features
|
||||
- 🛠️ **Extensible**: Built on Milkdown's plugin system
|
||||
|
||||
## Quick Start
|
||||
|
||||
---
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @milkdown/crepe
|
||||
|
||||
# Using yarn
|
||||
yarn add @milkdown/crepe
|
||||
|
||||
# Using pnpm
|
||||
pnpm add @milkdown/crepe
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Crepe } from "@milkdown/crepe";
|
||||
import "@milkdown/crepe/theme/common/style.css";
|
||||
import "@milkdown/crepe/theme/frame.css";
|
||||
|
||||
// Choose your preferred theme
|
||||
|
||||
// Create editor instance
|
||||
const crepe = new Crepe({
|
||||
root: document.getElementById("app"),
|
||||
defaultValue: "# Hello, Crepe!\n\nStart writing your markdown...",
|
||||
});
|
||||
|
||||
// Initialize the editor
|
||||
await crepe.create();
|
||||
|
||||
// Clean up when done
|
||||
crepe.destroy();
|
||||
```
|
||||
|
||||
::iframe{src="https://stackblitz.com/github/Milkdown/examples/tree/main/editor-crepe"}
|
||||
|
||||
## Themes
|
||||
|
||||
---
|
||||
|
||||
Crepe comes with several beautiful themes out of the box:
|
||||
|
||||
### Light Themes
|
||||
|
||||
- `frame` - Modern frame-based design
|
||||
- `classic` - Traditional editor look
|
||||
- `nord` - Clean, minimal Nord color scheme
|
||||
|
||||
### Dark Themes
|
||||
|
||||
- `frame-dark` - Dark version of frame theme
|
||||
- `classic-dark` - Dark version of classic theme
|
||||
- `nord-dark` - Dark version of nord theme
|
||||
|
||||
To use a theme:
|
||||
|
||||
```typescript
|
||||
// Import base styles first
|
||||
import "@milkdown/crepe/theme/common/style.css";
|
||||
// Then import your chosen theme
|
||||
import "@milkdown/crepe/theme/frame.css";
|
||||
```
|
||||
|
||||
### Custom Themes
|
||||
|
||||
You can create your own theme by extending the base styles. Check out the [existing themes](https://github.com/Milkdown/milkdown/tree/main/packages/crepe/src/theme) for reference.
|
||||
|
||||
## Features
|
||||
|
||||
---
|
||||
|
||||
Crepe includes a comprehensive set of features that can be enabled or disabled as needed.
|
||||
|
||||
### Feature Configuration
|
||||
|
||||
> **Note**: For any configuration that ends with `Icon` (like `boldIcon`, `linkIcon`, etc.), you can use a HTML string or a simply string. This applies to all icon configurations throughout Crepe's features.
|
||||
|
||||
```typescript
|
||||
const crepe = new Crepe({
|
||||
features: {
|
||||
// Disable specific features
|
||||
[Crepe.Feature.CodeMirror]: false,
|
||||
[Crepe.Feature.Table]: false,
|
||||
},
|
||||
featureConfigs: {
|
||||
// Configure feature behavior
|
||||
[Crepe.Feature.LinkTooltip]: {
|
||||
inputPlaceholder: "Enter URL...",
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Available Features
|
||||
|
||||
#### 1. Code Editor (`CodeMirror`)
|
||||
|
||||
Syntax highlighting and editing for code blocks with language support, theme customization, and preview capabilities.
|
||||
|
||||
#### 2. List Management (`ListItem`)
|
||||
|
||||
Support for bullet lists, ordered lists, and todo lists with customizable icons and formatting.
|
||||
|
||||
#### 3. Link Management (`LinkTooltip`)
|
||||
|
||||
Enhanced link editing and preview with customizable tooltips, edit/remove actions, and copy functionality.
|
||||
|
||||
#### 4. Image Handling (`ImageBlock`)
|
||||
|
||||
Image upload and management with resizing, captions, and support for both inline and block images.
|
||||
|
||||
#### 5. Block Editing (`BlockEdit`)
|
||||
|
||||
Drag-and-drop block management and slash commands for quick content insertion and organization.
|
||||
|
||||
#### 6. Table Support (`Table`)
|
||||
|
||||
Full-featured table editing with row/column management, alignment options, and drag-and-drop functionality.
|
||||
|
||||
#### 7. Toolbar (`Toolbar`)
|
||||
|
||||
Formatting toolbar for selected text with customizable icons and actions.
|
||||
|
||||
#### 8. Cursor (`Cursor`)
|
||||
|
||||
Enhanced cursor experience with drop cursor and gap cursor for better content placement.
|
||||
|
||||
#### 9. Placeholder (`Placeholder`)
|
||||
|
||||
Document or block level placeholders to guide users when content is empty.
|
||||
|
||||
#### 10. Latex (`Latex`)
|
||||
|
||||
Mathematical formula support with both inline and block math rendering using KaTeX.
|
||||
|
||||
For detailed configuration options of each feature, please refer to the [API documentation](/docs/api/crepe).
|
||||
|
||||
## Editor Instance Methods
|
||||
|
||||
---
|
||||
|
||||
#### `crepe.editor`
|
||||
|
||||
Access the underlying Milkdown editor instance.
|
||||
|
||||
```typescript
|
||||
const editor = crepe.editor;
|
||||
editor.use(customPlugin);
|
||||
editor.action(insert("Hello"));
|
||||
```
|
||||
|
||||
#### `crepe.create()`
|
||||
|
||||
Initialize the editor.
|
||||
|
||||
```typescript
|
||||
await crepe.create();
|
||||
```
|
||||
|
||||
#### `crepe.destroy()`
|
||||
|
||||
Clean up the editor instance.
|
||||
|
||||
```typescript
|
||||
crepe.destroy();
|
||||
```
|
||||
|
||||
#### `crepe.setReadonly(value: boolean)`
|
||||
|
||||
Toggle readonly mode.
|
||||
|
||||
```typescript
|
||||
crepe.setReadonly(true); // Make editor read-only
|
||||
crepe.setReadonly(false); // Make editor editable
|
||||
```
|
||||
|
||||
#### `crepe.on`
|
||||
|
||||
Add event listeners.
|
||||
|
||||
```typescript
|
||||
crepe.on((listener) => {
|
||||
listener.markdownUpdated((markdown) => {
|
||||
console.log("Markdown updated:", markdown);
|
||||
});
|
||||
|
||||
listener.updated((doc) => {
|
||||
console.log("Document updated");
|
||||
});
|
||||
|
||||
listener.focus(() => {
|
||||
console.log("Editor focused");
|
||||
});
|
||||
|
||||
listener.blur(() => {
|
||||
console.log("Editor blurred");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### `crepe.getMarkdown()`
|
||||
|
||||
Get current markdown content.
|
||||
|
||||
```typescript
|
||||
const markdown = crepe.getMarkdown();
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
---
|
||||
|
||||
- Learn about [Milkdown's architecture](/docs/guide/architecture-overview)
|
||||
- Explore [available plugins](/docs/plugin/using-plugins)
|
||||
- Read the [API reference](/docs/api/crepe)
|
||||
@@ -0,0 +1,37 @@
|
||||
# Using @milkdown/kit
|
||||
|
||||
Milkdown provides a set of utilities to help you build your editor.
|
||||
These utilities are re-exported from the `@milkdown/kit` package.
|
||||
Thus, you don't need to install the common dependencies manually like `@milkdown/prose`, `@milkdown/core` or `@milkdown/preset-common` in your project.
|
||||
|
||||
## What's included
|
||||
|
||||
`@milkdown/kit` re-exports the following packages:
|
||||
|
||||
| Package | Import path | Scope |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | --------- |
|
||||
| [@milkdown/core](/docs/api/core) | `@milkdown/kit/core` | Framework |
|
||||
| [@milkdown/ctx](/docs/api/ctx) | `@milkdown/kit/ctx` | Framework |
|
||||
| [@milkdown/prose](/docs/guide/prosemirror-api) | `@milkdown/kit/prose` | Framework |
|
||||
| [@milkdown/prose/\*](/docs/guide/prosemirror-api) | `@milkdown/kit/prose/*` | Framework |
|
||||
| [@milkdown/transformer](/docs/api/transformer) | `@milkdown/kit/transformer` | Framework |
|
||||
| [@milkdown/utils](/docs/api/utils) | `@milkdown/kit/utils` | Framework |
|
||||
| [@milkdown/preset-commonmark](/docs/api/preset-commonmark) | `@milkdown/kit/preset/commonmark` | Preset |
|
||||
| [@milkdown/preset-gfm](/docs/api/preset-gfm) | `@milkdown/kit/preset/gfm` | Preset |
|
||||
| [@milkdown/plugin-block](/docs/api/plugin-block) | `@milkdown/kit/plugin/block` | Plugin |
|
||||
| [@milkdown/plugin-clipboard](/docs/api/plugin-clipboard) | `@milkdown/kit/plugin/clipboard` | Plugin |
|
||||
| [@milkdown/plugin-cursor](/docs/api/plugin-cursor) | `@milkdown/kit/plugin/cursor` | Plugin |
|
||||
| [@milkdown/plugin-history](/docs/api/plugin-history) | `@milkdown/kit/plugin/history` | Plugin |
|
||||
| [@milkdown/plugin-indent](/docs/api/plugin-indent) | `@milkdown/kit/plugin/indent` | Plugin |
|
||||
| [@milkdown/plugin-listener](/docs/api/plugin-listener) | `@milkdown/kit/plugin/listener` | Plugin |
|
||||
| [@milkdown/plugin-slash](/docs/api/plugin-slash) | `@milkdown/kit/plugin/slash` | Plugin |
|
||||
| [@milkdown/plugin-tooltip](/docs/api/plugin-tooltip) | `@milkdown/kit/plugin/tooltip` | Plugin |
|
||||
| [@milkdown/plugin-trailing](/docs/api/plugin-trailing) | `@milkdown/kit/plugin/trailing` | Plugin |
|
||||
| [@milkdown/plugin-upload](/docs/api/plugin-upload) | `@milkdown/kit/plugin/upload` | Plugin |
|
||||
| @milkdown/component | `@milkdown/kit/component` | Component |
|
||||
| @milkdown/component/code-block | `@milkdown/kit/component/code-block` | Component |
|
||||
| @milkdown/component/image-block | `@milkdown/kit/component/image-block` | Component |
|
||||
| @milkdown/component/image-inline | `@milkdown/kit/component/image-inline` | Component |
|
||||
| @milkdown/component/link-tooltip | `@milkdown/kit/component/link-tooltip` | Component |
|
||||
| @milkdown/component/list-item-block | `@milkdown/kit/component/list-item-block` | Component |
|
||||
| @milkdown/component/table-block | `@milkdown/kit/component/table-block` | Component |
|
||||
@@ -0,0 +1,30 @@
|
||||
# Why Milkdown
|
||||
|
||||
There are different kinds of markdown editors, such as [Typora](https://typora.io/), [tui](https://github.com/nhn/tui.editor) and [Bear](https://bear.app/).
|
||||
They work pretty well for writing notes in markdown on different platforms. So why bother making Milkdown?
|
||||
|
||||
Milkdown aims to provide an **open source solution** for developers to make their editors more powerful, and attractive, it also ensures it runs everywhere.
|
||||
|
||||
---
|
||||
|
||||
## Open Source & Easy to Integrate
|
||||
|
||||
Different from industrial apps such as [Notion](https://notion.so) and [Typora](https://typora.io/),
|
||||
Milkdown is open source and fully free. You can integrate it everywhere legally.
|
||||
|
||||
> If you like milkdown, please consider to fund me in order to help with the maintenance.
|
||||
|
||||
## Plugin Driven
|
||||
|
||||
Milkdown treats every feature as a plugin.
|
||||
With this pattern, developers can choose what they need in an editor instead of bundling all features even they won't need.
|
||||
Developers can extend their plugins to satisfy their habits such as defining a vim keymap via a custom plugin.
|
||||
|
||||
## Reliable
|
||||
|
||||
Milkdown is powered by [Prosemirror](https://prosemirror.net/) and [Remark](https://github.com/remarkjs/remark), which has a large community and stands the test of the industry.
|
||||
What's more, plugins from the prosemirror and remark community can be easily reused in order to build a Milkdown plugin.
|
||||
|
||||
## Themable & Hackable
|
||||
|
||||
Themes and plugins for Milkdown can be shared and installed using npm packages. Milkdown is a headless component, which means you can fully control its style.
|
||||
Reference in New Issue
Block a user