CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-milkdown--transformer

Bidirectional transformation library between markdown AST and ProseMirror document structures for the Milkdown editor

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

specifications.mddocs/

Specification System

The specification system provides interfaces and types for defining custom transformation behavior between markdown and ProseMirror elements. It enables extensible parsing and serialization through match functions and runner implementations.

Capabilities

Node Schema Extensions

Extended schema specifications that include both ProseMirror schema information and transformation rules.

/**
 * Schema spec for node. It is a super set of NodeSpec.
 */
interface NodeSchema extends NodeSpec {
  /**
   * To markdown serializer spec.
   */
  readonly toMarkdown: NodeSerializerSpec;
  
  /**
   * Parse markdown serializer spec.
   */
  readonly parseMarkdown: NodeParserSpec;
  
  /**
   * The priority of the node, by default it's 50.
   */
  readonly priority?: number;
}

/**
 * Schema spec for mark. It is a super set of MarkSpec.
 */
interface MarkSchema extends MarkSpec {
  /**
   * To markdown serializer spec.
   */
  readonly toMarkdown: MarkSerializerSpec;
  
  /**
   * Parse markdown serializer spec.  
   */
  readonly parseMarkdown: MarkParserSpec;
}

Usage Examples:

import { NodeSchema, MarkSchema } from "@milkdown/transformer";

// Define a heading node schema
const headingSchema: NodeSchema = {
  attrs: { level: { default: 1 } },
  content: "inline*",
  group: "block",
  defining: true,
  
  parseMarkdown: {
    match: (node) => node.type === 'heading',
    runner: (state, node, type) => {
      state
        .openNode(type, { level: node.depth })
        .next(node.children)
        .closeNode();
    }
  },
  
  toMarkdown: {
    match: (node) => node.type.name === 'heading',
    runner: (state, node) => {
      state
        .openNode('heading', undefined, { depth: node.attrs.level })
        .next(node.content)
        .closeNode();
    }
  },
  
  priority: 100
};

// Define an emphasis mark schema
const emphasisSchema: MarkSchema = {
  parseMarkdown: {
    match: (node) => node.type === 'emphasis',
    runner: (state, node, type) => {
      state
        .openMark(type)
        .next(node.children)
        .closeMark(type);
    }
  },
  
  toMarkdown: {
    match: (mark) => mark.type.name === 'emphasis',
    runner: (state, mark, node) => {
      state.withMark(mark, 'emphasis');
    }
  }
};

Parser Specifications

Specifications for transforming markdown AST nodes into ProseMirror structures.

/**
 * The spec for node parser in schema.
 */
interface NodeParserSpec {
  /**
   * The match function to check if the node is the target node.
   * For example: match: (node) => node.type === 'paragraph'
   * @param node - Markdown AST node to evaluate
   * @returns true if this spec should handle the node
   */
  match: (node: MarkdownNode) => boolean;
  
  /**
   * The runner function to transform the node into prosemirror node.
   * Generally, you should call methods in state to add node to state.
   * @param state - Parser state for building ProseMirror structure
   * @param node - Markdown AST node to transform
   * @param proseType - Target ProseMirror node type
   */
  runner: (state: ParserState, node: MarkdownNode, proseType: NodeType) => void;
}

/**
 * The spec for mark parser in schema.
 */
interface MarkParserSpec {
  /**
   * The match function to check if the node is the target mark.
   * For example: match: (mark) => mark.type === 'emphasis'
   * @param node - Markdown AST node to evaluate
   * @returns true if this spec should handle the node
   */
  match: (node: MarkdownNode) => boolean;
  
  /**
   * The runner function to transform the node into prosemirror mark.
   * Generally, you should call methods in state to add mark to state.
   * @param state - Parser state for building ProseMirror structure
   * @param node - Markdown AST node to transform
   * @param proseType - Target ProseMirror mark type
   */
  runner: (state: ParserState, node: MarkdownNode, proseType: MarkType) => void;
}

Serializer Specifications

Specifications for transforming ProseMirror structures into markdown AST nodes.

/**
 * The spec for node serializer in schema.
 */
interface NodeSerializerSpec {
  /**
   * The match function to check if the node is the target node.
   * For example: match: (node) => node.type.name === 'paragraph'
   * @param node - ProseMirror node to evaluate
   * @returns true if this spec should handle the node
   */
  match: (node: Node) => boolean;
  
  /**
   * The runner function to transform the node into markdown text.
   * Generally, you should call methods in state to add node to state.
   * @param state - Serializer state for building markdown AST
   * @param node - ProseMirror node to transform
   */
  runner: (state: SerializerState, node: Node) => void;
}

/**
 * The spec for mark serializer in schema.
 */
interface MarkSerializerSpec {
  /**
   * The match function to check if the mark is the target mark.
   * For example: match: (mark) => mark.type.name === 'emphasis'
   * @param mark - ProseMirror mark to evaluate
   * @returns true if this spec should handle the mark
   */
  match: (mark: Mark) => boolean;
  
  /**
   * The runner function to transform the mark into markdown text.
   * Generally, you should call methods in state to add mark to state.
   * @param state - Serializer state for building markdown AST
   * @param mark - ProseMirror mark to transform
   * @param node - Associated ProseMirror node
   * @returns Optional boolean to prevent further node processing
   */
  runner: (state: SerializerState, mark: Mark, node: Node) => void | boolean;
}

Advanced Specification Examples:

// Complex list item parser with nested content
const listItemParserSpec: NodeParserSpec = {
  match: (node) => node.type === 'listItem',
  runner: (state, node, type) => {
    state.openNode(type);
    
    // Handle paragraph content
    if (node.children) {
      node.children.forEach(child => {
        if (child.type === 'paragraph') {
          state.next(child.children);
        } else {
          state.next(child);
        }
      });
    }
    
    state.closeNode();
  }
};

// Code block serializer with language support
const codeBlockSerializerSpec: NodeSerializerSpec = {
  match: (node) => node.type.name === 'code_block',
  runner: (state, node) => {
    const lang = node.attrs.language || '';
    state
      .openNode('code', node.textContent, { 
        lang: lang,
        meta: node.attrs.meta || null
      })
      .closeNode();
  }
};

Remark Integration Types

Types for integrating with the remark markdown processing ecosystem.

/**
 * The universal type of a remark plugin.
 */
interface RemarkPlugin<T = Record<string, unknown>> {
  plugin: Plugin<[T], Root>;
  options: T;
}

/**
 * The type of remark instance.
 */
type RemarkParser = ReturnType<typeof remark>;

/**
 * Raw plugin type for remark integration.
 */
type RemarkPluginRaw<T> = Plugin<[T], Root>;

Remark Plugin Integration Examples:

// Custom remark plugin for parsing
const customParserPlugin: RemarkPlugin<{ strict: boolean }> = {
  plugin: (options) => (tree, file) => {
    // Custom transformation logic
    if (options.strict) {
      // Apply strict parsing rules
    }
  },
  options: { strict: true }
};

// Using custom plugins with transformer
const customRemark = remark()
  .use(customParserPlugin.plugin, customParserPlugin.options);

const parser = ParserState.create(schema, customRemark);

docs

index.md

parser.md

serializer.md

specifications.md

utilities.md

tile.json