The core module of milkdown - a plugin-driven WYSIWYG markdown Editor built on top of prosemirror and remark
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
The command system provides type-safe command registration and execution for editor actions. Commands are the primary way to interact with and modify the editor state in Milkdown.
The central command management system that handles registration, retrieval, and execution of commands.
/**
* The command manager that handles all editor commands
*/
class CommandManager {
/** Register a command with the manager */
create<T>(meta: CmdKey<T>, value: Cmd<T>): SliceType<Cmd<T>>;
/** Get a registered command by its key */
get<T extends CmdKey<any>>(slice: string): Cmd<InferParams<T>>;
get<T>(slice: CmdKey<T>): Cmd<T>;
/** Remove a command from the manager */
remove<T extends CmdKey<any>>(slice: string): void;
remove<T>(slice: CmdKey<T>): void;
/** Execute a registered command with optional payload */
call<T extends CmdKey<any>>(slice: string, payload?: InferParams<T>): boolean;
call<T>(slice: CmdKey<T>, payload?: T): boolean;
/** Execute an inline ProseMirror command directly */
inline(command: Command): boolean;
/** Create a command chain for sequential execution */
chain(): CommandChain;
/** Get the current editor context */
readonly ctx: Ctx | null;
}Usage Examples:
import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
// Create a command key
const toggleBoldKey = createCmdKey<void>('toggleBold');
// Register the command
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
commands.create(toggleBoldKey, () => (state, dispatch) => {
// ProseMirror command implementation
const { schema } = state;
const markType = schema.marks.strong;
return toggleMark(markType)(state, dispatch);
});
});
// Execute the command
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
const success = commands.call(toggleBoldKey);
console.log('Command executed:', success);
});Interface for chaining multiple commands together with sequential execution.
/**
* A chainable command helper for executing multiple commands in sequence
*/
interface CommandChain {
/** Execute all commands in the chain */
run(): boolean;
/** Add a ProseMirror command directly to the chain */
inline(command: Command): CommandChain;
/** Add a registered command to the chain with optional payload */
pipe<T extends CmdKey<any>>(slice: string, payload?: InferParams<T>): CommandChain;
pipe<T>(slice: CmdKey<T>, payload?: T): CommandChain;
pipe(slice: string | CmdKey<any>, payload?: any): CommandChain;
}Usage Examples:
import { Editor, commandsCtx } from "@milkdown/core";
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
// Chain multiple commands
const success = commands.chain()
.pipe(toggleBoldKey)
.inline((state, dispatch) => {
// Inline command
return selectAll(state, dispatch);
})
.pipe(insertTextKey, 'Hello World')
.run();
console.log('Chain executed:', success);
});Function to create type-safe command keys for command registration.
/**
* Create a command key for type-safe command registration
* @param key - Optional identifier for the command key
* @returns A typed command key slice
*/
function createCmdKey<T = undefined>(key?: string): CmdKey<T>;Usage Examples:
import { createCmdKey } from "@milkdown/core";
// Command with no payload
const saveDocumentKey = createCmdKey<void>('saveDocument');
// Command with string payload
const insertTextKey = createCmdKey<string>('insertText');
// Command with complex payload
interface FormatOptions {
bold?: boolean;
italic?: boolean;
color?: string;
}
const formatTextKey = createCmdKey<FormatOptions>('formatText');
// Usage
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
commands.call(saveDocumentKey);
commands.call(insertTextKey, 'Hello World');
commands.call(formatTextKey, { bold: true, color: 'red' });
});The context slice that provides access to the command manager.
/**
* Context slice containing the command manager instance
*/
const commandsCtx: SliceType<CommandManager, 'commands'>;/**
* Command function type that returns a ProseMirror command
* @param payload - Optional payload data for the command
* @returns ProseMirror Command function
*/
type Cmd<T = undefined> = (payload?: T) => Command;
/**
* Command key type for type-safe command registration
*/
type CmdKey<T = undefined> = SliceType<Cmd<T>>;
/**
* Type helper to infer command payload types from CmdKey
*/
type InferParams<T> = T extends CmdKey<infer U> ? U : never;import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
import { setBlockType } from "@milkdown/prose/commands";
// Create a custom heading command
const setHeadingKey = createCmdKey<{ level: number }>('setHeading');
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
commands.create(setHeadingKey, ({ level }) => (state, dispatch) => {
const { schema } = state;
const headingType = schema.nodes.heading;
if (!headingType) return false;
return setBlockType(headingType, { level })(state, dispatch);
});
});
// Use the command
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
commands.call(setHeadingKey, { level: 2 });
});import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
// Compose multiple operations into a single command
const formatParagraphKey = createCmdKey<{
bold?: boolean;
italic?: boolean;
align?: 'left' | 'center' | 'right';
}>('formatParagraph');
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
commands.create(formatParagraphKey, (options) => (state, dispatch, view) => {
// Chain multiple formatting commands
return commands.chain()
.pipe(toggleBoldKey)
.pipe(toggleItalicKey)
.pipe(setAlignmentKey, options.align || 'left')
.run();
});
});import { Editor, commandsCtx } from "@milkdown/core";
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
// Execute command only if selection is not empty
const success = commands.inline((state, dispatch) => {
if (state.selection.empty) {
console.log('Cannot format empty selection');
return false;
}
return commands.call(formatSelectionKey, { bold: true });
});
});import { Editor, commandsCtx } from "@milkdown/core";
import { callCommandBeforeEditorView } from "@milkdown/exception";
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
try {
const success = commands.call(someCommandKey, payload);
if (!success) {
console.warn('Command execution failed - command returned false');
}
} catch (error) {
if (error === callCommandBeforeEditorView()) {
console.error('Cannot execute command before editor view is ready');
} else {
console.error('Command execution error:', error);
}
}
});import { Editor, commandsCtx, createCmdKey } from "@milkdown/core";
const myCommandKey = createCmdKey<string>('myCommand');
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
// Check if command already exists before registering
try {
const existing = commands.get(myCommandKey);
console.log('Command already registered');
} catch {
// Command doesn't exist, safe to register
commands.create(myCommandKey, (text) => (state, dispatch) => {
// Command implementation
return true;
});
}
});The command system seamlessly integrates with ProseMirror's command system:
import { Editor, commandsCtx } from "@milkdown/core";
import {
toggleMark,
wrapIn,
setBlockType,
chainCommands,
exitCode
} from "@milkdown/prose/commands";
editor.action((ctx) => {
const commands = ctx.get(commandsCtx);
// Use ProseMirror commands directly
commands.inline(toggleMark(schema.marks.strong));
// Chain ProseMirror commands
commands.inline(chainCommands(
exitCode,
(state, dispatch) => {
// Custom logic
return true;
}
));
});