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 context system is the foundational dependency injection and state management system that powers all Milkdown functionality. It provides type-safe access to shared state, configuration, and services throughout the editor lifecycle.
The primary context slices that manage essential editor state and functionality.
/**
* Core editor state and view management slices
*/
/** Contains the ProseMirror editor view instance */
const editorViewCtx: SliceType<EditorView, 'editorView'>;
/** Contains the ProseMirror editor state */
const editorStateCtx: SliceType<EditorState, 'editorState'>;
/** Contains the main editor instance */
const editorCtx: SliceType<Editor, 'editor'>;
/** Contains the ProseMirror schema */
const schemaCtx: SliceType<Schema, 'schema'>;
/** Contains the markdown-to-ProseMirror parser */
const parserCtx: SliceType<Parser, 'parser'>;
/** Contains the ProseMirror-to-markdown serializer */
const serializerCtx: SliceType<Serializer, 'serializer'>;
/** Contains the command manager instance */
const commandsCtx: SliceType<CommandManager, 'commands'>;
/** Contains the keymap manager instance */
const keymapCtx: SliceType<KeymapManager, 'keymap'>;Usage Examples:
import { Editor, editorStateCtx, editorViewCtx, schemaCtx } from "@milkdown/core";
editor.action((ctx) => {
// Access core editor components
const state = ctx.get(editorStateCtx);
const view = ctx.get(editorViewCtx);
const schema = ctx.get(schemaCtx);
console.log('Current selection:', state.selection);
console.log('Editor DOM element:', view.dom);
console.log('Available nodes:', Object.keys(schema.nodes));
});Context slices for editor configuration and customization.
/**
* Configuration and customization slices
*/
/** The default content for editor initialization (string, HTML, or JSON) */
const defaultValueCtx: SliceType<DefaultValue, 'defaultValue'>;
/** The root element or selector for editor mounting */
const rootCtx: SliceType<RootType, 'root'>;
/** Attributes to apply to the root container element */
const rootAttrsCtx: SliceType<Record<string, string>, 'rootAttrs'>;
/** The actual root DOM element after resolution */
const rootDOMCtx: SliceType<HTMLElement, 'rootDOM'>;
/** Options passed to the ProseMirror EditorView constructor */
const editorViewOptionsCtx: SliceType<Partial<EditorOptions>, 'editorViewOptions'>;
/** Function to override editor state creation options */
const editorStateOptionsCtx: SliceType<StateOptionsOverride, 'editorStateOptions'>;Usage Examples:
import { Editor, defaultValueCtx, rootCtx, rootAttrsCtx } from "@milkdown/core";
const editor = Editor.make()
.config((ctx) => {
// Set initial content
ctx.set(defaultValueCtx, '# Welcome\n\nStart writing...');
// Set root element
ctx.set(rootCtx, document.getElementById('editor'));
// Add CSS classes and attributes to root
ctx.set(rootAttrsCtx, {
'class': 'my-editor-theme dark-mode',
'data-testid': 'milkdown-editor'
});
});Context slices that store various types of plugins and rules used by the editor.
/**
* Plugin and rule storage slices
*/
/** Array of ProseMirror input rules for text transformations */
const inputRulesCtx: SliceType<InputRule[], 'inputRules'>;
/** Array of ProseMirror plugins for editor behavior */
const prosePluginsCtx: SliceType<Plugin[], 'prosePlugins'>;
/** Array of remark plugins for markdown processing */
const remarkPluginsCtx: SliceType<RemarkPlugin[], 'remarkPlugins'>;
/** Array of custom node view constructors */
const nodeViewCtx: SliceType<NodeView[], 'nodeView'>;
/** Array of custom mark view constructors */
const markViewCtx: SliceType<MarkView[], 'markView'>;Usage Examples:
import {
Editor,
inputRulesCtx,
prosePluginsCtx,
nodeViewCtx
} from "@milkdown/core";
import { Plugin } from "@milkdown/prose/state";
import { InputRule } from "@milkdown/prose/inputrules";
editor.config((ctx) => {
// Add input rules
ctx.update(inputRulesCtx, (rules) => [
...rules,
new InputRule(/--$/, '—'), // Convert -- to em dash
new InputRule(/\.\.\./, '…') // Convert ... to ellipsis
]);
// Add ProseMirror plugins
ctx.update(prosePluginsCtx, (plugins) => [
...plugins,
new Plugin({
key: new PluginKey('myCustomPlugin'),
// Plugin implementation
})
]);
// Add custom node views
ctx.update(nodeViewCtx, (views) => [
...views,
['image', MyImageNodeView],
['code_block', MyCodeBlockView]
]);
});Context slices for defining the editor's document schema.
/**
* Schema definition slices
*/
/** Array of node specifications for the schema */
const nodesCtx: SliceType<Array<[string, NodeSchema]>, 'nodes'>;
/** Array of mark specifications for the schema */
const marksCtx: SliceType<Array<[string, MarkSchema]>, 'marks'>;Usage Examples:
import { Editor, nodesCtx, marksCtx } from "@milkdown/core";
editor.config((ctx) => {
// Add custom nodes
ctx.update(nodesCtx, (nodes) => [
...nodes,
['callout', {
content: 'block+',
group: 'block',
defining: true,
attrs: {
type: { default: 'info' }
},
parseDOM: [{ tag: 'div.callout' }],
toDOM: (node) => ['div', { class: `callout ${node.attrs.type}` }, 0]
}]
]);
// Add custom marks
ctx.update(marksCtx, (marks) => [
...marks,
['highlight', {
attrs: {
color: { default: 'yellow' }
},
parseDOM: [{ tag: 'mark' }],
toDOM: (mark) => ['mark', { style: `background-color: ${mark.attrs.color}` }]
}]
]);
});Context slices for markdown processing with remark.
/**
* Remark processing slices
*/
/** The remark processor instance for markdown parsing/serialization */
const remarkCtx: SliceType<RemarkParser, 'remark'>;
/** Options for remark stringify operation */
const remarkStringifyOptionsCtx: SliceType<Options, 'remarkStringifyOptions'>;Usage Examples:
import { Editor, remarkCtx, remarkStringifyOptionsCtx } from "@milkdown/core";
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
editor.config((ctx) => {
// Configure remark processor
ctx.update(remarkCtx, (remark) =>
remark.use(remarkGfm).use(remarkMath)
);
// Configure stringify options
ctx.update(remarkStringifyOptionsCtx, (options) => ({
...options,
bullet: '-', // Use - for bullets
emphasis: '*', // Use * for emphasis
strong: '**', // Use ** for strong
listItemIndent: 'one'
}));
});Context slices that manage plugin loading order and dependencies.
/**
* Timer management slices for plugin coordination
*/
/** Timers to wait for before initializing the init plugin */
const initTimerCtx: SliceType<TimerType[], 'initTimer'>;
/** Timers to wait for before initializing the schema plugin */
const schemaTimerCtx: SliceType<TimerType[], 'schemaTimer'>;
/** Timers to wait for before initializing the parser plugin */
const parserTimerCtx: SliceType<TimerType[], 'parserTimer'>;
/** Timers to wait for before initializing the serializer plugin */
const serializerTimerCtx: SliceType<TimerType[], 'serializerTimer'>;
/** Timers to wait for before initializing the commands plugin */
const commandsTimerCtx: SliceType<TimerType[], 'commandsTimer'>;
/** Timers to wait for before initializing the keymap plugin */
const keymapTimerCtx: SliceType<TimerType[], 'keymapTimer'>;
/** Timers to wait for before initializing the editor state plugin */
const editorStateTimerCtx: SliceType<TimerType[], 'editorStateTimer'>;
/** Timers to wait for before initializing the editor view plugin */
const editorViewTimerCtx: SliceType<TimerType[], 'editorViewTimer'>;import { createSlice } from "@milkdown/ctx";
// Create custom context slices for your plugin
const myDataCtx = createSlice([], 'myData');
const myConfigCtx = createSlice({ enabled: true }, 'myConfig');
editor.config((ctx) => {
// Initialize custom slices
ctx.inject(myDataCtx, []);
ctx.inject(myConfigCtx, { enabled: true, theme: 'dark' });
// Use custom slices
const data = ctx.get(myDataCtx);
ctx.set(myConfigCtx, { ...ctx.get(myConfigCtx), theme: 'light' });
});import { Editor, editorStateCtx } from "@milkdown/core";
editor.action((ctx) => {
// Get current state
const currentState = ctx.get(editorStateCtx);
// Create new state with modifications
const transaction = currentState.tr.insertText('Hello World');
const newState = currentState.apply(transaction);
// Update the context (usually done by plugins)
ctx.set(editorStateCtx, newState);
});import { MilkdownPlugin, createTimer } from "@milkdown/ctx";
import { SchemaReady, ParserReady } from "@milkdown/core";
const MyPluginReady = createTimer('MyPluginReady');
const myPlugin: MilkdownPlugin = (ctx) => {
ctx.record(MyPluginReady);
return async () => {
// Wait for dependencies
await ctx.wait(SchemaReady);
await ctx.wait(ParserReady);
// Plugin is ready
ctx.done(MyPluginReady);
return () => {
// Cleanup
ctx.clearTimer(MyPluginReady);
};
};
};import { Editor, editorViewCtx, schemaCtx } from "@milkdown/core";
function createContextAwareComponent() {
return editor.action((ctx) => {
const view = ctx.get(editorViewCtx);
const schema = ctx.get(schemaCtx);
// Create component that can interact with editor
return {
insertText: (text: string) => {
const transaction = view.state.tr.insertText(text);
view.dispatch(transaction);
},
getNodeTypes: () => Object.keys(schema.nodes),
focus: () => view.focus()
};
});
}
const component = createContextAwareComponent();
component.insertText('Hello from component!');/**
* Core context system types
*/
/** Default value types for editor initialization */
type DefaultValue =
| string
| { type: 'html'; dom: HTMLElement }
| { type: 'json'; value: JSONRecord };
/** Root element types */
type RootType = Node | undefined | null | string;
/** Editor view option types */
type EditorOptions = Omit<DirectEditorProps, 'state'>;
/** State creation override function */
type StateOptionsOverride = (prev: StateOptions) => StateOptions;
/** Node view tuple type */
type NodeView = [nodeId: string, view: NodeViewConstructor];
/** Mark view tuple type */
type MarkView = [markId: string, view: MarkViewConstructor];import { Editor, editorViewCtx } from "@milkdown/core";
import { ctxCallOutOfScope } from "@milkdown/exception";
function safeContextAccess() {
try {
editor.action((ctx) => {
const view = ctx.get(editorViewCtx);
// Safe to use view here
});
} catch (error) {
if (error === ctxCallOutOfScope()) {
console.error('Context accessed outside of valid scope');
}
}
}import { Editor, editorStateCtx } from "@milkdown/core";
editor.action((ctx) => {
try {
const state = ctx.get(editorStateCtx);
if (!state || !state.doc) {
throw new Error('Invalid editor state');
}
// Safe to use state
} catch (error) {
console.error('Context slice validation failed:', error);
}
});import { createSlice } from "@milkdown/ctx";
// Good: Descriptive names with 'Ctx' suffix
const userPreferencesCtx = createSlice({}, 'userPreferences');
const syntaxHighlightCtx = createSlice(null, 'syntaxHighlight');
// Avoid: Generic or unclear names
const dataCtx = createSlice({}, 'data'); // Too generic
const ctx1 = createSlice({}, 'ctx1'); // Unclear purposeimport { Editor, prosePluginsCtx } from "@milkdown/core";
editor.config((ctx) => {
// Good: Preserve existing state
ctx.update(prosePluginsCtx, (plugins) => [...plugins, newPlugin]);
// Avoid: Overwriting without preserving
ctx.set(prosePluginsCtx, [newPlugin]); // Lost existing plugins
});import { MilkdownPlugin } from "@milkdown/ctx";
const myPlugin: MilkdownPlugin = (ctx) => {
const mySlice = createSlice([], 'mySlice');
ctx.inject(mySlice, []);
return async () => {
// Plugin initialization
return () => {
// Important: Clean up custom slices
ctx.remove(mySlice);
};
};
};