@bendyline/squisq-formats 0.1.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.
Files changed (61) hide show
  1. package/dist/chunk-532L4D5D.js +21 -0
  2. package/dist/chunk-532L4D5D.js.map +1 -0
  3. package/dist/chunk-67KIJHV2.js +21 -0
  4. package/dist/chunk-67KIJHV2.js.map +1 -0
  5. package/dist/chunk-KAK4V57E.js +387 -0
  6. package/dist/chunk-KAK4V57E.js.map +1 -0
  7. package/dist/chunk-MQHCXI56.js +1035 -0
  8. package/dist/chunk-MQHCXI56.js.map +1 -0
  9. package/dist/chunk-TBPD5PCU.js +1136 -0
  10. package/dist/chunk-TBPD5PCU.js.map +1 -0
  11. package/dist/chunk-ULLIPBEJ.js +271 -0
  12. package/dist/chunk-ULLIPBEJ.js.map +1 -0
  13. package/dist/docx/index.d.ts +108 -0
  14. package/dist/docx/index.js +14 -0
  15. package/dist/docx/index.js.map +1 -0
  16. package/dist/html/index.d.ts +166 -0
  17. package/dist/html/index.js +17 -0
  18. package/dist/html/index.js.map +1 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.js +54 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/ooxml/index.d.ts +351 -0
  23. package/dist/ooxml/index.js +99 -0
  24. package/dist/ooxml/index.js.map +1 -0
  25. package/dist/pdf/index.d.ts +130 -0
  26. package/dist/pdf/index.js +15 -0
  27. package/dist/pdf/index.js.map +1 -0
  28. package/dist/pptx/index.d.ts +58 -0
  29. package/dist/pptx/index.js +13 -0
  30. package/dist/pptx/index.js.map +1 -0
  31. package/dist/xlsx/index.d.ts +58 -0
  32. package/dist/xlsx/index.js +13 -0
  33. package/dist/xlsx/index.js.map +1 -0
  34. package/package.json +85 -0
  35. package/src/__tests__/docxExport.test.ts +457 -0
  36. package/src/__tests__/docxImport.test.ts +410 -0
  37. package/src/__tests__/html.test.ts +295 -0
  38. package/src/__tests__/ooxml.test.ts +197 -0
  39. package/src/__tests__/pdfExport.test.ts +322 -0
  40. package/src/__tests__/pdfImport.test.ts +290 -0
  41. package/src/__tests__/roundTrip.test.ts +201 -0
  42. package/src/docx/export.ts +978 -0
  43. package/src/docx/import.ts +909 -0
  44. package/src/docx/index.ts +26 -0
  45. package/src/docx/styles.ts +145 -0
  46. package/src/html/htmlTemplate.ts +307 -0
  47. package/src/html/imageUtils.ts +66 -0
  48. package/src/html/index.ts +168 -0
  49. package/src/index.ts +51 -0
  50. package/src/ooxml/index.ts +87 -0
  51. package/src/ooxml/namespaces.ts +160 -0
  52. package/src/ooxml/reader.ts +218 -0
  53. package/src/ooxml/types.ts +104 -0
  54. package/src/ooxml/writer.ts +321 -0
  55. package/src/ooxml/xmlUtils.ts +123 -0
  56. package/src/pdf/export.ts +1029 -0
  57. package/src/pdf/import.ts +835 -0
  58. package/src/pdf/index.ts +29 -0
  59. package/src/pdf/styles.ts +180 -0
  60. package/src/pptx/index.ts +78 -0
  61. package/src/xlsx/index.ts +78 -0
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Tests for DOCX export: markdownDocToDocx, docToDocx.
3
+ *
4
+ * Exports a MarkdownDocument to .docx, then unzips and inspects
5
+ * the OOXML structure to verify correctness.
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+ import JSZip from 'jszip';
10
+ import type {
11
+ MarkdownDocument,
12
+ MarkdownHeading,
13
+ MarkdownList,
14
+ MarkdownListItem,
15
+ MarkdownTable,
16
+ MarkdownTableRow,
17
+ MarkdownTableCell,
18
+ MarkdownCodeBlock,
19
+ MarkdownBlockquote,
20
+ MarkdownThematicBreak,
21
+ MarkdownStrong,
22
+ MarkdownEmphasis,
23
+ MarkdownInlineCode,
24
+ MarkdownLink,
25
+ } from '@bendyline/squisq/markdown';
26
+
27
+ import { markdownDocToDocx, docToDocx } from '../docx/export';
28
+
29
+ // ============================================
30
+ // Helpers
31
+ // ============================================
32
+
33
+ async function exportAndParse(
34
+ doc: MarkdownDocument,
35
+ options?: Parameters<typeof markdownDocToDocx>[1],
36
+ ) {
37
+ const buffer = await markdownDocToDocx(doc, options);
38
+ expect(buffer).toBeInstanceOf(ArrayBuffer);
39
+ expect(buffer.byteLength).toBeGreaterThan(0);
40
+
41
+ const zip = await JSZip.loadAsync(buffer);
42
+ return zip;
43
+ }
44
+
45
+ async function getDocumentXml(zip: JSZip): Promise<Document> {
46
+ const text = await zip.file('word/document.xml')!.async('text');
47
+ return new DOMParser().parseFromString(text, 'application/xml');
48
+ }
49
+
50
+ function _getBodyParagraphs(doc: Document): Element[] {
51
+ const body = doc.getElementsByTagName('w:body')[0];
52
+ return Array.from(body.children).filter((el) => el.localName === 'p');
53
+ }
54
+
55
+ // ============================================
56
+ // Basic Structure
57
+ // ============================================
58
+
59
+ describe('markdownDocToDocx', () => {
60
+ it('produces a valid zip with required parts', async () => {
61
+ const doc: MarkdownDocument = {
62
+ type: 'document',
63
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Hello' }] }],
64
+ };
65
+
66
+ const zip = await exportAndParse(doc);
67
+
68
+ expect(zip.file('[Content_Types].xml')).not.toBeNull();
69
+ expect(zip.file('_rels/.rels')).not.toBeNull();
70
+ expect(zip.file('word/document.xml')).not.toBeNull();
71
+ expect(zip.file('word/styles.xml')).not.toBeNull();
72
+ expect(zip.file('word/settings.xml')).not.toBeNull();
73
+ expect(zip.file('word/fontTable.xml')).not.toBeNull();
74
+ });
75
+
76
+ it('exports an empty document', async () => {
77
+ const doc: MarkdownDocument = { type: 'document', children: [] };
78
+ const zip = await exportAndParse(doc);
79
+ const xmlDoc = await getDocumentXml(zip);
80
+ const body = xmlDoc.getElementsByTagName('w:body')[0];
81
+ expect(body).toBeDefined();
82
+ });
83
+
84
+ // ============================================
85
+ // Headings
86
+ // ============================================
87
+
88
+ it('exports headings with correct styles', async () => {
89
+ const doc: MarkdownDocument = {
90
+ type: 'document',
91
+ children: [
92
+ {
93
+ type: 'heading',
94
+ depth: 1,
95
+ children: [{ type: 'text', value: 'Title' }],
96
+ } satisfies MarkdownHeading,
97
+ {
98
+ type: 'heading',
99
+ depth: 2,
100
+ children: [{ type: 'text', value: 'Subtitle' }],
101
+ } satisfies MarkdownHeading,
102
+ {
103
+ type: 'heading',
104
+ depth: 3,
105
+ children: [{ type: 'text', value: 'Section' }],
106
+ } satisfies MarkdownHeading,
107
+ ],
108
+ };
109
+
110
+ const zip = await exportAndParse(doc);
111
+ const xmlText = await zip.file('word/document.xml')!.async('text');
112
+
113
+ // Verify heading styles are present
114
+ expect(xmlText).toContain('w:val="Heading1"');
115
+ expect(xmlText).toContain('w:val="Heading2"');
116
+ expect(xmlText).toContain('w:val="Heading3"');
117
+
118
+ // Verify heading text
119
+ expect(xmlText).toContain('Title');
120
+ expect(xmlText).toContain('Subtitle');
121
+ expect(xmlText).toContain('Section');
122
+ });
123
+
124
+ // ============================================
125
+ // Inline Formatting
126
+ // ============================================
127
+
128
+ it('exports bold text with w:b element', async () => {
129
+ const doc: MarkdownDocument = {
130
+ type: 'document',
131
+ children: [
132
+ {
133
+ type: 'paragraph',
134
+ children: [
135
+ {
136
+ type: 'strong',
137
+ children: [{ type: 'text', value: 'bold text' }],
138
+ } satisfies MarkdownStrong,
139
+ ],
140
+ },
141
+ ],
142
+ };
143
+
144
+ const zip = await exportAndParse(doc);
145
+ const xmlText = await zip.file('word/document.xml')!.async('text');
146
+ expect(xmlText).toContain('<w:b/>');
147
+ expect(xmlText).toContain('bold text');
148
+ });
149
+
150
+ it('exports italic text with w:i element', async () => {
151
+ const doc: MarkdownDocument = {
152
+ type: 'document',
153
+ children: [
154
+ {
155
+ type: 'paragraph',
156
+ children: [
157
+ {
158
+ type: 'emphasis',
159
+ children: [{ type: 'text', value: 'italic text' }],
160
+ } satisfies MarkdownEmphasis,
161
+ ],
162
+ },
163
+ ],
164
+ };
165
+
166
+ const zip = await exportAndParse(doc);
167
+ const xmlText = await zip.file('word/document.xml')!.async('text');
168
+ expect(xmlText).toContain('<w:i/>');
169
+ expect(xmlText).toContain('italic text');
170
+ });
171
+
172
+ it('exports inline code with monospace font', async () => {
173
+ const doc: MarkdownDocument = {
174
+ type: 'document',
175
+ children: [
176
+ {
177
+ type: 'paragraph',
178
+ children: [{ type: 'inlineCode', value: 'console.log()' } satisfies MarkdownInlineCode],
179
+ },
180
+ ],
181
+ };
182
+
183
+ const zip = await exportAndParse(doc);
184
+ const xmlText = await zip.file('word/document.xml')!.async('text');
185
+ expect(xmlText).toContain('Consolas');
186
+ expect(xmlText).toContain('console.log()');
187
+ });
188
+
189
+ // ============================================
190
+ // Lists
191
+ // ============================================
192
+
193
+ it('exports unordered list with numbering', async () => {
194
+ const doc: MarkdownDocument = {
195
+ type: 'document',
196
+ children: [
197
+ {
198
+ type: 'list',
199
+ ordered: false,
200
+ children: [
201
+ {
202
+ type: 'listItem',
203
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Item 1' }] }],
204
+ } satisfies MarkdownListItem,
205
+ {
206
+ type: 'listItem',
207
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Item 2' }] }],
208
+ } satisfies MarkdownListItem,
209
+ ],
210
+ } satisfies MarkdownList,
211
+ ],
212
+ };
213
+
214
+ const zip = await exportAndParse(doc);
215
+
216
+ // Should have numbering.xml
217
+ expect(zip.file('word/numbering.xml')).not.toBeNull();
218
+
219
+ const xmlText = await zip.file('word/document.xml')!.async('text');
220
+ expect(xmlText).toContain('Item 1');
221
+ expect(xmlText).toContain('Item 2');
222
+ expect(xmlText).toContain('w:numId');
223
+
224
+ // Numbering should be bullet type
225
+ const numXml = await zip.file('word/numbering.xml')!.async('text');
226
+ expect(numXml).toContain('w:val="bullet"');
227
+ });
228
+
229
+ it('exports ordered list with decimal numbering', async () => {
230
+ const doc: MarkdownDocument = {
231
+ type: 'document',
232
+ children: [
233
+ {
234
+ type: 'list',
235
+ ordered: true,
236
+ children: [
237
+ {
238
+ type: 'listItem',
239
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'First' }] }],
240
+ } satisfies MarkdownListItem,
241
+ ],
242
+ } satisfies MarkdownList,
243
+ ],
244
+ };
245
+
246
+ const zip = await exportAndParse(doc);
247
+ const numXml = await zip.file('word/numbering.xml')!.async('text');
248
+ expect(numXml).toContain('w:val="decimal"');
249
+ });
250
+
251
+ // ============================================
252
+ // Tables
253
+ // ============================================
254
+
255
+ it('exports tables with rows and cells', async () => {
256
+ const doc: MarkdownDocument = {
257
+ type: 'document',
258
+ children: [
259
+ {
260
+ type: 'table',
261
+ children: [
262
+ {
263
+ type: 'tableRow',
264
+ children: [
265
+ {
266
+ type: 'tableCell',
267
+ isHeader: true,
268
+ children: [{ type: 'text', value: 'Name' }],
269
+ } satisfies MarkdownTableCell,
270
+ {
271
+ type: 'tableCell',
272
+ isHeader: true,
273
+ children: [{ type: 'text', value: 'Value' }],
274
+ } satisfies MarkdownTableCell,
275
+ ],
276
+ } satisfies MarkdownTableRow,
277
+ {
278
+ type: 'tableRow',
279
+ children: [
280
+ {
281
+ type: 'tableCell',
282
+ children: [{ type: 'text', value: 'A' }],
283
+ } satisfies MarkdownTableCell,
284
+ {
285
+ type: 'tableCell',
286
+ children: [{ type: 'text', value: '1' }],
287
+ } satisfies MarkdownTableCell,
288
+ ],
289
+ } satisfies MarkdownTableRow,
290
+ ],
291
+ } satisfies MarkdownTable,
292
+ ],
293
+ };
294
+
295
+ const zip = await exportAndParse(doc);
296
+ const xmlText = await zip.file('word/document.xml')!.async('text');
297
+ expect(xmlText).toContain('<w:tbl>');
298
+ expect(xmlText).toContain('<w:tr>');
299
+ expect(xmlText).toContain('<w:tc>');
300
+ expect(xmlText).toContain('Name');
301
+ expect(xmlText).toContain('Value');
302
+ });
303
+
304
+ // ============================================
305
+ // Code Blocks
306
+ // ============================================
307
+
308
+ it('exports code blocks with code styling', async () => {
309
+ const doc: MarkdownDocument = {
310
+ type: 'document',
311
+ children: [
312
+ {
313
+ type: 'code',
314
+ value: 'const x = 1;\nconst y = 2;',
315
+ } satisfies MarkdownCodeBlock,
316
+ ],
317
+ };
318
+
319
+ const zip = await exportAndParse(doc);
320
+ const xmlText = await zip.file('word/document.xml')!.async('text');
321
+ expect(xmlText).toContain('const x = 1;');
322
+ expect(xmlText).toContain('const y = 2;');
323
+ expect(xmlText).toContain('Consolas');
324
+ });
325
+
326
+ // ============================================
327
+ // Blockquotes
328
+ // ============================================
329
+
330
+ it('exports blockquotes with quote style', async () => {
331
+ const doc: MarkdownDocument = {
332
+ type: 'document',
333
+ children: [
334
+ {
335
+ type: 'blockquote',
336
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'A wise quote' }] }],
337
+ } satisfies MarkdownBlockquote,
338
+ ],
339
+ };
340
+
341
+ const zip = await exportAndParse(doc);
342
+ const xmlText = await zip.file('word/document.xml')!.async('text');
343
+ expect(xmlText).toContain('Quote');
344
+ expect(xmlText).toContain('A wise quote');
345
+ });
346
+
347
+ // ============================================
348
+ // Hyperlinks
349
+ // ============================================
350
+
351
+ it('exports hyperlinks with relationship', async () => {
352
+ const doc: MarkdownDocument = {
353
+ type: 'document',
354
+ children: [
355
+ {
356
+ type: 'paragraph',
357
+ children: [
358
+ {
359
+ type: 'link',
360
+ url: 'https://example.com',
361
+ children: [{ type: 'text', value: 'click here' }],
362
+ } satisfies MarkdownLink,
363
+ ],
364
+ },
365
+ ],
366
+ };
367
+
368
+ const zip = await exportAndParse(doc);
369
+ const xmlText = await zip.file('word/document.xml')!.async('text');
370
+ expect(xmlText).toContain('<w:hyperlink');
371
+ expect(xmlText).toContain('click here');
372
+
373
+ // Check relationships
374
+ const relsText = await zip.file('word/_rels/document.xml.rels')!.async('text');
375
+ expect(relsText).toContain('https://example.com');
376
+ expect(relsText).toContain('TargetMode="External"');
377
+ });
378
+
379
+ // ============================================
380
+ // Thematic Break
381
+ // ============================================
382
+
383
+ it('exports thematic breaks', async () => {
384
+ const doc: MarkdownDocument = {
385
+ type: 'document',
386
+ children: [{ type: 'thematicBreak' } satisfies MarkdownThematicBreak],
387
+ };
388
+
389
+ const zip = await exportAndParse(doc);
390
+ const xmlText = await zip.file('word/document.xml')!.async('text');
391
+ expect(xmlText).toContain('w:pBdr');
392
+ expect(xmlText).toContain('w:bottom');
393
+ });
394
+
395
+ // ============================================
396
+ // Core Properties
397
+ // ============================================
398
+
399
+ it('includes core properties when options are provided', async () => {
400
+ const doc: MarkdownDocument = {
401
+ type: 'document',
402
+ children: [{ type: 'paragraph', children: [{ type: 'text', value: 'test' }] }],
403
+ };
404
+
405
+ const zip = await exportAndParse(doc, {
406
+ title: 'My Document',
407
+ author: 'Test Author',
408
+ });
409
+
410
+ const coreXml = await zip.file('docProps/core.xml')?.async('text');
411
+ expect(coreXml).toBeDefined();
412
+ expect(coreXml).toContain('My Document');
413
+ expect(coreXml).toContain('Test Author');
414
+ });
415
+ });
416
+
417
+ // ============================================
418
+ // docToDocx convenience wrapper
419
+ // ============================================
420
+
421
+ describe('docToDocx', () => {
422
+ it('converts a Doc to docx', async () => {
423
+ const doc = {
424
+ articleId: 'test',
425
+ duration: 10,
426
+ blocks: [
427
+ {
428
+ id: 'block-1',
429
+ startTime: 0,
430
+ duration: 5,
431
+ audioSegment: 0,
432
+ sourceHeading: {
433
+ type: 'heading' as const,
434
+ depth: 1 as const,
435
+ children: [{ type: 'text' as const, value: 'Test Heading' }],
436
+ },
437
+ contents: [
438
+ {
439
+ type: 'paragraph' as const,
440
+ children: [{ type: 'text' as const, value: 'Body text' }],
441
+ },
442
+ ],
443
+ },
444
+ ],
445
+ audio: { segments: [] },
446
+ };
447
+
448
+ const buffer = await docToDocx(doc);
449
+ expect(buffer).toBeInstanceOf(ArrayBuffer);
450
+ expect(buffer.byteLength).toBeGreaterThan(0);
451
+
452
+ const zip = await JSZip.loadAsync(buffer);
453
+ const xmlText = await zip.file('word/document.xml')!.async('text');
454
+ expect(xmlText).toContain('Test Heading');
455
+ expect(xmlText).toContain('Body text');
456
+ });
457
+ });