CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-milkdown--core

The core module of milkdown - a plugin-driven WYSIWYG markdown Editor built on top of prosemirror and remark

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

internal-plugins.mddocs/

Internal Plugins

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.

Capabilities

Core Internal Plugins

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
  };
};

Configuration and Initialization Plugins

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();

Plugin Timing System

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
  };
};

Plugin Loading Order

Internal plugins load in a specific order to ensure proper dependencies:

  1. Config - Executes user configuration
  2. Init - Prepares core context slices
  3. Schema - Creates ProseMirror schema from nodes/marks
  4. Parser - Creates markdown-to-ProseMirror parser
  5. Serializer - Creates ProseMirror-to-markdown serializer
  6. Commands - Initializes command system
  7. Keymap - Initializes keymap system
  8. Editor State - Creates ProseMirror editor state
  9. Editor View - Creates and mounts ProseMirror editor view
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');
  };
};

Advanced Usage

Custom Plugin Dependencies

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
  };
};

Intercepting Internal Plugin Behavior

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
  };
};

Accessing Internal Plugin State

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 });
  };
};

Configuration Types

Configuration Function Type

/**
 * Type for configuration functions executed during editor initialization
 * @param ctx - The editor context to configure
 */
type Config = (ctx: Ctx) => void | Promise<void>;

Additional Types

/** 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');
  }
};

Plugin Metadata

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' }

Error Handling

Plugin Loading Failures

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
}

Timer Resolution Issues

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;
    }
  };
};

Best Practices

Plugin Dependency Management

// 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!
  };
};

Custom Timer Usage

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
    };
  };
};

Configuration Organization

// 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
};

docs

command-system.md

context-system.md

editor-management.md

index.md

internal-plugins.md

keymap-management.md

tile.json