@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,217 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Attrs,
|
|
3
|
+
MarkType,
|
|
4
|
+
Node,
|
|
5
|
+
NodeType,
|
|
6
|
+
Schema,
|
|
7
|
+
} from '@jvs-milkdown/prose/model'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createNodeInParserFail,
|
|
11
|
+
parserMatchError,
|
|
12
|
+
stackOverFlow,
|
|
13
|
+
} from '@jvs-milkdown/exception'
|
|
14
|
+
import { Mark } from '@jvs-milkdown/prose/model'
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
MarkSchema,
|
|
18
|
+
MarkdownNode,
|
|
19
|
+
NodeSchema,
|
|
20
|
+
RemarkParser,
|
|
21
|
+
} from '../utility'
|
|
22
|
+
import type { Parser } from './types'
|
|
23
|
+
|
|
24
|
+
import { Stack } from '../utility'
|
|
25
|
+
import { ParserStackElement } from './stack-element'
|
|
26
|
+
|
|
27
|
+
/// A state machine for parser. Transform remark AST into prosemirror state.
|
|
28
|
+
export class ParserState extends Stack<Node, ParserStackElement> {
|
|
29
|
+
/// The schema in current editor.
|
|
30
|
+
readonly schema: Schema
|
|
31
|
+
|
|
32
|
+
/// @internal
|
|
33
|
+
#marks: readonly Mark[] = Mark.none
|
|
34
|
+
|
|
35
|
+
/// Create a parser from schema and remark instance.
|
|
36
|
+
///
|
|
37
|
+
/// ```typescript
|
|
38
|
+
/// const parser = ParserState.create(schema, remark)
|
|
39
|
+
/// const prosemirrorNode = parser(SomeMarkdownText)
|
|
40
|
+
/// ```
|
|
41
|
+
static create = (schema: Schema, remark: RemarkParser): Parser => {
|
|
42
|
+
const state = new this(schema)
|
|
43
|
+
return (text) => {
|
|
44
|
+
state.run(remark, text)
|
|
45
|
+
return state.toDoc()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// @internal
|
|
50
|
+
constructor(schema: Schema) {
|
|
51
|
+
super()
|
|
52
|
+
this.schema = schema
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// @internal
|
|
56
|
+
#hasText = (node: Node): node is Node & { text: string } => node.isText
|
|
57
|
+
|
|
58
|
+
/// @internal
|
|
59
|
+
#maybeMerge = (a: Node, b: Node): Node | undefined => {
|
|
60
|
+
if (this.#hasText(a) && this.#hasText(b) && Mark.sameSet(a.marks, b.marks))
|
|
61
|
+
return this.schema.text(a.text + b.text, a.marks)
|
|
62
|
+
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// @internal
|
|
67
|
+
#matchTarget = (node: MarkdownNode): NodeType | MarkType => {
|
|
68
|
+
const result = Object.values({
|
|
69
|
+
...this.schema.nodes,
|
|
70
|
+
...this.schema.marks,
|
|
71
|
+
}).find((x): x is NodeType | MarkType => {
|
|
72
|
+
const spec = x.spec as NodeSchema | MarkSchema
|
|
73
|
+
return spec.parseMarkdown.match(node)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!result) throw parserMatchError(node)
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// @internal
|
|
82
|
+
#runNode = (node: MarkdownNode) => {
|
|
83
|
+
const type = this.#matchTarget(node)
|
|
84
|
+
const spec = type.spec as NodeSchema | MarkSchema
|
|
85
|
+
|
|
86
|
+
spec.parseMarkdown.runner(this, node, type as NodeType & MarkType)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Inject root node for prosemirror state.
|
|
90
|
+
injectRoot = (node: MarkdownNode, nodeType: NodeType, attrs?: Attrs) => {
|
|
91
|
+
this.openNode(nodeType, attrs)
|
|
92
|
+
this.next(node.children)
|
|
93
|
+
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Open a new node, the next operations will
|
|
98
|
+
/// add nodes into that new node until `closeNode` is called.
|
|
99
|
+
openNode = (nodeType: NodeType, attrs?: Attrs) => {
|
|
100
|
+
this.open(ParserStackElement.create(nodeType, [], attrs))
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// @internal
|
|
105
|
+
#closeNodeAndPush = (): Node => {
|
|
106
|
+
this.#marks = Mark.none
|
|
107
|
+
const element = this.close()
|
|
108
|
+
|
|
109
|
+
return this.#addNodeAndPush(element.type, element.attrs, element.content)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Close the current node and push it into the parent node.
|
|
113
|
+
closeNode = () => {
|
|
114
|
+
try {
|
|
115
|
+
this.#closeNodeAndPush()
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error(e)
|
|
118
|
+
}
|
|
119
|
+
return this
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// @internal
|
|
123
|
+
#addNodeAndPush = (
|
|
124
|
+
nodeType: NodeType,
|
|
125
|
+
attrs?: Attrs,
|
|
126
|
+
content?: Node[]
|
|
127
|
+
): Node => {
|
|
128
|
+
const node = nodeType.createAndFill(attrs, content, this.#marks)
|
|
129
|
+
if (!node) throw createNodeInParserFail(nodeType, attrs, content)
|
|
130
|
+
|
|
131
|
+
this.push(node)
|
|
132
|
+
|
|
133
|
+
return node
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Add a node into current node.
|
|
137
|
+
addNode = (nodeType: NodeType, attrs?: Attrs, content?: Node[]) => {
|
|
138
|
+
try {
|
|
139
|
+
this.#addNodeAndPush(nodeType, attrs, content)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(e)
|
|
142
|
+
}
|
|
143
|
+
return this
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Open a new mark, the next nodes added will have that mark.
|
|
147
|
+
openMark = (markType: MarkType, attrs?: Attrs) => {
|
|
148
|
+
const mark = markType.create(attrs)
|
|
149
|
+
|
|
150
|
+
this.#marks = mark.addToSet(this.#marks)
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Close a opened mark.
|
|
155
|
+
closeMark = (markType: MarkType) => {
|
|
156
|
+
this.#marks = markType.removeFromSet(this.#marks)
|
|
157
|
+
return this
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Add a text node into current node.
|
|
161
|
+
addText = (text: string) => {
|
|
162
|
+
try {
|
|
163
|
+
const topElement = this.top()
|
|
164
|
+
if (!topElement) throw stackOverFlow()
|
|
165
|
+
|
|
166
|
+
const prevNode = topElement.pop()
|
|
167
|
+
const currNode = this.schema.text(text, this.#marks)
|
|
168
|
+
|
|
169
|
+
if (!prevNode) {
|
|
170
|
+
topElement.push(currNode)
|
|
171
|
+
return this
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const merged = this.#maybeMerge(prevNode, currNode)
|
|
175
|
+
if (merged) {
|
|
176
|
+
topElement.push(merged)
|
|
177
|
+
return this
|
|
178
|
+
}
|
|
179
|
+
topElement.push(prevNode, currNode)
|
|
180
|
+
return this
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error(e)
|
|
183
|
+
return this
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// @internal
|
|
188
|
+
build = (): Node => {
|
|
189
|
+
let doc: Node | undefined
|
|
190
|
+
|
|
191
|
+
do doc = this.#closeNodeAndPush()
|
|
192
|
+
while (this.size())
|
|
193
|
+
|
|
194
|
+
return doc
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Give the node or node list back to the state and
|
|
198
|
+
/// the state will find a proper runner (by `match` method in parser spec) to handle it.
|
|
199
|
+
next = (nodes: MarkdownNode | MarkdownNode[] = []) => {
|
|
200
|
+
;[nodes].flat().forEach((node) => this.#runNode(node))
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Build the current state into a [prosemirror document](https://prosemirror.net/docs/ref/#model.Document_Structure).
|
|
205
|
+
toDoc = () => this.build()
|
|
206
|
+
|
|
207
|
+
/// Transform a markdown string into prosemirror state.
|
|
208
|
+
run = (remark: RemarkParser, markdown: string) => {
|
|
209
|
+
const tree = remark.runSync(
|
|
210
|
+
remark.parse(markdown),
|
|
211
|
+
markdown
|
|
212
|
+
) as MarkdownNode
|
|
213
|
+
this.next(tree)
|
|
214
|
+
|
|
215
|
+
return this
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MarkType, Node, NodeType } from '@jvs-milkdown/prose/model'
|
|
2
|
+
|
|
3
|
+
import type { MarkdownNode } from '../utility/types'
|
|
4
|
+
import type { ParserState } from './state'
|
|
5
|
+
|
|
6
|
+
/// The parser type which is used to transform markdown text into prosemirror node.
|
|
7
|
+
export type Parser = (text: string) => Node
|
|
8
|
+
|
|
9
|
+
/// The spec for node parser in schema.
|
|
10
|
+
export interface NodeParserSpec {
|
|
11
|
+
/// The match function to check if the node is the target node.
|
|
12
|
+
/// For example:
|
|
13
|
+
///
|
|
14
|
+
/// ```typescript
|
|
15
|
+
/// match: (node) => node.type === 'paragraph'
|
|
16
|
+
/// ```
|
|
17
|
+
match: (node: MarkdownNode) => boolean
|
|
18
|
+
/// The runner function to transform the node into prosemirror node.
|
|
19
|
+
/// Generally, you should call methods in `state` to add node to state.
|
|
20
|
+
/// For example:
|
|
21
|
+
///
|
|
22
|
+
/// ```typescript
|
|
23
|
+
/// runner: (state, node, type) => {
|
|
24
|
+
/// state
|
|
25
|
+
/// .openNode(type)
|
|
26
|
+
/// .next(node.children)
|
|
27
|
+
/// .closeNode();
|
|
28
|
+
/// }
|
|
29
|
+
/// ```
|
|
30
|
+
runner: (state: ParserState, node: MarkdownNode, proseType: NodeType) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// The spec for mark parser in schema.
|
|
34
|
+
export interface MarkParserSpec {
|
|
35
|
+
/// The match function to check if the node is the target mark.
|
|
36
|
+
/// For example:
|
|
37
|
+
///
|
|
38
|
+
/// ```typescript
|
|
39
|
+
/// match: (mark) => mark.type === 'emphasis'
|
|
40
|
+
/// ```
|
|
41
|
+
match: (node: MarkdownNode) => boolean
|
|
42
|
+
/// The runner function to transform the node into prosemirror mark.
|
|
43
|
+
/// Generally, you should call methods in `state` to add mark to state.
|
|
44
|
+
/// For example:
|
|
45
|
+
///
|
|
46
|
+
/// ```typescript
|
|
47
|
+
/// runner: (state, node, type) => {
|
|
48
|
+
/// state
|
|
49
|
+
/// .openMark(type)
|
|
50
|
+
/// .next(node.children)
|
|
51
|
+
/// .closeMark(type)
|
|
52
|
+
/// }
|
|
53
|
+
/// ```
|
|
54
|
+
runner: (state: ParserState, node: MarkdownNode, proseType: MarkType) => void
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { SerializerStackElement } from './stack-element'
|
|
4
|
+
|
|
5
|
+
it('serializer-stack-element', () => {
|
|
6
|
+
const element = SerializerStackElement.create('type')
|
|
7
|
+
|
|
8
|
+
expect(element.props).toEqual({})
|
|
9
|
+
|
|
10
|
+
element.push({ type: 'text' })
|
|
11
|
+
|
|
12
|
+
expect(element.children).toEqual([{ type: 'text' }])
|
|
13
|
+
|
|
14
|
+
const node = element.pop()
|
|
15
|
+
|
|
16
|
+
expect(node).toEqual({ type: 'text' })
|
|
17
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MarkdownNode } from '..'
|
|
2
|
+
import type { JSONRecord } from '../utility'
|
|
3
|
+
|
|
4
|
+
import { StackElement } from '../utility'
|
|
5
|
+
|
|
6
|
+
export class SerializerStackElement extends StackElement<MarkdownNode> {
|
|
7
|
+
constructor(
|
|
8
|
+
public type: string,
|
|
9
|
+
public children?: MarkdownNode[],
|
|
10
|
+
public value?: string,
|
|
11
|
+
public props: JSONRecord = {}
|
|
12
|
+
) {
|
|
13
|
+
super()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static create = (
|
|
17
|
+
type: string,
|
|
18
|
+
children?: MarkdownNode[],
|
|
19
|
+
value?: string,
|
|
20
|
+
props: JSONRecord = {}
|
|
21
|
+
) => new SerializerStackElement(type, children, value, props)
|
|
22
|
+
|
|
23
|
+
push = (node: MarkdownNode, ...rest: MarkdownNode[]) => {
|
|
24
|
+
if (!this.children) this.children = []
|
|
25
|
+
|
|
26
|
+
this.children.push(node, ...rest)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pop = (): MarkdownNode | undefined => this.children?.pop()
|
|
30
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { Mark, Schema } from '@jvs-milkdown/prose/model'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { SerializerState } from './state'
|
|
6
|
+
|
|
7
|
+
const boldMark = {
|
|
8
|
+
isInSet: (arr: string[]) => arr.includes('bold'),
|
|
9
|
+
addToSet: (arr: string[]) => arr.concat('bold'),
|
|
10
|
+
type: {
|
|
11
|
+
removeFromSet: (arr: string[]) => arr.filter((x) => x !== 'bold'),
|
|
12
|
+
},
|
|
13
|
+
} as unknown as Mark
|
|
14
|
+
|
|
15
|
+
const italicMark = {
|
|
16
|
+
isInSet: (arr: string[]) => arr.includes('italic'),
|
|
17
|
+
addToSet: (arr: string[]) => arr.concat('italic'),
|
|
18
|
+
type: {
|
|
19
|
+
removeFromSet: (arr: string[]) => arr.filter((x) => x !== 'italic'),
|
|
20
|
+
},
|
|
21
|
+
} as unknown as Mark
|
|
22
|
+
|
|
23
|
+
const inlineCodeMark = {
|
|
24
|
+
isInSet: (arr: string[]) => arr.includes('inlineCode'),
|
|
25
|
+
addToSet: (arr: string[]) => arr.concat('inlineCode'),
|
|
26
|
+
type: {
|
|
27
|
+
removeFromSet: (arr: string[]) => arr.filter((x) => x !== 'inlineCode'),
|
|
28
|
+
},
|
|
29
|
+
} as unknown as Mark
|
|
30
|
+
|
|
31
|
+
const schema = {
|
|
32
|
+
nodes: {
|
|
33
|
+
paragraph: {
|
|
34
|
+
spec: {
|
|
35
|
+
toMarkdown: {
|
|
36
|
+
match: (node: { type: string }) => node.type === 'paragraph',
|
|
37
|
+
runner: (state: SerializerState, node: { value: string }) => {
|
|
38
|
+
state.addNode('text', [], node.value)
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
blockquote: {
|
|
44
|
+
spec: {
|
|
45
|
+
toMarkdown: {
|
|
46
|
+
match: (node: { type: string }) => node.type === 'blockquote',
|
|
47
|
+
runner: (state: SerializerState, node: { content: never }) => {
|
|
48
|
+
state.openNode('blockquote')
|
|
49
|
+
state.next(node.content)
|
|
50
|
+
state.closeNode()
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
marks: {},
|
|
57
|
+
text: (text: string, marks: string[]) => ({ text, marks, isText: true }),
|
|
58
|
+
} as unknown as Schema
|
|
59
|
+
|
|
60
|
+
describe('serializer-state', () => {
|
|
61
|
+
it('node', () => {
|
|
62
|
+
const state = new SerializerState(schema)
|
|
63
|
+
state.openNode('doc')
|
|
64
|
+
state.openNode('paragraph', 'paragraph node value', { foo: 'bar' })
|
|
65
|
+
state.addNode('text', [], 'text node value')
|
|
66
|
+
state.closeNode()
|
|
67
|
+
|
|
68
|
+
expect(state.top()).toMatchObject({
|
|
69
|
+
type: 'doc',
|
|
70
|
+
children: [
|
|
71
|
+
{
|
|
72
|
+
type: 'paragraph',
|
|
73
|
+
value: 'paragraph node value',
|
|
74
|
+
children: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
value: 'text node value',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('maybe merge children for same mark', () => {
|
|
86
|
+
const state = new SerializerState(schema)
|
|
87
|
+
state.openNode('doc')
|
|
88
|
+
state.openNode('paragraph')
|
|
89
|
+
state.withMark(boldMark, 'bold')
|
|
90
|
+
state.addNode('text', [], 'The lunatic is on the grass.')
|
|
91
|
+
state.closeMark(boldMark)
|
|
92
|
+
state.withMark(boldMark, 'bold')
|
|
93
|
+
state.addNode('text', [], 'The lunatic is in the hell.')
|
|
94
|
+
state.closeMark(boldMark)
|
|
95
|
+
state.closeNode()
|
|
96
|
+
|
|
97
|
+
expect(state.top()).toMatchObject({
|
|
98
|
+
type: 'doc',
|
|
99
|
+
children: [
|
|
100
|
+
{
|
|
101
|
+
type: 'paragraph',
|
|
102
|
+
children: [
|
|
103
|
+
{
|
|
104
|
+
type: 'bold',
|
|
105
|
+
isMark: true,
|
|
106
|
+
children: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
value: 'The lunatic is on the grass.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
value: 'The lunatic is in the hell.',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('build', () => {
|
|
124
|
+
const state = new SerializerState(schema)
|
|
125
|
+
state.openNode('doc')
|
|
126
|
+
state.openNode('paragraph', 'paragraph node value', { foo: 'bar' })
|
|
127
|
+
state.addNode('text', [], 'text node value')
|
|
128
|
+
state.closeNode()
|
|
129
|
+
|
|
130
|
+
expect(state.build()).toMatchObject({
|
|
131
|
+
type: 'doc',
|
|
132
|
+
children: [
|
|
133
|
+
{
|
|
134
|
+
type: 'paragraph',
|
|
135
|
+
value: 'paragraph node value',
|
|
136
|
+
children: [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
value: 'text node value',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('next', () => {
|
|
148
|
+
const state = new SerializerState(schema)
|
|
149
|
+
state.openNode('doc')
|
|
150
|
+
state.next({
|
|
151
|
+
type: 'blockquote',
|
|
152
|
+
marks: [],
|
|
153
|
+
content: {
|
|
154
|
+
type: 'paragraph',
|
|
155
|
+
marks: [],
|
|
156
|
+
value: 'The lunatic is on the grass.',
|
|
157
|
+
},
|
|
158
|
+
} as any)
|
|
159
|
+
|
|
160
|
+
expect(state.build()).toMatchObject({
|
|
161
|
+
type: 'doc',
|
|
162
|
+
children: [
|
|
163
|
+
{
|
|
164
|
+
type: 'blockquote',
|
|
165
|
+
children: [
|
|
166
|
+
{
|
|
167
|
+
type: 'text',
|
|
168
|
+
value: 'The lunatic is on the grass.',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('trim spaces around marks', () => {
|
|
177
|
+
const state = new SerializerState(schema)
|
|
178
|
+
|
|
179
|
+
state.openNode('doc')
|
|
180
|
+
state.openNode('paragraph')
|
|
181
|
+
// Open a bold mark, add a text node with surrounding spaces, then close the mark
|
|
182
|
+
state.withMark(boldMark, 'bold')
|
|
183
|
+
state.addNode('text', [], ' hello ')
|
|
184
|
+
state.closeMark(boldMark)
|
|
185
|
+
state.closeNode() // close paragraph
|
|
186
|
+
|
|
187
|
+
// Expect SerializerState to move the spaces outside of the mark node.
|
|
188
|
+
expect(state.top()).toMatchObject({
|
|
189
|
+
type: 'doc',
|
|
190
|
+
children: [
|
|
191
|
+
{
|
|
192
|
+
type: 'paragraph',
|
|
193
|
+
children: [
|
|
194
|
+
{ type: 'text', value: ' ' },
|
|
195
|
+
{
|
|
196
|
+
type: 'bold',
|
|
197
|
+
isMark: true,
|
|
198
|
+
children: [{ type: 'text', value: 'hello' }],
|
|
199
|
+
},
|
|
200
|
+
{ type: 'text', value: ' ' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should not corrupt value-based marks during merge', () => {
|
|
208
|
+
const state = new SerializerState(schema)
|
|
209
|
+
|
|
210
|
+
state.openNode('doc')
|
|
211
|
+
state.openNode('paragraph')
|
|
212
|
+
// Simulate: inlineCode("Hello") followed by bold > inlineCode("World")
|
|
213
|
+
state.withMark(inlineCodeMark, 'inlineCode', 'Hello')
|
|
214
|
+
state.closeMark(inlineCodeMark)
|
|
215
|
+
state.withMark(boldMark, 'bold')
|
|
216
|
+
state.withMark(inlineCodeMark, 'inlineCode', 'World')
|
|
217
|
+
state.closeMark(inlineCodeMark)
|
|
218
|
+
state.closeMark(boldMark)
|
|
219
|
+
state.closeNode()
|
|
220
|
+
|
|
221
|
+
// The bold wrapper around inlineCode should be preserved,
|
|
222
|
+
// not restructured by searchType
|
|
223
|
+
expect(state.top()).toMatchObject({
|
|
224
|
+
type: 'doc',
|
|
225
|
+
children: [
|
|
226
|
+
{
|
|
227
|
+
type: 'paragraph',
|
|
228
|
+
children: [
|
|
229
|
+
{
|
|
230
|
+
type: 'inlineCode',
|
|
231
|
+
isMark: true,
|
|
232
|
+
value: 'Hello',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'bold',
|
|
236
|
+
isMark: true,
|
|
237
|
+
children: [
|
|
238
|
+
{
|
|
239
|
+
type: 'inlineCode',
|
|
240
|
+
isMark: true,
|
|
241
|
+
value: 'World',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('try to merge marks', () => {
|
|
252
|
+
const state = new SerializerState(schema)
|
|
253
|
+
|
|
254
|
+
state.openNode('doc')
|
|
255
|
+
state.openNode('paragraph')
|
|
256
|
+
state.withMark(boldMark, 'bold')
|
|
257
|
+
state.withMark(italicMark, 'italic')
|
|
258
|
+
state.addNode('text', [], 'hello')
|
|
259
|
+
state.closeMark(italicMark)
|
|
260
|
+
state.addNode('text', [], 'world')
|
|
261
|
+
state.closeMark(boldMark)
|
|
262
|
+
state.closeNode()
|
|
263
|
+
|
|
264
|
+
expect(state.top()).toMatchObject({
|
|
265
|
+
type: 'doc',
|
|
266
|
+
children: [
|
|
267
|
+
{
|
|
268
|
+
type: 'paragraph',
|
|
269
|
+
children: [
|
|
270
|
+
{
|
|
271
|
+
type: 'bold',
|
|
272
|
+
isMark: true,
|
|
273
|
+
children: [
|
|
274
|
+
{
|
|
275
|
+
type: 'italic',
|
|
276
|
+
isMark: true,
|
|
277
|
+
children: [{ type: 'text', value: 'hello' }],
|
|
278
|
+
},
|
|
279
|
+
{ type: 'text', value: 'world' },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
})
|