@bendyline/squisq-formats 2.1.0 → 2.2.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -11
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-4VUWTSGM.js.map +0 -1
  62. package/dist/chunk-6M7Z25LA.js +0 -46
  63. package/dist/chunk-6M7Z25LA.js.map +0 -1
  64. package/dist/chunk-74GO3FVS.js.map +0 -1
  65. package/dist/chunk-7ARKUCQT.js.map +0 -1
  66. package/dist/chunk-DTDF6QDP.js.map +0 -1
  67. package/dist/chunk-EW54IRRS.js.map +0 -1
  68. package/dist/chunk-FE6OJV6O.js.map +0 -1
  69. package/dist/chunk-GVS2XXV6.js.map +0 -1
  70. package/dist/chunk-KURGXM4I.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-NNHKUXKA.js.map +0 -1
  74. package/dist/chunk-O3GVVND4.js.map +0 -1
  75. package/dist/chunk-ODL3SSPT.js.map +0 -1
  76. package/dist/chunk-PN52A5AA.js.map +0 -1
  77. package/dist/chunk-QFLDYKCR.js.map +0 -1
  78. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  79. package/dist/chunk-RLU7UFYU.js.map +0 -1
  80. package/dist/chunk-SC67HYQJ.js.map +0 -1
  81. package/dist/chunk-SSUPBUF5.js.map +0 -1
  82. package/dist/chunk-U4MRIFKL.js.map +0 -1
  83. package/dist/chunk-UGYF5AZE.js +0 -275
  84. package/dist/chunk-UGYF5AZE.js.map +0 -1
  85. package/dist/chunk-WQSHGBLN.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-OJ7ZQV6P.js +0 -15
  93. package/dist/extract-OJ7ZQV6P.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-5VDIRPIJ.js +0 -12
  100. package/dist/layouts-5VDIRPIJ.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -491
  111. package/src/__tests__/docxImport.test.ts +0 -531
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -439
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1375
  143. package/src/docx/import.ts +0 -1250
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -217
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -1,531 +0,0 @@
1
- /**
2
- * Tests for DOCX import: docxToMarkdownDoc, docxToDoc.
3
- *
4
- * Builds minimal .docx test fixtures programmatically using our own
5
- * OOXML writer (dogfooding the shared layer), then imports them.
6
- */
7
-
8
- import { describe, it, expect } from 'vitest';
9
- import { createPackage } from '../ooxml/writer';
10
- import {
11
- NS_WML,
12
- NS_R,
13
- REL_OFFICE_DOCUMENT,
14
- REL_STYLES,
15
- REL_NUMBERING,
16
- REL_HYPERLINK,
17
- REL_HEADER,
18
- REL_FOOTER,
19
- CONTENT_TYPE_DOCX_DOCUMENT,
20
- CONTENT_TYPE_DOCX_STYLES,
21
- CONTENT_TYPE_DOCX_NUMBERING,
22
- CONTENT_TYPE_DOCX_HEADER,
23
- CONTENT_TYPE_DOCX_FOOTER,
24
- } from '../ooxml/namespaces';
25
- import { xmlDeclaration } from '../ooxml/xmlUtils';
26
- import { docxToMarkdownDoc, docxToDoc } from '../docx/import';
27
- import type {
28
- MarkdownHeading,
29
- MarkdownParagraph,
30
- MarkdownList,
31
- MarkdownTable,
32
- MarkdownText,
33
- MarkdownStrong,
34
- MarkdownEmphasis,
35
- MarkdownStrikethrough,
36
- MarkdownLink,
37
- MarkdownInlineNode,
38
- MarkdownContainerDirective,
39
- } from '@bendyline/squisq/markdown';
40
-
41
- // ============================================
42
- // Test Fixture Builder
43
- // ============================================
44
-
45
- interface DocxFixtureOptions {
46
- bodyXml: string;
47
- stylesXml?: string;
48
- numberingXml?: string;
49
- documentRels?: Array<{
50
- id: string;
51
- type: string;
52
- target: string;
53
- targetMode?: 'External';
54
- }>;
55
- extraParts?: Array<{ path: string; content: string; contentType: string }>;
56
- }
57
-
58
- async function buildTestDocx(options: DocxFixtureOptions): Promise<ArrayBuffer> {
59
- const pkg = createPackage();
60
-
61
- // document.xml
62
- const documentXml =
63
- xmlDeclaration() +
64
- `<w:document xmlns:w="${NS_WML}" xmlns:r="${NS_R}">` +
65
- `<w:body>${options.bodyXml}</w:body>` +
66
- `</w:document>`;
67
- pkg.addPart('word/document.xml', documentXml, CONTENT_TYPE_DOCX_DOCUMENT);
68
-
69
- // styles.xml
70
- if (options.stylesXml) {
71
- pkg.addPart('word/styles.xml', options.stylesXml, CONTENT_TYPE_DOCX_STYLES);
72
- } else {
73
- // Minimal default styles
74
- const defaultStyles =
75
- xmlDeclaration() +
76
- `<w:styles xmlns:w="${NS_WML}">` +
77
- `<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/></w:style>` +
78
- `<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/></w:style>` +
79
- `<w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/></w:style>` +
80
- `<w:style w:type="paragraph" w:styleId="Quote"><w:name w:val="Quote"/></w:style>` +
81
- `</w:styles>`;
82
- pkg.addPart('word/styles.xml', defaultStyles, CONTENT_TYPE_DOCX_STYLES);
83
- }
84
-
85
- // numbering.xml
86
- if (options.numberingXml) {
87
- pkg.addPart('word/numbering.xml', options.numberingXml, CONTENT_TYPE_DOCX_NUMBERING);
88
- }
89
-
90
- for (const part of options.extraParts ?? []) {
91
- pkg.addPart(part.path, part.content, part.contentType);
92
- }
93
-
94
- // Root relationship
95
- pkg.addRelationship('', {
96
- id: 'rId1',
97
- type: REL_OFFICE_DOCUMENT,
98
- target: 'word/document.xml',
99
- });
100
-
101
- // Document relationships
102
- pkg.addRelationship('word/document.xml', {
103
- id: 'rIdStyles',
104
- type: REL_STYLES,
105
- target: 'styles.xml',
106
- });
107
-
108
- if (options.numberingXml) {
109
- pkg.addRelationship('word/document.xml', {
110
- id: 'rIdNumbering',
111
- type: REL_NUMBERING,
112
- target: 'numbering.xml',
113
- });
114
- }
115
-
116
- if (options.documentRels) {
117
- for (const rel of options.documentRels) {
118
- pkg.addRelationship('word/document.xml', {
119
- id: rel.id,
120
- type: rel.type,
121
- target: rel.target,
122
- ...(rel.targetMode ? { targetMode: rel.targetMode } : {}),
123
- });
124
- }
125
- }
126
-
127
- return pkg.toArrayBuffer();
128
- }
129
-
130
- // ============================================
131
- // Headings
132
- // ============================================
133
-
134
- describe('docxToMarkdownDoc', () => {
135
- it('imports headings based on paragraph style', async () => {
136
- const data = await buildTestDocx({
137
- bodyXml:
138
- `<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>Title</w:t></w:r></w:p>` +
139
- `<w:p><w:pPr><w:pStyle w:val="Heading2"/></w:pPr><w:r><w:t>Subtitle</w:t></w:r></w:p>`,
140
- });
141
-
142
- const doc = await docxToMarkdownDoc(data);
143
- expect(doc.type).toBe('document');
144
- expect(doc.children.length).toBe(2);
145
-
146
- expect(doc.children[0].type).toBe('heading');
147
- const h1 = doc.children[0] as MarkdownHeading;
148
- expect(h1.depth).toBe(1);
149
- expect(h1.children[0].type).toBe('text');
150
- expect((h1.children[0] as MarkdownText).value).toBe('Title');
151
-
152
- expect(doc.children[1].type).toBe('heading');
153
- const h2 = doc.children[1] as MarkdownHeading;
154
- expect(h2.depth).toBe(2);
155
- expect((h2.children[0] as MarkdownText).value).toBe('Subtitle');
156
- });
157
-
158
- // ============================================
159
- // Paragraphs
160
- // ============================================
161
-
162
- it('imports plain paragraphs', async () => {
163
- const data = await buildTestDocx({
164
- bodyXml:
165
- `<w:p><w:r><w:t>Hello world</w:t></w:r></w:p>` +
166
- `<w:p><w:r><w:t>Second paragraph</w:t></w:r></w:p>`,
167
- });
168
-
169
- const doc = await docxToMarkdownDoc(data);
170
- expect(doc.children.length).toBe(2);
171
- expect(doc.children[0].type).toBe('paragraph');
172
- expect(((doc.children[0] as MarkdownParagraph).children[0] as MarkdownText).value).toBe(
173
- 'Hello world',
174
- );
175
- });
176
-
177
- it('imports visible content inside controls, revisions, and text boxes once', async () => {
178
- const data = await buildTestDocx({
179
- bodyXml:
180
- `<w:p>` +
181
- `<w:r><w:t>Body </w:t></w:r>` +
182
- `<w:sdt><w:sdtContent><w:r><w:t>controlled </w:t></w:r></w:sdtContent></w:sdt>` +
183
- `<w:ins><w:r><w:t>inserted </w:t></w:r></w:ins>` +
184
- `<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">` +
185
- `<mc:Choice><w:drawing><w:txbxContent><w:p><w:r><w:t>text box</w:t></w:r></w:p></w:txbxContent></w:drawing></mc:Choice>` +
186
- `<mc:Fallback><w:pict><w:txbxContent><w:p><w:r><w:t>fallback duplicate</w:t></w:r></w:p></w:txbxContent></w:pict></mc:Fallback>` +
187
- `</mc:AlternateContent>` +
188
- `</w:p>`,
189
- });
190
-
191
- const paragraph = docText(await docxToMarkdownDoc(data));
192
- expect(paragraph).toContain('Body controlled inserted text box');
193
- expect(paragraph).not.toContain('fallback duplicate');
194
- });
195
-
196
- it('skips empty paragraphs', async () => {
197
- const data = await buildTestDocx({
198
- bodyXml: `<w:p><w:pPr/></w:p><w:p><w:r><w:t>Text</w:t></w:r></w:p>`,
199
- });
200
-
201
- const doc = await docxToMarkdownDoc(data);
202
- expect(doc.children.length).toBe(1);
203
- expect(((doc.children[0] as MarkdownParagraph).children[0] as MarkdownText).value).toBe('Text');
204
- });
205
-
206
- // ============================================
207
- // Inline Formatting
208
- // ============================================
209
-
210
- it('imports bold text', async () => {
211
- const data = await buildTestDocx({
212
- bodyXml: `<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>bold</w:t></w:r></w:p>`,
213
- });
214
-
215
- const doc = await docxToMarkdownDoc(data);
216
- const para = doc.children[0] as MarkdownParagraph;
217
- expect(para.children[0].type).toBe('strong');
218
- const strong = para.children[0] as MarkdownStrong;
219
- expect(strong.children[0].type).toBe('text');
220
- expect((strong.children[0] as MarkdownText).value).toBe('bold');
221
- });
222
-
223
- it('imports italic text', async () => {
224
- const data = await buildTestDocx({
225
- bodyXml: `<w:p><w:r><w:rPr><w:i/></w:rPr><w:t>italic</w:t></w:r></w:p>`,
226
- });
227
-
228
- const doc = await docxToMarkdownDoc(data);
229
- const para = doc.children[0] as MarkdownParagraph;
230
- expect(para.children[0].type).toBe('emphasis');
231
- const em = para.children[0] as MarkdownEmphasis;
232
- expect((em.children[0] as MarkdownText).value).toBe('italic');
233
- });
234
-
235
- it('imports strikethrough text', async () => {
236
- const data = await buildTestDocx({
237
- bodyXml: `<w:p><w:r><w:rPr><w:strike/></w:rPr><w:t>struck</w:t></w:r></w:p>`,
238
- });
239
-
240
- const doc = await docxToMarkdownDoc(data);
241
- const para = doc.children[0] as MarkdownParagraph;
242
- const del = para.children[0] as MarkdownStrikethrough;
243
- expect(del.type).toBe('delete');
244
- expect((del.children[0] as MarkdownText).value).toBe('struck');
245
- });
246
-
247
- it('imports bold+italic combined formatting', async () => {
248
- const data = await buildTestDocx({
249
- bodyXml: `<w:p><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t>bold italic</w:t></w:r></w:p>`,
250
- });
251
-
252
- const doc = await docxToMarkdownDoc(data);
253
- const para = doc.children[0] as MarkdownParagraph;
254
- // Should be strong > emphasis > text (bold wraps italic wraps text)
255
- const strong = para.children[0] as MarkdownStrong;
256
- expect(strong.type).toBe('strong');
257
- const em = strong.children[0] as MarkdownEmphasis;
258
- expect(em.type).toBe('emphasis');
259
- expect((em.children[0] as MarkdownText).value).toBe('bold italic');
260
- });
261
-
262
- it('merges adjacent plain text runs', async () => {
263
- const data = await buildTestDocx({
264
- bodyXml: `<w:p>` + `<w:r><w:t>Hello </w:t></w:r>` + `<w:r><w:t>world</w:t></w:r>` + `</w:p>`,
265
- });
266
-
267
- const doc = await docxToMarkdownDoc(data);
268
- const para = doc.children[0] as MarkdownParagraph;
269
- expect(para.children.length).toBe(1);
270
- expect(para.children[0].type).toBe('text');
271
- expect((para.children[0] as MarkdownText).value).toBe('Hello world');
272
- });
273
-
274
- // ============================================
275
- // Hyperlinks
276
- // ============================================
277
-
278
- it('imports hyperlinks', async () => {
279
- const data = await buildTestDocx({
280
- bodyXml:
281
- `<w:p>` +
282
- `<w:hyperlink r:id="rId10">` +
283
- `<w:r><w:t>click me</w:t></w:r>` +
284
- `</w:hyperlink>` +
285
- `</w:p>`,
286
- documentRels: [
287
- {
288
- id: 'rId10',
289
- type: REL_HYPERLINK,
290
- target: 'https://example.com',
291
- targetMode: 'External',
292
- },
293
- ],
294
- });
295
-
296
- const doc = await docxToMarkdownDoc(data);
297
- const para = doc.children[0] as MarkdownParagraph;
298
- const link = para.children[0] as MarkdownLink;
299
- expect(link.type).toBe('link');
300
- expect(link.url).toBe('https://example.com');
301
- expect(link.children[0].type).toBe('text');
302
- expect((link.children[0] as MarkdownText).value).toBe('click me');
303
- });
304
-
305
- // ============================================
306
- // Lists
307
- // ============================================
308
-
309
- it('imports bullet lists', async () => {
310
- const numberingXml =
311
- xmlDeclaration() +
312
- `<w:numbering xmlns:w="${NS_WML}">` +
313
- `<w:abstractNum w:abstractNumId="1">` +
314
- `<w:lvl w:ilvl="0"><w:numFmt w:val="bullet"/></w:lvl>` +
315
- `</w:abstractNum>` +
316
- `<w:num w:numId="1"><w:abstractNumId w:val="1"/></w:num>` +
317
- `</w:numbering>`;
318
-
319
- const data = await buildTestDocx({
320
- bodyXml:
321
- `<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>` +
322
- `<w:r><w:t>Item A</w:t></w:r></w:p>` +
323
- `<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>` +
324
- `<w:r><w:t>Item B</w:t></w:r></w:p>`,
325
- numberingXml,
326
- });
327
-
328
- const doc = await docxToMarkdownDoc(data);
329
- expect(doc.children[0].type).toBe('list');
330
- const list = doc.children[0] as MarkdownList;
331
- expect(list.ordered).toBe(false);
332
- expect(list.children.length).toBe(2);
333
- expect(list.children[0].type).toBe('listItem');
334
- const listPara = list.children[0].children[0] as MarkdownParagraph;
335
- expect((listPara.children[0] as MarkdownText).value).toBe('Item A');
336
- });
337
-
338
- it('imports ordered lists', async () => {
339
- const numberingXml =
340
- xmlDeclaration() +
341
- `<w:numbering xmlns:w="${NS_WML}">` +
342
- `<w:abstractNum w:abstractNumId="2">` +
343
- `<w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/></w:lvl>` +
344
- `</w:abstractNum>` +
345
- `<w:num w:numId="2"><w:abstractNumId w:val="2"/></w:num>` +
346
- `</w:numbering>`;
347
-
348
- const data = await buildTestDocx({
349
- bodyXml:
350
- `<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr></w:pPr>` +
351
- `<w:r><w:t>First</w:t></w:r></w:p>` +
352
- `<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr></w:pPr>` +
353
- `<w:r><w:t>Second</w:t></w:r></w:p>`,
354
- numberingXml,
355
- });
356
-
357
- const doc = await docxToMarkdownDoc(data);
358
- const list = doc.children[0] as MarkdownList;
359
- expect(list.type).toBe('list');
360
- expect(list.ordered).toBe(true);
361
- });
362
-
363
- it('retains nested list text after an empty parent numbering paragraph', async () => {
364
- const numberingXml =
365
- xmlDeclaration() +
366
- `<w:numbering xmlns:w="${NS_WML}">` +
367
- `<w:abstractNum w:abstractNumId="3">` +
368
- `<w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/></w:lvl>` +
369
- `<w:lvl w:ilvl="1"><w:numFmt w:val="decimal"/></w:lvl>` +
370
- `</w:abstractNum>` +
371
- `<w:num w:numId="3"><w:abstractNumId w:val="3"/></w:num>` +
372
- `</w:numbering>`;
373
- const data = await buildTestDocx({
374
- bodyXml:
375
- `<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="3"/></w:numPr></w:pPr></w:p>` +
376
- `<w:p><w:pPr><w:numPr><w:ilvl w:val="1"/><w:numId w:val="3"/></w:numPr></w:pPr>` +
377
- `<w:r><w:t>Nested content survives</w:t></w:r></w:p>`,
378
- numberingXml,
379
- });
380
-
381
- expect(docText(await docxToMarkdownDoc(data))).toContain('Nested content survives');
382
- });
383
-
384
- // ============================================
385
- // Tables
386
- // ============================================
387
-
388
- it('imports tables', async () => {
389
- const data = await buildTestDocx({
390
- bodyXml:
391
- `<w:tbl>` +
392
- `<w:tr>` +
393
- `<w:tc><w:p><w:r><w:t>A</w:t></w:r></w:p></w:tc>` +
394
- `<w:tc><w:p><w:r><w:t>B</w:t></w:r></w:p></w:tc>` +
395
- `</w:tr>` +
396
- `<w:tr>` +
397
- `<w:tc><w:p><w:r><w:t>1</w:t></w:r></w:p></w:tc>` +
398
- `<w:tc><w:p><w:r><w:t>2</w:t></w:r></w:p></w:tc>` +
399
- `</w:tr>` +
400
- `</w:tbl>`,
401
- });
402
-
403
- const doc = await docxToMarkdownDoc(data);
404
- expect(doc.children[0].type).toBe('table');
405
- const table = doc.children[0] as MarkdownTable;
406
- expect(table.children.length).toBe(2);
407
- expect(table.children[0].children.length).toBe(2);
408
- const cell00 = table.children[0].children[0];
409
- const cell01 = table.children[0].children[1];
410
- const cell10 = table.children[1].children[0];
411
- expect((cell00.children[0] as MarkdownText).value).toBe('A');
412
- expect((cell01.children[0] as MarkdownText).value).toBe('B');
413
- expect((cell10.children[0] as MarkdownText).value).toBe('1');
414
- });
415
-
416
- it('flattens nested table contents without losing their text', async () => {
417
- const data = await buildTestDocx({
418
- bodyXml:
419
- `<w:tbl><w:tr><w:tc>` +
420
- `<w:p><w:r><w:t>Outer</w:t></w:r></w:p>` +
421
- `<w:tbl><w:tr>` +
422
- `<w:tc><w:p><w:r><w:t>Nested A</w:t></w:r></w:p></w:tc>` +
423
- `<w:tc><w:p><w:r><w:t>Nested B</w:t></w:r></w:p></w:tc>` +
424
- `</w:tr></w:tbl>` +
425
- `</w:tc></w:tr></w:tbl>`,
426
- });
427
-
428
- expect(docText(await docxToMarkdownDoc(data))).toContain('Outer Nested A Nested B');
429
- });
430
-
431
- it('imports headers and footers as explicit DOCX story directives', async () => {
432
- const headerXml =
433
- xmlDeclaration() +
434
- `<w:hdr xmlns:w="${NS_WML}"><w:p><w:r><w:t>Header text</w:t></w:r></w:p></w:hdr>`;
435
- const footerXml =
436
- xmlDeclaration() +
437
- `<w:ftr xmlns:w="${NS_WML}"><w:p><w:r><w:t>Footer text</w:t></w:r></w:p></w:ftr>`;
438
- const data = await buildTestDocx({
439
- bodyXml:
440
- `<w:p><w:r><w:t>Body text</w:t></w:r></w:p>` +
441
- `<w:sectPr><w:headerReference w:type="default" r:id="rIdHeader"/>` +
442
- `<w:footerReference w:type="default" r:id="rIdFooter"/></w:sectPr>`,
443
- documentRels: [
444
- { id: 'rIdHeader', type: REL_HEADER, target: 'header1.xml' },
445
- { id: 'rIdFooter', type: REL_FOOTER, target: 'footer1.xml' },
446
- ],
447
- extraParts: [
448
- { path: 'word/header1.xml', content: headerXml, contentType: CONTENT_TYPE_DOCX_HEADER },
449
- { path: 'word/footer1.xml', content: footerXml, contentType: CONTENT_TYPE_DOCX_FOOTER },
450
- ],
451
- });
452
-
453
- const doc = await docxToMarkdownDoc(data);
454
- const header = doc.children.find(
455
- (node): node is MarkdownContainerDirective =>
456
- node.type === 'containerDirective' && node.name === 'docx-header',
457
- );
458
- const footer = doc.children.find(
459
- (node): node is MarkdownContainerDirective =>
460
- node.type === 'containerDirective' && node.name === 'docx-footer',
461
- );
462
- expect(header?.attributes?.type).toBe('default');
463
- expect(footer?.attributes?.type).toBe('default');
464
- expect(docText(doc)).toContain('Body text Header text Footer text');
465
- });
466
-
467
- // ============================================
468
- // Empty Document
469
- // ============================================
470
-
471
- it('handles empty document', async () => {
472
- const data = await buildTestDocx({ bodyXml: '' });
473
- const doc = await docxToMarkdownDoc(data);
474
- expect(doc.type).toBe('document');
475
- expect(doc.children.length).toBe(0);
476
- });
477
-
478
- // ============================================
479
- // Line Breaks
480
- // ============================================
481
-
482
- it('imports line breaks', async () => {
483
- const data = await buildTestDocx({
484
- bodyXml: `<w:p><w:r><w:t>Before</w:t></w:r><w:r><w:br/></w:r><w:r><w:t>After</w:t></w:r></w:p>`,
485
- });
486
-
487
- const doc = await docxToMarkdownDoc(data);
488
- const para = doc.children[0] as MarkdownParagraph;
489
- expect(para.children.some((c: MarkdownInlineNode) => c.type === 'break')).toBe(true);
490
- });
491
-
492
- it('retains tabs as visible word boundaries', async () => {
493
- const data = await buildTestDocx({
494
- bodyXml: `<w:p><w:r><w:t>Before</w:t><w:tab/><w:t>After</w:t></w:r></w:p>`,
495
- });
496
-
497
- expect(docText(await docxToMarkdownDoc(data))).toBe('Before After');
498
- });
499
- });
500
-
501
- function docText(doc: Awaited<ReturnType<typeof docxToMarkdownDoc>>): string {
502
- const text: string[] = [];
503
- const visit = (node: unknown): void => {
504
- if (!node || typeof node !== 'object') return;
505
- const value = node as { type?: string; value?: string; children?: unknown[] };
506
- if ((value.type === 'text' || value.type === 'code') && value.value) text.push(value.value);
507
- value.children?.forEach(visit);
508
- };
509
- visit(doc);
510
- return text.join(' ').replace(/\s+/g, ' ').trim();
511
- }
512
-
513
- // ============================================
514
- // docxToDoc convenience wrapper
515
- // ============================================
516
-
517
- describe('docxToDoc', () => {
518
- it('converts docx to a squisq Doc', async () => {
519
- const data = await buildTestDocx({
520
- bodyXml:
521
- `<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>Main Title</w:t></w:r></w:p>` +
522
- `<w:p><w:r><w:t>Some content here.</w:t></w:r></w:p>`,
523
- });
524
-
525
- const doc = await docxToDoc(data);
526
- expect(doc.articleId).toBeDefined();
527
- expect(doc.blocks.length).toBeGreaterThan(0);
528
- // The first block should have the heading
529
- expect(doc.blocks[0].sourceHeading).toBeDefined();
530
- });
531
- });