0
# Specification System
1
2
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.
3
4
## Capabilities
5
6
### Node Schema Extensions
7
8
Extended schema specifications that include both ProseMirror schema information and transformation rules.
9
10
```typescript { .api }
11
/**
12
* Schema spec for node. It is a super set of NodeSpec.
13
*/
14
interface NodeSchema extends NodeSpec {
15
/**
16
* To markdown serializer spec.
17
*/
18
readonly toMarkdown: NodeSerializerSpec;
19
20
/**
21
* Parse markdown serializer spec.
22
*/
23
readonly parseMarkdown: NodeParserSpec;
24
25
/**
26
* The priority of the node, by default it's 50.
27
*/
28
readonly priority?: number;
29
}
30
31
/**
32
* Schema spec for mark. It is a super set of MarkSpec.
33
*/
34
interface MarkSchema extends MarkSpec {
35
/**
36
* To markdown serializer spec.
37
*/
38
readonly toMarkdown: MarkSerializerSpec;
39
40
/**
41
* Parse markdown serializer spec.
42
*/
43
readonly parseMarkdown: MarkParserSpec;
44
}
45
```
46
47
**Usage Examples:**
48
49
```typescript
50
import { NodeSchema, MarkSchema } from "@milkdown/transformer";
51
52
// Define a heading node schema
53
const headingSchema: NodeSchema = {
54
attrs: { level: { default: 1 } },
55
content: "inline*",
56
group: "block",
57
defining: true,
58
59
parseMarkdown: {
60
match: (node) => node.type === 'heading',
61
runner: (state, node, type) => {
62
state
63
.openNode(type, { level: node.depth })
64
.next(node.children)
65
.closeNode();
66
}
67
},
68
69
toMarkdown: {
70
match: (node) => node.type.name === 'heading',
71
runner: (state, node) => {
72
state
73
.openNode('heading', undefined, { depth: node.attrs.level })
74
.next(node.content)
75
.closeNode();
76
}
77
},
78
79
priority: 100
80
};
81
82
// Define an emphasis mark schema
83
const emphasisSchema: MarkSchema = {
84
parseMarkdown: {
85
match: (node) => node.type === 'emphasis',
86
runner: (state, node, type) => {
87
state
88
.openMark(type)
89
.next(node.children)
90
.closeMark(type);
91
}
92
},
93
94
toMarkdown: {
95
match: (mark) => mark.type.name === 'emphasis',
96
runner: (state, mark, node) => {
97
state.withMark(mark, 'emphasis');
98
}
99
}
100
};
101
```
102
103
### Parser Specifications
104
105
Specifications for transforming markdown AST nodes into ProseMirror structures.
106
107
```typescript { .api }
108
/**
109
* The spec for node parser in schema.
110
*/
111
interface NodeParserSpec {
112
/**
113
* The match function to check if the node is the target node.
114
* For example: match: (node) => node.type === 'paragraph'
115
* @param node - Markdown AST node to evaluate
116
* @returns true if this spec should handle the node
117
*/
118
match: (node: MarkdownNode) => boolean;
119
120
/**
121
* The runner function to transform the node into prosemirror node.
122
* Generally, you should call methods in state to add node to state.
123
* @param state - Parser state for building ProseMirror structure
124
* @param node - Markdown AST node to transform
125
* @param proseType - Target ProseMirror node type
126
*/
127
runner: (state: ParserState, node: MarkdownNode, proseType: NodeType) => void;
128
}
129
130
/**
131
* The spec for mark parser in schema.
132
*/
133
interface MarkParserSpec {
134
/**
135
* The match function to check if the node is the target mark.
136
* For example: match: (mark) => mark.type === 'emphasis'
137
* @param node - Markdown AST node to evaluate
138
* @returns true if this spec should handle the node
139
*/
140
match: (node: MarkdownNode) => boolean;
141
142
/**
143
* The runner function to transform the node into prosemirror mark.
144
* Generally, you should call methods in state to add mark to state.
145
* @param state - Parser state for building ProseMirror structure
146
* @param node - Markdown AST node to transform
147
* @param proseType - Target ProseMirror mark type
148
*/
149
runner: (state: ParserState, node: MarkdownNode, proseType: MarkType) => void;
150
}
151
```
152
153
### Serializer Specifications
154
155
Specifications for transforming ProseMirror structures into markdown AST nodes.
156
157
```typescript { .api }
158
/**
159
* The spec for node serializer in schema.
160
*/
161
interface NodeSerializerSpec {
162
/**
163
* The match function to check if the node is the target node.
164
* For example: match: (node) => node.type.name === 'paragraph'
165
* @param node - ProseMirror node to evaluate
166
* @returns true if this spec should handle the node
167
*/
168
match: (node: Node) => boolean;
169
170
/**
171
* The runner function to transform the node into markdown text.
172
* Generally, you should call methods in state to add node to state.
173
* @param state - Serializer state for building markdown AST
174
* @param node - ProseMirror node to transform
175
*/
176
runner: (state: SerializerState, node: Node) => void;
177
}
178
179
/**
180
* The spec for mark serializer in schema.
181
*/
182
interface MarkSerializerSpec {
183
/**
184
* The match function to check if the mark is the target mark.
185
* For example: match: (mark) => mark.type.name === 'emphasis'
186
* @param mark - ProseMirror mark to evaluate
187
* @returns true if this spec should handle the mark
188
*/
189
match: (mark: Mark) => boolean;
190
191
/**
192
* The runner function to transform the mark into markdown text.
193
* Generally, you should call methods in state to add mark to state.
194
* @param state - Serializer state for building markdown AST
195
* @param mark - ProseMirror mark to transform
196
* @param node - Associated ProseMirror node
197
* @returns Optional boolean to prevent further node processing
198
*/
199
runner: (state: SerializerState, mark: Mark, node: Node) => void | boolean;
200
}
201
```
202
203
**Advanced Specification Examples:**
204
205
```typescript
206
// Complex list item parser with nested content
207
const listItemParserSpec: NodeParserSpec = {
208
match: (node) => node.type === 'listItem',
209
runner: (state, node, type) => {
210
state.openNode(type);
211
212
// Handle paragraph content
213
if (node.children) {
214
node.children.forEach(child => {
215
if (child.type === 'paragraph') {
216
state.next(child.children);
217
} else {
218
state.next(child);
219
}
220
});
221
}
222
223
state.closeNode();
224
}
225
};
226
227
// Code block serializer with language support
228
const codeBlockSerializerSpec: NodeSerializerSpec = {
229
match: (node) => node.type.name === 'code_block',
230
runner: (state, node) => {
231
const lang = node.attrs.language || '';
232
state
233
.openNode('code', node.textContent, {
234
lang: lang,
235
meta: node.attrs.meta || null
236
})
237
.closeNode();
238
}
239
};
240
```
241
242
### Remark Integration Types
243
244
Types for integrating with the remark markdown processing ecosystem.
245
246
```typescript { .api }
247
/**
248
* The universal type of a remark plugin.
249
*/
250
interface RemarkPlugin<T = Record<string, unknown>> {
251
plugin: Plugin<[T], Root>;
252
options: T;
253
}
254
255
/**
256
* The type of remark instance.
257
*/
258
type RemarkParser = ReturnType<typeof remark>;
259
260
/**
261
* Raw plugin type for remark integration.
262
*/
263
type RemarkPluginRaw<T> = Plugin<[T], Root>;
264
```
265
266
**Remark Plugin Integration Examples:**
267
268
```typescript
269
// Custom remark plugin for parsing
270
const customParserPlugin: RemarkPlugin<{ strict: boolean }> = {
271
plugin: (options) => (tree, file) => {
272
// Custom transformation logic
273
if (options.strict) {
274
// Apply strict parsing rules
275
}
276
},
277
options: { strict: true }
278
};
279
280
// Using custom plugins with transformer
281
const customRemark = remark()
282
.use(customParserPlugin.plugin, customParserPlugin.options);
283
284
const parser = ParserState.create(schema, customRemark);
285
```