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 serializer system provides state machine functionality for converting ProseMirror document structures into markdown AST. It offers a fluent API for building complex document serialization with extensible node and mark processing.
The main state machine class for serializing ProseMirror nodes to markdown AST.
/**
* State for serializer. Transform prosemirror state into remark AST.
*/
class SerializerState extends Stack<MarkdownNode, SerializerStackElement> {
readonly schema: Schema;
/**
* Create a serializer from schema and remark instance.
* @param schema - ProseMirror schema defining available nodes and marks
* @param remark - Remark parser instance for processing markdown
* @returns Serializer function that converts ProseMirror nodes to markdown text
*/
static create(schema: Schema, remark: RemarkParser): Serializer;
/**
* Open a new node, the next operations will add nodes into that new node until closeNode is called.
* @param type - Markdown AST node type to open
* @param value - Optional text value for the node
* @param props - Optional additional properties for the node
* @returns SerializerState instance for chaining
*/
openNode(type: string, value?: string, props?: JSONRecord): SerializerState;
/**
* Close the current node and push it into the parent node.
* @returns SerializerState instance for chaining
*/
closeNode(): SerializerState;
/**
* Add a node into current node.
* @param type - Markdown AST node type to add
* @param children - Optional child nodes
* @param value - Optional text value for the node
* @param props - Optional additional properties for the node
* @returns SerializerState instance for chaining
*/
addNode(type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord): SerializerState;
/**
* Open a new mark, the next nodes added will have that mark.
* The mark will be closed automatically.
* @param mark - ProseMirror mark instance
* @param type - Markdown AST node type for the mark
* @param value - Optional text value for the mark
* @param props - Optional additional properties for the mark
* @returns SerializerState instance for chaining
*/
withMark(mark: Mark, type: string, value?: string, props?: JSONRecord): SerializerState;
/**
* Close a opened mark.
* In most cases you don't need this because marks will be closed automatically.
* @param mark - ProseMirror mark instance to close
* @returns SerializerState instance for chaining
*/
closeMark(mark: Mark): SerializerState;
/**
* Give the node or node list back to the state and the state will find a proper runner to handle it.
* @param nodes - ProseMirror nodes to process (single node or fragment)
* @returns SerializerState instance for chaining
*/
next(nodes: Node | Fragment): SerializerState;
/**
* Use a remark parser to serialize current AST stored.
* @param remark - Remark parser instance
* @returns Serialized markdown text
*/
toString(remark: RemarkParser): string;
/**
* Transform a prosemirror node tree into remark AST.
* @param tree - ProseMirror node tree to transform
* @returns SerializerState instance for chaining
*/
run(tree: Node): SerializerState;
}Usage Examples:
import { SerializerState } from "@milkdown/transformer";
import { remark } from "remark";
import { schema } from "@milkdown/prose/model";
// Create a serializer
const serializer = SerializerState.create(schema, remark());
// Serialize ProseMirror to markdown
const markdown = serializer(prosemirrorDoc);
// Manual state manipulation
const state = new SerializerState(schema);
state.openNode('root')
.openNode('heading', undefined, { depth: 1 })
.addNode('text', undefined, 'Hello World')
.closeNode()
.openNode('paragraph')
.openNode('strong')
.addNode('text', undefined, 'bold')
.closeNode()
.addNode('text', undefined, ' text')
.closeNode()
.closeNode();
const markdown = state.toString(remark());Stack element implementation for serializer state management.
/**
* Stack element for serializer state, holds markdown AST nodes during transformation.
*/
class SerializerStackElement extends StackElement<MarkdownNode> {
type: string;
children?: MarkdownNode[];
value?: string;
props: JSONRecord;
/**
* Create a new serializer stack element.
* @param type - Markdown AST node type
* @param children - Optional child nodes
* @param value - Optional text value
* @param props - Optional additional properties
* @returns New SerializerStackElement instance
*/
static create(type: string, children?: MarkdownNode[], value?: string, props?: JSONRecord): SerializerStackElement;
/**
* Add nodes to the element's children.
* @param node - Node to add
* @param rest - Additional nodes to add
*/
push(node: MarkdownNode, ...rest: MarkdownNode[]): void;
/**
* Remove and return the last node from children.
* @returns Last node or undefined if empty
*/
pop(): MarkdownNode | undefined;
}Types for defining custom serialization behavior.
/**
* The serializer type which is used to transform prosemirror node into markdown text.
*/
type Serializer = (content: Node) => string;
/**
* The spec for node serializer in schema.
*/
interface NodeSerializerSpec {
/**
* The match function to check if the node is the target node.
* @param node - ProseMirror node to check
* @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 adding nodes
* @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.
* @param mark - ProseMirror mark to check
* @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 adding marks
* @param mark - ProseMirror mark to transform
* @param node - Associated ProseMirror node
* @returns Optional boolean to prevent further processing
*/
runner: (state: SerializerState, mark: Mark, node: Node) => void | boolean;
}Specification Examples:
// Example node serializer spec for paragraphs
const paragraphSerializerSpec: NodeSerializerSpec = {
match: (node) => node.type.name === 'paragraph',
runner: (state, node) => {
state
.openNode('paragraph')
.next(node.content)
.closeNode();
}
};
// Example mark serializer spec for emphasis
const emphasisSerializerSpec: MarkSerializerSpec = {
match: (mark) => mark.type.name === 'emphasis',
runner: (state, mark, node) => {
state.withMark(mark, 'emphasis');
}
};
// Example node serializer spec for headings
const headingSerializerSpec: NodeSerializerSpec = {
match: (node) => node.type.name === 'heading',
runner: (state, node) => {
state
.openNode('heading', undefined, { depth: node.attrs.level })
.next(node.content)
.closeNode();
}
};