Bidirectional transformation library between markdown AST and ProseMirror document structures for the Milkdown editor
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
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');
}
}
};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;
}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();
}
};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);