@uniweb/content-writer 0.1.1
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 +674 -0
- package/package.json +43 -0
- package/src/attributes.js +49 -0
- package/src/frontmatter.js +34 -0
- package/src/index.js +36 -0
- package/src/marks.js +254 -0
- package/src/nodes.js +232 -0
- package/src/plain-text.js +123 -0
- package/src/serializer.js +62 -0
- package/tests/attributes.test.js +103 -0
- package/tests/frontmatter.test.js +69 -0
- package/tests/marks.test.js +315 -0
- package/tests/plain-text.test.js +218 -0
- package/tests/roundtrip.test.js +307 -0
- package/tests/serializer.test.js +652 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { serializeDoc } from '../src/serializer.js'
|
|
2
|
+
|
|
3
|
+
describe('Document Serialization', () => {
|
|
4
|
+
test('serializes plain text paragraph', () => {
|
|
5
|
+
const doc = {
|
|
6
|
+
type: 'doc',
|
|
7
|
+
content: [
|
|
8
|
+
{
|
|
9
|
+
type: 'paragraph',
|
|
10
|
+
content: [{ type: 'text', text: 'Hello world' }],
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
}
|
|
14
|
+
expect(serializeDoc(doc)).toBe('Hello world')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('serializes multiple paragraphs with blank line separation', () => {
|
|
18
|
+
const doc = {
|
|
19
|
+
type: 'doc',
|
|
20
|
+
content: [
|
|
21
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'First' }] },
|
|
22
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Second' }] },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
expect(serializeDoc(doc)).toBe('First\n\nSecond')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('serializes headings', () => {
|
|
29
|
+
const doc = {
|
|
30
|
+
type: 'doc',
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'heading',
|
|
34
|
+
attrs: { level: 1, id: null },
|
|
35
|
+
content: [{ type: 'text', text: 'Main Title' }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'heading',
|
|
39
|
+
attrs: { level: 2, id: null },
|
|
40
|
+
content: [{ type: 'text', text: 'Subtitle' }],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
expect(serializeDoc(doc)).toBe('# Main Title\n\n## Subtitle')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('serializes formatted text', () => {
|
|
48
|
+
const doc = {
|
|
49
|
+
type: 'doc',
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: 'paragraph',
|
|
53
|
+
content: [
|
|
54
|
+
{ type: 'text', text: 'Some ' },
|
|
55
|
+
{ type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
|
|
56
|
+
{ type: 'text', text: ' and ' },
|
|
57
|
+
{ type: 'text', text: 'italic', marks: [{ type: 'italic' }] },
|
|
58
|
+
{ type: 'text', text: ' text' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
expect(serializeDoc(doc)).toBe('Some **bold** and *italic* text')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('serializes dividers', () => {
|
|
67
|
+
const doc = {
|
|
68
|
+
type: 'doc',
|
|
69
|
+
content: [
|
|
70
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Text' }] },
|
|
71
|
+
{ type: 'divider', attrs: { style: 'line', size: 'normal' } },
|
|
72
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'More text' }] },
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
expect(serializeDoc(doc)).toBe('Text\n\n---\n\nMore text')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('Image Serialization', () => {
|
|
80
|
+
test('serializes basic image', () => {
|
|
81
|
+
const doc = {
|
|
82
|
+
type: 'doc',
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: 'image',
|
|
86
|
+
attrs: { src: 'path/to/image.svg', caption: null, alt: 'Alt Text', role: 'image' },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
expect(serializeDoc(doc)).toBe('')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('serializes image with caption', () => {
|
|
94
|
+
const doc = {
|
|
95
|
+
type: 'doc',
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'image',
|
|
99
|
+
attrs: {
|
|
100
|
+
src: './photo.jpg',
|
|
101
|
+
alt: 'Photo',
|
|
102
|
+
caption: 'A beautiful photo',
|
|
103
|
+
role: 'image',
|
|
104
|
+
width: 800,
|
|
105
|
+
height: 600,
|
|
106
|
+
loading: 'lazy',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}
|
|
111
|
+
expect(serializeDoc(doc)).toBe(
|
|
112
|
+
'{width=800 height=600 loading=lazy}'
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('serializes image with role attribute', () => {
|
|
117
|
+
const doc = {
|
|
118
|
+
type: 'doc',
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: 'image',
|
|
122
|
+
attrs: { src: './hero.jpg', alt: 'Hero Image', caption: null, role: 'hero' },
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
expect(serializeDoc(doc)).toBe('{role=hero}')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('serializes video with attributes', () => {
|
|
130
|
+
const doc = {
|
|
131
|
+
type: 'doc',
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'image',
|
|
135
|
+
attrs: {
|
|
136
|
+
src: './intro.mp4',
|
|
137
|
+
alt: 'Intro Video',
|
|
138
|
+
caption: null,
|
|
139
|
+
role: 'video',
|
|
140
|
+
poster: './poster.jpg',
|
|
141
|
+
autoplay: true,
|
|
142
|
+
muted: true,
|
|
143
|
+
loop: true,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
expect(serializeDoc(doc)).toBe(
|
|
149
|
+
'{role=video poster=./poster.jpg autoplay muted loop}'
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('serializes PDF with preview', () => {
|
|
154
|
+
const doc = {
|
|
155
|
+
type: 'doc',
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: 'image',
|
|
159
|
+
attrs: {
|
|
160
|
+
src: './guide.pdf',
|
|
161
|
+
alt: 'User Guide',
|
|
162
|
+
caption: null,
|
|
163
|
+
role: 'pdf',
|
|
164
|
+
preview: './guide-preview.jpg',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
expect(serializeDoc(doc)).toBe('{role=pdf preview=./guide-preview.jpg}')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('serializes image with class and id', () => {
|
|
173
|
+
const doc = {
|
|
174
|
+
type: 'doc',
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'image',
|
|
178
|
+
attrs: {
|
|
179
|
+
src: './logo.svg',
|
|
180
|
+
alt: 'Logo',
|
|
181
|
+
caption: null,
|
|
182
|
+
role: 'image',
|
|
183
|
+
class: 'featured',
|
|
184
|
+
id: 'main-logo',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
expect(serializeDoc(doc)).toBe('{.featured #main-logo}')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('serializes image with multiple classes', () => {
|
|
193
|
+
const doc = {
|
|
194
|
+
type: 'doc',
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'image',
|
|
198
|
+
attrs: {
|
|
199
|
+
src: './photo.jpg',
|
|
200
|
+
alt: 'Gallery',
|
|
201
|
+
caption: null,
|
|
202
|
+
role: 'image',
|
|
203
|
+
class: 'featured rounded shadow',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
expect(serializeDoc(doc)).toBe('{.featured .rounded .shadow}')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('serializes image with fit and position', () => {
|
|
212
|
+
const doc = {
|
|
213
|
+
type: 'doc',
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
type: 'image',
|
|
217
|
+
attrs: {
|
|
218
|
+
src: './bg.jpg',
|
|
219
|
+
alt: 'Background',
|
|
220
|
+
caption: null,
|
|
221
|
+
role: 'image',
|
|
222
|
+
fit: 'cover',
|
|
223
|
+
position: 'center',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
expect(serializeDoc(doc)).toBe('{fit=cover position=center}')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('serializes icon with library and name (dash format)', () => {
|
|
232
|
+
const doc = {
|
|
233
|
+
type: 'doc',
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'paragraph',
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: 'image',
|
|
240
|
+
attrs: {
|
|
241
|
+
src: null,
|
|
242
|
+
caption: null,
|
|
243
|
+
alt: null,
|
|
244
|
+
role: 'icon',
|
|
245
|
+
library: 'lu',
|
|
246
|
+
name: 'home',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
expect(serializeDoc(doc)).toBe('')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('serializes icon with src (icon: prefix)', () => {
|
|
257
|
+
const doc = {
|
|
258
|
+
type: 'doc',
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: 'paragraph',
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'image',
|
|
265
|
+
attrs: {
|
|
266
|
+
src: 'path/to/image.svg',
|
|
267
|
+
caption: 'Caption text',
|
|
268
|
+
alt: 'Alt Text',
|
|
269
|
+
role: 'icon',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
}
|
|
276
|
+
expect(serializeDoc(doc)).toBe('')
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
describe('Component References', () => {
|
|
281
|
+
test('serializes bare @ComponentName', () => {
|
|
282
|
+
const doc = {
|
|
283
|
+
type: 'doc',
|
|
284
|
+
content: [
|
|
285
|
+
{ type: 'inset_ref', attrs: { component: 'Hero', alt: null } },
|
|
286
|
+
],
|
|
287
|
+
}
|
|
288
|
+
expect(serializeDoc(doc)).toBe('')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('serializes @ComponentName with alt and params', () => {
|
|
292
|
+
const doc = {
|
|
293
|
+
type: 'doc',
|
|
294
|
+
content: [
|
|
295
|
+
{
|
|
296
|
+
type: 'inset_ref',
|
|
297
|
+
attrs: {
|
|
298
|
+
component: 'NetworkDiagram',
|
|
299
|
+
alt: 'Architecture diagram',
|
|
300
|
+
variant: 'compact',
|
|
301
|
+
size: 'lg',
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
}
|
|
306
|
+
expect(serializeDoc(doc)).toBe(
|
|
307
|
+
'{variant=compact size=lg}'
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('serializes multiple component refs', () => {
|
|
312
|
+
const doc = {
|
|
313
|
+
type: 'doc',
|
|
314
|
+
content: [
|
|
315
|
+
{ type: 'inset_ref', attrs: { component: 'Widget', alt: null } },
|
|
316
|
+
{ type: 'inset_ref', attrs: { component: 'Chart', alt: null } },
|
|
317
|
+
],
|
|
318
|
+
}
|
|
319
|
+
expect(serializeDoc(doc)).toBe('\n\n')
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe('Code Blocks', () => {
|
|
324
|
+
test('serializes fenced code block with language', () => {
|
|
325
|
+
const doc = {
|
|
326
|
+
type: 'doc',
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: 'codeBlock',
|
|
330
|
+
attrs: { language: 'javascript' },
|
|
331
|
+
content: [{ type: 'text', text: "const x = 1;\nconsole.log('x:', x);" }],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
}
|
|
335
|
+
expect(serializeDoc(doc)).toBe("```javascript\nconst x = 1;\nconsole.log('x:', x);\n```")
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('serializes code block without language', () => {
|
|
339
|
+
const doc = {
|
|
340
|
+
type: 'doc',
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: 'codeBlock',
|
|
344
|
+
attrs: { language: null },
|
|
345
|
+
content: [{ type: 'text', text: 'line 1\n\nline 2' }],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
}
|
|
349
|
+
expect(serializeDoc(doc)).toBe('```\nline 1\n\nline 2\n```')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('serializes data block', () => {
|
|
353
|
+
const doc = {
|
|
354
|
+
type: 'doc',
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: 'dataBlock',
|
|
358
|
+
attrs: { tag: 'nav-links', data: [{ label: 'Home' }] },
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
}
|
|
362
|
+
const expected = '```json:nav-links\n[\n {\n "label": "Home"\n }\n]\n```'
|
|
363
|
+
expect(serializeDoc(doc)).toBe(expected)
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
describe('Blockquotes', () => {
|
|
368
|
+
test('serializes simple blockquote', () => {
|
|
369
|
+
const doc = {
|
|
370
|
+
type: 'doc',
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: 'blockquote',
|
|
374
|
+
content: [
|
|
375
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Quoted text' }] },
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
}
|
|
380
|
+
expect(serializeDoc(doc)).toBe('> Quoted text')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('serializes multi-paragraph blockquote', () => {
|
|
384
|
+
const doc = {
|
|
385
|
+
type: 'doc',
|
|
386
|
+
content: [
|
|
387
|
+
{
|
|
388
|
+
type: 'blockquote',
|
|
389
|
+
content: [
|
|
390
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'First paragraph' }] },
|
|
391
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Second paragraph' }] },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
}
|
|
396
|
+
expect(serializeDoc(doc)).toBe('> First paragraph\n>\n> Second paragraph')
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
describe('Lists', () => {
|
|
401
|
+
test('serializes bullet list', () => {
|
|
402
|
+
const doc = {
|
|
403
|
+
type: 'doc',
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: 'bulletList',
|
|
407
|
+
content: [
|
|
408
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'First item' }] }] },
|
|
409
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Second item' }] }] },
|
|
410
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Third item' }] }] },
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
}
|
|
415
|
+
expect(serializeDoc(doc)).toBe('- First item\n- Second item\n- Third item')
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('serializes ordered list', () => {
|
|
419
|
+
const doc = {
|
|
420
|
+
type: 'doc',
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: 'orderedList',
|
|
424
|
+
attrs: { start: 1 },
|
|
425
|
+
content: [
|
|
426
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'First item' }] }] },
|
|
427
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Second item' }] }] },
|
|
428
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Third item' }] }] },
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
}
|
|
433
|
+
expect(serializeDoc(doc)).toBe('1. First item\n2. Second item\n3. Third item')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test('serializes nested lists', () => {
|
|
437
|
+
const doc = {
|
|
438
|
+
type: 'doc',
|
|
439
|
+
content: [
|
|
440
|
+
{
|
|
441
|
+
type: 'bulletList',
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: 'listItem',
|
|
445
|
+
content: [
|
|
446
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'First item' }] },
|
|
447
|
+
{
|
|
448
|
+
type: 'bulletList',
|
|
449
|
+
content: [
|
|
450
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Nested item 1' }] }] },
|
|
451
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Nested item 2' }] }] },
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
type: 'listItem',
|
|
458
|
+
content: [
|
|
459
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Second item' }] },
|
|
460
|
+
{
|
|
461
|
+
type: 'orderedList',
|
|
462
|
+
attrs: { start: 1 },
|
|
463
|
+
content: [
|
|
464
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Nested ordered 1' }] }] },
|
|
465
|
+
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Nested ordered 2' }] }] },
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
}
|
|
474
|
+
const expected = [
|
|
475
|
+
'- First item',
|
|
476
|
+
' - Nested item 1',
|
|
477
|
+
' - Nested item 2',
|
|
478
|
+
'- Second item',
|
|
479
|
+
' 1. Nested ordered 1',
|
|
480
|
+
' 2. Nested ordered 2',
|
|
481
|
+
].join('\n')
|
|
482
|
+
expect(serializeDoc(doc)).toBe(expected)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test('serializes list items with formatted text', () => {
|
|
486
|
+
const doc = {
|
|
487
|
+
type: 'doc',
|
|
488
|
+
content: [
|
|
489
|
+
{
|
|
490
|
+
type: 'bulletList',
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: 'listItem',
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: 'paragraph',
|
|
497
|
+
content: [
|
|
498
|
+
{ type: 'text', text: 'Item with ' },
|
|
499
|
+
{ type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
|
|
500
|
+
{ type: 'text', text: ' text' },
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
type: 'listItem',
|
|
507
|
+
content: [
|
|
508
|
+
{
|
|
509
|
+
type: 'paragraph',
|
|
510
|
+
content: [
|
|
511
|
+
{ type: 'text', text: 'Item with ' },
|
|
512
|
+
{
|
|
513
|
+
type: 'text',
|
|
514
|
+
text: 'link',
|
|
515
|
+
marks: [{ type: 'link', attrs: { href: 'https://example.com', title: null } }],
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
}
|
|
525
|
+
expect(serializeDoc(doc)).toBe(
|
|
526
|
+
'- Item with **bold** text\n- Item with [link](https://example.com)'
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
describe('Tables', () => {
|
|
532
|
+
test('serializes basic table', () => {
|
|
533
|
+
const doc = {
|
|
534
|
+
type: 'doc',
|
|
535
|
+
content: [
|
|
536
|
+
{
|
|
537
|
+
type: 'table',
|
|
538
|
+
content: [
|
|
539
|
+
{
|
|
540
|
+
type: 'tableRow',
|
|
541
|
+
content: [
|
|
542
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Column 1' }] }] },
|
|
543
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Column 2' }] }] },
|
|
544
|
+
],
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
type: 'tableRow',
|
|
548
|
+
content: [
|
|
549
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Cell 1' }] }] },
|
|
550
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Cell 2' }] }] },
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
}
|
|
557
|
+
const expected = [
|
|
558
|
+
'| Column 1 | Column 2 |',
|
|
559
|
+
'| --- | --- |',
|
|
560
|
+
'| Cell 1 | Cell 2 |',
|
|
561
|
+
].join('\n')
|
|
562
|
+
expect(serializeDoc(doc)).toBe(expected)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('serializes table with alignments', () => {
|
|
566
|
+
const doc = {
|
|
567
|
+
type: 'doc',
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
570
|
+
type: 'table',
|
|
571
|
+
content: [
|
|
572
|
+
{
|
|
573
|
+
type: 'tableRow',
|
|
574
|
+
content: [
|
|
575
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: 'left', header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Left' }] }] },
|
|
576
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: 'center', header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Center' }] }] },
|
|
577
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: 'right', header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Right' }] }] },
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
type: 'tableRow',
|
|
582
|
+
content: [
|
|
583
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: 'left', header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '1' }] }] },
|
|
584
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: 'center', header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '2' }] }] },
|
|
585
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: 'right', header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: '3' }] }] },
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
}
|
|
592
|
+
const expected = [
|
|
593
|
+
'| Left | Center | Right |',
|
|
594
|
+
'| :--- | :---: | ---: |',
|
|
595
|
+
'| 1 | 2 | 3 |',
|
|
596
|
+
].join('\n')
|
|
597
|
+
expect(serializeDoc(doc)).toBe(expected)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
test('serializes table with formatted content', () => {
|
|
601
|
+
const doc = {
|
|
602
|
+
type: 'doc',
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: 'table',
|
|
606
|
+
content: [
|
|
607
|
+
{
|
|
608
|
+
type: 'tableRow',
|
|
609
|
+
content: [
|
|
610
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Style' }] }] },
|
|
611
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: true }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Example' }] }] },
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
type: 'tableRow',
|
|
616
|
+
content: [
|
|
617
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Bold' }] }] },
|
|
618
|
+
{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, align: null, header: false }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'text', marks: [{ type: 'bold' }] }] }] },
|
|
619
|
+
],
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
}
|
|
625
|
+
const expected = [
|
|
626
|
+
'| Style | Example |',
|
|
627
|
+
'| --- | --- |',
|
|
628
|
+
'| Bold | **text** |',
|
|
629
|
+
].join('\n')
|
|
630
|
+
expect(serializeDoc(doc)).toBe(expected)
|
|
631
|
+
})
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
describe('Edge Cases', () => {
|
|
635
|
+
test('handles empty document', () => {
|
|
636
|
+
expect(serializeDoc({ type: 'doc', content: [] })).toBe('')
|
|
637
|
+
expect(serializeDoc({ type: 'doc' })).toBe('')
|
|
638
|
+
expect(serializeDoc(null)).toBe('')
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test('skips unknown node types', () => {
|
|
642
|
+
const doc = {
|
|
643
|
+
type: 'doc',
|
|
644
|
+
content: [
|
|
645
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Text' }] },
|
|
646
|
+
{ type: 'unknownNode', attrs: {} },
|
|
647
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'More' }] },
|
|
648
|
+
],
|
|
649
|
+
}
|
|
650
|
+
expect(serializeDoc(doc)).toBe('Text\n\nMore')
|
|
651
|
+
})
|
|
652
|
+
})
|