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
Internal plugins provide the fundamental functionality required for editor operation. These plugins are automatically loaded during editor creation and form the foundation upon which all other features are built.
The essential plugins that provide basic editor functionality.
/**
* Core internal plugins automatically loaded by the editor
*/
/** Creates the ProseMirror schema from registered nodes and marks */
const schema: MilkdownPlugin;
/** Creates the markdown-to-ProseMirror parser */
const parser: MilkdownPlugin;
/** Creates the ProseMirror-to-markdown serializer */
const serializer: MilkdownPlugin;
/** Initializes the command management system */
const commands: MilkdownPlugin;
/** Initializes the keymap management system */
const keymap: MilkdownPlugin;
/** Creates the ProseMirror editor state */
const editorState: MilkdownPlugin;
/** Creates the ProseMirror editor view and mounts it to DOM */
const editorView: MilkdownPlugin;Usage:
These plugins are automatically loaded by the editor. You typically don't need to use them directly, but they're available for inspection or custom editor setups:
import { Editor, schema, parser, commands } from "@milkdown/core";
// These are loaded automatically
const editor = Editor.make().create();
// For custom setups, you could theoretically use them individually
// (though this is not recommended for normal usage)
const customPlugin: MilkdownPlugin = (ctx) => {
return async () => {
await ctx.wait(SchemaReady);
await ctx.wait(ParserReady);
await ctx.wait(CommandsReady);
// Your plugin logic here
};
};Plugins that handle editor configuration and initialization.
/**
* Configuration and initialization plugin factories
*/
/**
* Creates a configuration plugin that executes user configuration
* @param configure - Configuration function to execute
* @returns MilkdownPlugin for configuration
*/
function config(configure: Config): MilkdownPlugin;
/**
* Creates the initialization plugin that prepares core slices
* @param editor - The editor instance to initialize
* @returns MilkdownPlugin for initialization
*/
function init(editor: Editor): MilkdownPlugin;Usage Examples:
import { Editor, config } from "@milkdown/core";
// Create configuration plugin
const myConfigPlugin = config(async (ctx) => {
// Configure editor context
ctx.set(defaultValueCtx, '# Hello World');
ctx.set(rootCtx, document.getElementById('editor'));
// Async configuration
const theme = await loadUserTheme();
ctx.set(themeCtx, theme);
});
// The editor automatically creates and uses config plugins
// from functions passed to .config()
const editor = Editor.make()
.config((ctx) => {
// This creates a config plugin internally
ctx.set(defaultValueCtx, 'Initial content');
})
.create();Timers that coordinate plugin loading order and dependencies.
/**
* Plugin timing system - timers that resolve when plugins are ready
*/
/** Resolved when configuration plugin completes */
const ConfigReady: TimerType;
/** Resolved when initialization plugin completes */
const InitReady: TimerType;
/** Resolved when schema plugin completes */
const SchemaReady: TimerType;
/** Resolved when parser plugin completes */
const ParserReady: TimerType;
/** Resolved when serializer plugin completes */
const SerializerReady: TimerType;
/** Resolved when commands plugin completes */
const CommandsReady: TimerType;
/** Resolved when keymap plugin completes */
const KeymapReady: TimerType;
/** Resolved when editor state plugin completes */
const EditorStateReady: TimerType;
/** Resolved when editor view plugin completes */
const EditorViewReady: TimerType;Usage Examples:
import {
MilkdownPlugin,
SchemaReady,
ParserReady,
CommandsReady
} from "@milkdown/core";
const myPlugin: MilkdownPlugin = (ctx) => {
return async () => {
// Wait for required plugins to be ready
await ctx.wait(SchemaReady);
await ctx.wait(ParserReady);
await ctx.wait(CommandsReady);
// Now safe to use schema, parser, and commands
const schema = ctx.get(schemaCtx);
const parser = ctx.get(parserCtx);
const commands = ctx.get(commandsCtx);
// Plugin initialization logic
};
};Internal plugins load in a specific order to ensure proper dependencies:
import {
ConfigReady,
InitReady,
SchemaReady,
ParserReady,
SerializerReady,
CommandsReady,
KeymapReady,
EditorStateReady,
EditorViewReady
} from "@milkdown/core";
// Example plugin that waits for multiple stages
const advancedPlugin: MilkdownPlugin = (ctx) => {
return async () => {
// Wait for basic setup
await ctx.wait(InitReady);
console.log('Editor initialized');
// Wait for content processing
await ctx.wait(ParserReady);
await ctx.wait(SerializerReady);
console.log('Content processing ready');
// Wait for interaction systems
await ctx.wait(CommandsReady);
await ctx.wait(KeymapReady);
console.log('Interaction systems ready');
// Wait for final editor
await ctx.wait(EditorViewReady);
console.log('Editor fully ready');
};
};import { MilkdownPlugin, createTimer } from "@milkdown/ctx";
import { SchemaReady, CommandsReady } from "@milkdown/core";
// Create custom timer
const MyPluginReady = createTimer('MyPluginReady');
const myPlugin: MilkdownPlugin = (ctx) => {
ctx.record(MyPluginReady);
return async () => {
// Wait for dependencies
await ctx.wait(SchemaReady);
await ctx.wait(CommandsReady);
// Plugin logic
setupMyPlugin(ctx);
// Signal ready
ctx.done(MyPluginReady);
return () => {
// Cleanup
ctx.clearTimer(MyPluginReady);
};
};
};
// Another plugin can wait for your plugin
const dependentPlugin: MilkdownPlugin = (ctx) => {
return async () => {
await ctx.wait(MyPluginReady);
// This runs after myPlugin is ready
};
};import { Editor, schemaTimerCtx, SchemaReady } from "@milkdown/core";
// Plugin that modifies internal plugin timing
const schemaModifier: MilkdownPlugin = (ctx) => {
return async () => {
// Add additional dependency for schema plugin
ctx.update(schemaTimerCtx, (timers) => [
...timers,
MyCustomTimer
]);
// Schema plugin will now wait for MyCustomTimer too
};
};import { Editor, schemaCtx, parserCtx, serializerCtx } from "@milkdown/core";
const inspectorPlugin: MilkdownPlugin = (ctx) => {
return async () => {
await ctx.wait(SchemaReady);
await ctx.wait(ParserReady);
await ctx.wait(SerializerReady);
// Access internal plugin state
const schema = ctx.get(schemaCtx);
const parser = ctx.get(parserCtx);
const serializer = ctx.get(serializerCtx);
console.log('Available nodes:', Object.keys(schema.nodes));
console.log('Available marks:', Object.keys(schema.marks));
// Test parser/serializer
const markdown = '# Hello World';
const doc = parser(markdown);
const backToMarkdown = serializer(doc);
console.log('Round trip:', { markdown, backToMarkdown });
};
};/**
* Type for configuration functions executed during editor initialization
* @param ctx - The editor context to configure
*/
type Config = (ctx: Ctx) => void | Promise<void>;/** Editor view options type (ProseMirror DirectEditorProps without state) */
type EditorOptions = Omit<DirectEditorProps, 'state'>;
/** Root element types for editor mounting */
type RootType = Node | undefined | null | string;
/** Editor state creation options override function */
type StateOptionsOverride = (prev: StateOptions) => StateOptions;Examples:
import { Config, defaultValueCtx, rootCtx } from "@milkdown/core";
// Synchronous configuration
const syncConfig: Config = (ctx) => {
ctx.set(defaultValueCtx, '# Hello');
ctx.set(rootCtx, document.body);
};
// Asynchronous configuration
const asyncConfig: Config = async (ctx) => {
const content = await fetch('/api/content').then(r => r.text());
ctx.set(defaultValueCtx, content);
const userPrefs = await loadUserPreferences();
ctx.set(themeCtx, userPrefs.theme);
};
// Error handling in configuration
const safeConfig: Config = async (ctx) => {
try {
const content = await fetch('/api/content').then(r => r.text());
ctx.set(defaultValueCtx, content);
} catch (error) {
console.warn('Failed to load remote content, using default');
ctx.set(defaultValueCtx, '# Welcome');
}
};Internal plugins include metadata for debugging and inspection:
import { Editor } from "@milkdown/core";
const editor = Editor.make()
.enableInspector(true)
.create();
// Get telemetry for internal plugins
const telemetry = editor.inspect();
telemetry.forEach(item => {
console.log(`Plugin: ${item.displayName}`);
console.log(`Package: ${item.package}`);
console.log(`Group: ${item.group}`);
});
// Internal plugins have metadata like:
// { displayName: 'Schema', package: '@milkdown/core', group: 'System' }
// { displayName: 'Parser', package: '@milkdown/core', group: 'System' }
// { displayName: 'Commands', package: '@milkdown/core', group: 'System' }import { Editor } from "@milkdown/core";
try {
const editor = await Editor.make()
.config((ctx) => {
// Potentially failing configuration
if (!document.getElementById('editor')) {
throw new Error('Editor mount point not found');
}
})
.create();
} catch (error) {
console.error('Editor creation failed:', error);
// Handle plugin loading failure
}import { MilkdownPlugin } from "@milkdown/ctx";
import { SchemaReady } from "@milkdown/core";
const timeoutPlugin: MilkdownPlugin = (ctx) => {
return async () => {
try {
// Wait with timeout
await Promise.race([
ctx.wait(SchemaReady),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Schema timeout')), 5000)
)
]);
} catch (error) {
console.error('Plugin loading timed out:', error);
throw error;
}
};
};// Good: Explicit dependency waiting
const myPlugin: MilkdownPlugin = (ctx) => {
return async () => {
await ctx.wait(SchemaReady);
await ctx.wait(CommandsReady);
// Safe to use schema and commands
};
};
// Avoid: Assuming plugins are ready
const badPlugin: MilkdownPlugin = (ctx) => {
return async () => {
const schema = ctx.get(schemaCtx); // May not be ready!
};
};import { createTimer } from "@milkdown/ctx";
// Good: Descriptive timer names
const DataLoaderReady = createTimer('DataLoaderReady');
const ThemeManagerReady = createTimer('ThemeManagerReady');
// Good: Proper timer lifecycle
const myPlugin: MilkdownPlugin = (ctx) => {
ctx.record(DataLoaderReady);
return async () => {
// Plugin logic
ctx.done(DataLoaderReady);
return () => {
ctx.clearTimer(DataLoaderReady); // Important cleanup
};
};
};// Good: Organized configuration
const editorConfig: Config = async (ctx) => {
// Content configuration
ctx.set(defaultValueCtx, await loadContent());
// UI configuration
ctx.set(rootCtx, document.getElementById('editor'));
ctx.set(rootAttrsCtx, { class: 'editor-theme' });
// Feature configuration
ctx.update(inputRulesCtx, (rules) => [...rules, ...customRules]);
};
// Avoid: Scattered configuration
const messyConfig: Config = (ctx) => {
ctx.set(defaultValueCtx, 'content');
ctx.update(inputRulesCtx, (rules) => [...rules, rule1]);
ctx.set(rootCtx, document.body);
ctx.update(inputRulesCtx, (rules) => [...rules, rule2]); // Redundant
};