@jvs-milkdown/transformer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +384 -0
- package/lib/index.js.map +1 -0
- package/lib/parser/index.d.ts +3 -0
- package/lib/parser/index.d.ts.map +1 -0
- package/lib/parser/stack-element.d.ts +12 -0
- package/lib/parser/stack-element.d.ts.map +1 -0
- package/lib/parser/stack-element.spec.d.ts +2 -0
- package/lib/parser/stack-element.spec.d.ts.map +1 -0
- package/lib/parser/state.d.ts +23 -0
- package/lib/parser/state.d.ts.map +1 -0
- package/lib/parser/state.spec.d.ts +2 -0
- package/lib/parser/state.spec.d.ts.map +1 -0
- package/lib/parser/types.d.ts +13 -0
- package/lib/parser/types.d.ts.map +1 -0
- package/lib/serializer/index.d.ts +3 -0
- package/lib/serializer/index.d.ts.map +1 -0
- package/lib/serializer/stack-element.d.ts +14 -0
- package/lib/serializer/stack-element.d.ts.map +1 -0
- package/lib/serializer/stack-element.spec.d.ts +2 -0
- package/lib/serializer/stack-element.spec.d.ts.map +1 -0
- package/lib/serializer/state.d.ts +22 -0
- package/lib/serializer/state.d.ts.map +1 -0
- package/lib/serializer/state.spec.d.ts +2 -0
- package/lib/serializer/state.spec.d.ts.map +1 -0
- package/lib/serializer/types.d.ts +12 -0
- package/lib/serializer/types.d.ts.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/utility/index.d.ts +3 -0
- package/lib/utility/index.d.ts.map +1 -0
- package/lib/utility/stack.d.ts +12 -0
- package/lib/utility/stack.d.ts.map +1 -0
- package/lib/utility/stack.spec.d.ts +2 -0
- package/lib/utility/stack.spec.d.ts.map +1 -0
- package/lib/utility/types.d.ts +31 -0
- package/lib/utility/types.d.ts.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +3 -0
- package/src/parser/index.ts +2 -0
- package/src/parser/stack-element.spec.ts +25 -0
- package/src/parser/stack-element.ts +25 -0
- package/src/parser/state.spec.ts +265 -0
- package/src/parser/state.ts +217 -0
- package/src/parser/types.ts +55 -0
- package/src/serializer/index.ts +2 -0
- package/src/serializer/stack-element.spec.ts +17 -0
- package/src/serializer/stack-element.ts +30 -0
- package/src/serializer/state.spec.ts +287 -0
- package/src/serializer/state.ts +355 -0
- package/src/serializer/types.ts +51 -0
- package/src/utility/index.ts +2 -0
- package/src/utility/stack.spec.ts +30 -0
- package/src/utility/stack.ts +48 -0
- package/src/utility/types.ts +63 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Fragment,
|
|
3
|
+
MarkType,
|
|
4
|
+
Node,
|
|
5
|
+
NodeType,
|
|
6
|
+
Schema,
|
|
7
|
+
} from '@jvs-milkdown/prose/model'
|
|
8
|
+
|
|
9
|
+
import { serializerMatchError } from '@jvs-milkdown/exception'
|
|
10
|
+
import { Mark } from '@jvs-milkdown/prose/model'
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
JSONRecord,
|
|
14
|
+
MarkSchema,
|
|
15
|
+
MarkdownNode,
|
|
16
|
+
NodeSchema,
|
|
17
|
+
RemarkParser,
|
|
18
|
+
Root,
|
|
19
|
+
} from '../utility'
|
|
20
|
+
import type { Serializer } from './types'
|
|
21
|
+
|
|
22
|
+
import { Stack } from '../utility'
|
|
23
|
+
import { SerializerStackElement } from './stack-element'
|
|
24
|
+
|
|
25
|
+
const isFragment = (x: Node | Fragment): x is Fragment =>
|
|
26
|
+
Object.prototype.hasOwnProperty.call(x, 'size')
|
|
27
|
+
|
|
28
|
+
/// State for serializer.
|
|
29
|
+
/// Transform prosemirror state into remark AST.
|
|
30
|
+
export class SerializerState extends Stack<
|
|
31
|
+
MarkdownNode,
|
|
32
|
+
SerializerStackElement
|
|
33
|
+
> {
|
|
34
|
+
/// @internal
|
|
35
|
+
#marks: readonly Mark[] = Mark.none
|
|
36
|
+
/// Get the schema of state.
|
|
37
|
+
readonly schema: Schema
|
|
38
|
+
|
|
39
|
+
/// Create a serializer from schema and remark instance.
|
|
40
|
+
///
|
|
41
|
+
/// ```typescript
|
|
42
|
+
/// const serializer = SerializerState.create(schema, remark)
|
|
43
|
+
/// const markdown = parser(prosemirrorDoc)
|
|
44
|
+
/// ```
|
|
45
|
+
static create = (schema: Schema, remark: RemarkParser): Serializer => {
|
|
46
|
+
const state = new this(schema)
|
|
47
|
+
return (content: Node) => {
|
|
48
|
+
state.run(content)
|
|
49
|
+
return state.toString(remark)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// @internal
|
|
54
|
+
constructor(schema: Schema) {
|
|
55
|
+
super()
|
|
56
|
+
this.schema = schema
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// @internal
|
|
60
|
+
#matchTarget = (node: Node | Mark): NodeType | MarkType => {
|
|
61
|
+
const result = Object.values({
|
|
62
|
+
...this.schema.nodes,
|
|
63
|
+
...this.schema.marks,
|
|
64
|
+
}).find((x): x is NodeType | MarkType => {
|
|
65
|
+
const spec = x.spec as NodeSchema | MarkSchema
|
|
66
|
+
return spec.toMarkdown.match(node as Node & Mark)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (!result) throw serializerMatchError(node.type)
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @internal
|
|
75
|
+
#runProseNode = (node: Node) => {
|
|
76
|
+
const type = this.#matchTarget(node)
|
|
77
|
+
const spec = type.spec as NodeSchema
|
|
78
|
+
return spec.toMarkdown.runner(this, node)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// @internal
|
|
82
|
+
#runProseMark = (mark: Mark, node: Node) => {
|
|
83
|
+
const type = this.#matchTarget(mark)
|
|
84
|
+
const spec = type.spec as MarkSchema
|
|
85
|
+
return spec.toMarkdown.runner(this, mark, node)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// @internal
|
|
89
|
+
#runNode = (node: Node) => {
|
|
90
|
+
const { marks } = node
|
|
91
|
+
const getPriority = (x: Mark) => x.type.spec.priority ?? 50
|
|
92
|
+
const tmp = [...marks].sort((a, b) => getPriority(a) - getPriority(b))
|
|
93
|
+
const unPreventNext = tmp.every((mark) => !this.#runProseMark(mark, node))
|
|
94
|
+
if (unPreventNext) this.#runProseNode(node)
|
|
95
|
+
|
|
96
|
+
marks.forEach((mark) => this.#closeMark(mark))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// @internal
|
|
100
|
+
#searchType = (child: MarkdownNode, type: string): MarkdownNode => {
|
|
101
|
+
if (child.type === type) return child
|
|
102
|
+
|
|
103
|
+
if (child.children?.length !== 1) return child
|
|
104
|
+
|
|
105
|
+
const searchNode = (node: MarkdownNode): MarkdownNode | null => {
|
|
106
|
+
if (node.type === type) return node.value != null ? null : node
|
|
107
|
+
|
|
108
|
+
if (node.children?.length !== 1) return null
|
|
109
|
+
|
|
110
|
+
const [firstChild] = node.children
|
|
111
|
+
if (!firstChild) return null
|
|
112
|
+
|
|
113
|
+
return searchNode(firstChild)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const target = searchNode(child)
|
|
117
|
+
|
|
118
|
+
if (!target) return child
|
|
119
|
+
|
|
120
|
+
const tmp = target.children ? [...target.children] : undefined
|
|
121
|
+
const node = { ...child, children: tmp }
|
|
122
|
+
node.children = tmp
|
|
123
|
+
target.children = [node]
|
|
124
|
+
|
|
125
|
+
return target
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// @internal
|
|
129
|
+
#maybeMergeChildren = (node: MarkdownNode): MarkdownNode => {
|
|
130
|
+
const { children } = node
|
|
131
|
+
if (!children) return node
|
|
132
|
+
|
|
133
|
+
node.children = children.reduce((nextChildren, child, index) => {
|
|
134
|
+
if (index === 0) return [child]
|
|
135
|
+
|
|
136
|
+
const last = nextChildren.at(-1)
|
|
137
|
+
if (last && last.isMark && child.isMark) {
|
|
138
|
+
child = this.#searchType(child, last.type)
|
|
139
|
+
const { children: currChildren, ...currRest } = child
|
|
140
|
+
const { children: prevChildren, ...prevRest } = last
|
|
141
|
+
if (
|
|
142
|
+
child.type === last.type &&
|
|
143
|
+
currChildren &&
|
|
144
|
+
prevChildren &&
|
|
145
|
+
JSON.stringify(currRest) === JSON.stringify(prevRest)
|
|
146
|
+
) {
|
|
147
|
+
const next = {
|
|
148
|
+
...prevRest,
|
|
149
|
+
children: [...prevChildren, ...currChildren],
|
|
150
|
+
}
|
|
151
|
+
return nextChildren
|
|
152
|
+
.slice(0, -1)
|
|
153
|
+
.concat(this.#maybeMergeChildren(next))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return nextChildren.concat(child)
|
|
157
|
+
}, [] as MarkdownNode[])
|
|
158
|
+
|
|
159
|
+
return node
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// @internal
|
|
163
|
+
#createMarkdownNode = (element: SerializerStackElement) => {
|
|
164
|
+
const node: MarkdownNode = {
|
|
165
|
+
...element.props,
|
|
166
|
+
type: element.type,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (element.children) node.children = element.children
|
|
170
|
+
|
|
171
|
+
if (element.value) node.value = element.value
|
|
172
|
+
|
|
173
|
+
return node
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Open a new node, the next operations will
|
|
177
|
+
/// add nodes into that new node until `closeNode` is called.
|
|
178
|
+
openNode = (type: string, value?: string, props?: JSONRecord) => {
|
|
179
|
+
this.open(SerializerStackElement.create(type, undefined, value, props))
|
|
180
|
+
return this
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#moveSpaces = (
|
|
184
|
+
element: SerializerStackElement,
|
|
185
|
+
onPush: () => MarkdownNode
|
|
186
|
+
) => {
|
|
187
|
+
let startSpaces = ''
|
|
188
|
+
let endSpaces = ''
|
|
189
|
+
const children = element.children
|
|
190
|
+
let first = -1
|
|
191
|
+
let last = -1
|
|
192
|
+
const findIndex = (node: MarkdownNode[]) => {
|
|
193
|
+
if (!node) return
|
|
194
|
+
node.forEach((child, index) => {
|
|
195
|
+
if (child.type === 'text' && child.value) {
|
|
196
|
+
if (first < 0) first = index
|
|
197
|
+
|
|
198
|
+
last = index
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (children) {
|
|
204
|
+
findIndex(children)
|
|
205
|
+
const lastChild = children?.[last] as
|
|
206
|
+
| (MarkdownNode & { value: string })
|
|
207
|
+
| undefined
|
|
208
|
+
const firstChild = children?.[first] as
|
|
209
|
+
| (MarkdownNode & { value: string })
|
|
210
|
+
| undefined
|
|
211
|
+
if (lastChild && lastChild.value.endsWith(' ')) {
|
|
212
|
+
const text = lastChild.value
|
|
213
|
+
const trimmed = text.trimEnd()
|
|
214
|
+
endSpaces = text.slice(trimmed.length)
|
|
215
|
+
lastChild.value = trimmed
|
|
216
|
+
}
|
|
217
|
+
if (firstChild && firstChild.value.startsWith(' ')) {
|
|
218
|
+
const text = firstChild.value
|
|
219
|
+
const trimmed = text.trimStart()
|
|
220
|
+
startSpaces = text.slice(0, text.length - trimmed.length)
|
|
221
|
+
firstChild.value = trimmed
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (startSpaces.length) this.#addNodeAndPush('text', undefined, startSpaces)
|
|
226
|
+
|
|
227
|
+
const result = onPush()
|
|
228
|
+
|
|
229
|
+
if (endSpaces.length) this.#addNodeAndPush('text', undefined, endSpaces)
|
|
230
|
+
|
|
231
|
+
return result
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// @internal
|
|
235
|
+
#closeNodeAndPush = (trim: boolean = false): MarkdownNode => {
|
|
236
|
+
const element = this.close()
|
|
237
|
+
|
|
238
|
+
const onPush = () =>
|
|
239
|
+
this.#addNodeAndPush(
|
|
240
|
+
element.type,
|
|
241
|
+
element.children,
|
|
242
|
+
element.value,
|
|
243
|
+
element.props
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if (trim) return this.#moveSpaces(element, onPush)
|
|
247
|
+
|
|
248
|
+
return onPush()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Close the current node and push it into the parent node.
|
|
252
|
+
closeNode = () => {
|
|
253
|
+
this.#closeNodeAndPush()
|
|
254
|
+
return this
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// @internal
|
|
258
|
+
#addNodeAndPush = (
|
|
259
|
+
type: string,
|
|
260
|
+
children?: MarkdownNode[],
|
|
261
|
+
value?: string,
|
|
262
|
+
props?: JSONRecord
|
|
263
|
+
): MarkdownNode => {
|
|
264
|
+
const element = SerializerStackElement.create(type, children, value, props)
|
|
265
|
+
const node: MarkdownNode = this.#maybeMergeChildren(
|
|
266
|
+
this.#createMarkdownNode(element)
|
|
267
|
+
)
|
|
268
|
+
this.push(node)
|
|
269
|
+
return node
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Add a node into current node.
|
|
273
|
+
addNode = (
|
|
274
|
+
type: string,
|
|
275
|
+
children?: MarkdownNode[],
|
|
276
|
+
value?: string,
|
|
277
|
+
props?: JSONRecord
|
|
278
|
+
) => {
|
|
279
|
+
this.#addNodeAndPush(type, children, value, props)
|
|
280
|
+
return this
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// @internal
|
|
284
|
+
#openMark = (
|
|
285
|
+
mark: Mark,
|
|
286
|
+
type: string,
|
|
287
|
+
value?: string,
|
|
288
|
+
props?: JSONRecord
|
|
289
|
+
) => {
|
|
290
|
+
const isIn = mark.isInSet(this.#marks)
|
|
291
|
+
|
|
292
|
+
if (isIn) return this
|
|
293
|
+
|
|
294
|
+
this.#marks = mark.addToSet(this.#marks)
|
|
295
|
+
return this.openNode(type, value, { ...props, isMark: true })
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// @internal
|
|
299
|
+
#closeMark = (mark: Mark): void => {
|
|
300
|
+
const isIn = mark.isInSet(this.#marks)
|
|
301
|
+
|
|
302
|
+
if (!isIn) return
|
|
303
|
+
|
|
304
|
+
this.#marks = mark.type.removeFromSet(this.#marks)
|
|
305
|
+
this.#closeNodeAndPush(true)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Open a new mark, the next nodes added will have that mark.
|
|
309
|
+
/// The mark will be closed automatically.
|
|
310
|
+
withMark = (mark: Mark, type: string, value?: string, props?: JSONRecord) => {
|
|
311
|
+
this.#openMark(mark, type, value, props)
|
|
312
|
+
return this
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Close a opened mark.
|
|
316
|
+
/// In most cases you don't need this because
|
|
317
|
+
/// marks will be closed automatically.
|
|
318
|
+
closeMark = (mark: Mark) => {
|
|
319
|
+
this.#closeMark(mark)
|
|
320
|
+
return this
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// @internal
|
|
324
|
+
build = (): MarkdownNode => {
|
|
325
|
+
let doc: MarkdownNode | null = null
|
|
326
|
+
do doc = this.#closeNodeAndPush()
|
|
327
|
+
while (this.size())
|
|
328
|
+
|
|
329
|
+
return doc
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// Give the node or node list back to the state and
|
|
333
|
+
/// the state will find a proper runner (by `match` method in serializer spec) to handle it.
|
|
334
|
+
next = (nodes: Node | Fragment) => {
|
|
335
|
+
if (isFragment(nodes)) {
|
|
336
|
+
nodes.forEach((node) => {
|
|
337
|
+
this.#runNode(node)
|
|
338
|
+
})
|
|
339
|
+
return this
|
|
340
|
+
}
|
|
341
|
+
this.#runNode(nodes)
|
|
342
|
+
return this
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// Use a remark parser to serialize current AST stored.
|
|
346
|
+
override toString = (remark: RemarkParser): string =>
|
|
347
|
+
remark.stringify(this.build() as Root)
|
|
348
|
+
|
|
349
|
+
/// Transform a prosemirror node tree into remark AST.
|
|
350
|
+
run = (tree: Node) => {
|
|
351
|
+
this.next(tree)
|
|
352
|
+
|
|
353
|
+
return this
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Mark, Node } from '@jvs-milkdown/prose/model'
|
|
2
|
+
|
|
3
|
+
import type { SerializerState } from './state'
|
|
4
|
+
|
|
5
|
+
/// The serializer type which is used to transform prosemirror node into markdown text.
|
|
6
|
+
export type Serializer = (content: Node) => string
|
|
7
|
+
|
|
8
|
+
/// The spec for node serializer in schema.
|
|
9
|
+
export interface NodeSerializerSpec {
|
|
10
|
+
/// The match function to check if the node is the target node.
|
|
11
|
+
/// For example:
|
|
12
|
+
///
|
|
13
|
+
/// ```typescript
|
|
14
|
+
/// match: (node) => node.type.name === 'paragraph'
|
|
15
|
+
/// ```
|
|
16
|
+
match: (node: Node) => boolean
|
|
17
|
+
/// The runner function to transform the node into markdown text.
|
|
18
|
+
/// Generally, you should call methods in `state` to add node to state.
|
|
19
|
+
/// For example:
|
|
20
|
+
///
|
|
21
|
+
/// ```typescript
|
|
22
|
+
/// runner: (state, node) => {
|
|
23
|
+
/// state
|
|
24
|
+
/// .openNode(node.type.name)
|
|
25
|
+
/// .next(node.content)
|
|
26
|
+
/// .closeNode();
|
|
27
|
+
/// }
|
|
28
|
+
/// ```
|
|
29
|
+
runner: (state: SerializerState, node: Node) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// The spec for mark serializer in schema.
|
|
33
|
+
export interface MarkSerializerSpec {
|
|
34
|
+
/// The match function to check if the node is the target mark.
|
|
35
|
+
/// For example:
|
|
36
|
+
///
|
|
37
|
+
/// ```typescript
|
|
38
|
+
/// match: (mark) => mark.type.name === 'emphasis'
|
|
39
|
+
/// ```
|
|
40
|
+
match: (mark: Mark) => boolean
|
|
41
|
+
/// The runner function to transform the node into markdown text.
|
|
42
|
+
/// Generally, you should call methods in `state` to add mark to state.
|
|
43
|
+
/// For example:
|
|
44
|
+
///
|
|
45
|
+
/// ```typescript
|
|
46
|
+
/// runner: (state, mark, node) => {
|
|
47
|
+
/// state.withMark(mark, 'emphasis');
|
|
48
|
+
/// }
|
|
49
|
+
/// ```
|
|
50
|
+
runner: (state: SerializerState, mark: Mark, node: Node) => void | boolean
|
|
51
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Stack } from './stack'
|
|
4
|
+
|
|
5
|
+
it('stack', () => {
|
|
6
|
+
const stack = new Stack<number, Array<number>>()
|
|
7
|
+
|
|
8
|
+
expect(stack.size()).toBe(0)
|
|
9
|
+
|
|
10
|
+
stack.open([])
|
|
11
|
+
stack.push(1)
|
|
12
|
+
|
|
13
|
+
expect(stack.size()).toBe(1)
|
|
14
|
+
expect(stack.top()).toEqual([1])
|
|
15
|
+
|
|
16
|
+
stack.open([])
|
|
17
|
+
stack.push(2)
|
|
18
|
+
|
|
19
|
+
expect(stack.size()).toBe(2)
|
|
20
|
+
expect(stack.top()).toEqual([2])
|
|
21
|
+
|
|
22
|
+
stack.push(3)
|
|
23
|
+
const a = stack.close()
|
|
24
|
+
expect(a).toEqual([2, 3])
|
|
25
|
+
expect(stack.size()).toBe(1)
|
|
26
|
+
|
|
27
|
+
const b = stack.close()
|
|
28
|
+
expect(b).toEqual([1])
|
|
29
|
+
expect(stack.size()).toBe(0)
|
|
30
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { stackOverFlow } from '@jvs-milkdown/exception'
|
|
2
|
+
|
|
3
|
+
/// The element of the stack, which holds an array of nodes.
|
|
4
|
+
export abstract class StackElement<Node> {
|
|
5
|
+
/// A method that can `push` a node into the element.
|
|
6
|
+
abstract push(node: Node, ...rest: Node[]): void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/// The stack that is used to store the elements.
|
|
10
|
+
///
|
|
11
|
+
/// > Generally, you don't need to use this class directly.
|
|
12
|
+
///
|
|
13
|
+
/// When using the stack, users can call `stack.open` to push a new element into the stack.
|
|
14
|
+
/// And use `stack.push` to push a node into the top element.
|
|
15
|
+
/// Then use `stack.close` to close the top element and pop it.
|
|
16
|
+
///
|
|
17
|
+
/// For example: `stack.open(A).push(B).push(C).close()` will generate a structure like `A(B, C)`.
|
|
18
|
+
export class Stack<Node, Element extends StackElement<Node>> {
|
|
19
|
+
protected elements: Element[] = []
|
|
20
|
+
|
|
21
|
+
/// Get the size of the stack.
|
|
22
|
+
size = (): number => {
|
|
23
|
+
return this.elements.length
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Get the top element of the stack.
|
|
27
|
+
top = (): Element | undefined => {
|
|
28
|
+
return this.elements.at(-1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Push a node into the top element.
|
|
32
|
+
push = (node: Node): void => {
|
|
33
|
+
this.top()?.push(node)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Push a new element.
|
|
37
|
+
open = (node: Element): void => {
|
|
38
|
+
this.elements.push(node)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Close the top element and pop it.
|
|
42
|
+
close = (): Element => {
|
|
43
|
+
const el = this.elements.pop()
|
|
44
|
+
if (!el) throw stackOverFlow()
|
|
45
|
+
|
|
46
|
+
return el
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { MarkSpec, NodeSpec } from '@jvs-milkdown/prose/model'
|
|
2
|
+
import type { remark } from 'remark'
|
|
3
|
+
import type { Plugin, Transformer } from 'unified'
|
|
4
|
+
|
|
5
|
+
import type { MarkParserSpec, NodeParserSpec } from '../parser/types'
|
|
6
|
+
import type {
|
|
7
|
+
MarkSerializerSpec,
|
|
8
|
+
NodeSerializerSpec,
|
|
9
|
+
} from '../serializer/types'
|
|
10
|
+
|
|
11
|
+
/// @internal
|
|
12
|
+
export type Node = Parameters<Transformer>[0]
|
|
13
|
+
|
|
14
|
+
/// @internal
|
|
15
|
+
export type Root = Parameters<(typeof remark)['stringify']>[0]
|
|
16
|
+
|
|
17
|
+
/// @internal
|
|
18
|
+
export type JSONValue =
|
|
19
|
+
| string
|
|
20
|
+
| number
|
|
21
|
+
| boolean
|
|
22
|
+
| null
|
|
23
|
+
| JSONValue[]
|
|
24
|
+
| { [key: string]: JSONValue }
|
|
25
|
+
|
|
26
|
+
/// @internal
|
|
27
|
+
export type JSONRecord = Record<string, JSONValue>
|
|
28
|
+
|
|
29
|
+
/// @internal
|
|
30
|
+
export type RemarkPluginRaw<T> = Plugin<[T], Root>
|
|
31
|
+
|
|
32
|
+
/// The universal type of a [remark plugin](https://github.com/remarkjs/remark/blob/main/doc/plugins.md).
|
|
33
|
+
export interface RemarkPlugin<T = Record<string, unknown>> {
|
|
34
|
+
plugin: Plugin<[T], Root>
|
|
35
|
+
options: T
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The type of [remark instance](https://github.com/remarkjs/remark/tree/main/packages/remark#remark-1).
|
|
39
|
+
export type RemarkParser = ReturnType<typeof remark>
|
|
40
|
+
|
|
41
|
+
/// The universal type of a node in [mdast](https://github.com/syntax-tree/mdast).
|
|
42
|
+
export type MarkdownNode = Node & {
|
|
43
|
+
children?: MarkdownNode[]
|
|
44
|
+
[x: string]: unknown
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Schema spec for node. It is a super set of [NodeSpec](https://prosemirror.net/docs/ref/#model.NodeSpec).
|
|
48
|
+
export interface NodeSchema extends NodeSpec {
|
|
49
|
+
/// To markdown serializer spec.
|
|
50
|
+
readonly toMarkdown: NodeSerializerSpec
|
|
51
|
+
/// Parse markdown serializer spec.
|
|
52
|
+
readonly parseMarkdown: NodeParserSpec
|
|
53
|
+
/// The priority of the node, by default it's 50.
|
|
54
|
+
readonly priority?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Schema spec for mark. It is a super set of [MarkSpec](https://prosemirror.net/docs/ref/#model.MarkSpec).
|
|
58
|
+
export interface MarkSchema extends MarkSpec {
|
|
59
|
+
/// To markdown serializer spec.
|
|
60
|
+
readonly toMarkdown: MarkSerializerSpec
|
|
61
|
+
/// Parse markdown serializer spec.
|
|
62
|
+
readonly parseMarkdown: MarkParserSpec
|
|
63
|
+
}
|