@bendyline/squisq-formats 2.0.1 → 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-CRAVSMPZ.js → chunk-26ISNJ7Y.js} +356 -114
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-HTW2M27H.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-U32AG3G3.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-QRVN6A6E.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-XKUMNGBW.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-ABVI556T.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-LXYLOOST.js → chunk-JE6LSIHE.js} +83 -22
  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-2KPARF2P.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-VSYHZECT.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-WC7WULGV.js → chunk-X2DEAXNK.js} +62 -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 -10
  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-2KPARF2P.js.map +0 -1
  62. package/dist/chunk-4VUWTSGM.js.map +0 -1
  63. package/dist/chunk-6M7Z25LA.js +0 -46
  64. package/dist/chunk-6M7Z25LA.js.map +0 -1
  65. package/dist/chunk-ABVI556T.js.map +0 -1
  66. package/dist/chunk-CRAVSMPZ.js.map +0 -1
  67. package/dist/chunk-FE6OJV6O.js.map +0 -1
  68. package/dist/chunk-GVS2XXV6.js.map +0 -1
  69. package/dist/chunk-HTW2M27H.js.map +0 -1
  70. package/dist/chunk-LXYLOOST.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-O3GVVND4.js.map +0 -1
  74. package/dist/chunk-ODL3SSPT.js.map +0 -1
  75. package/dist/chunk-PN52A5AA.js.map +0 -1
  76. package/dist/chunk-QRVN6A6E.js.map +0 -1
  77. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  78. package/dist/chunk-SSUPBUF5.js.map +0 -1
  79. package/dist/chunk-U32AG3G3.js.map +0 -1
  80. package/dist/chunk-U4MRIFKL.js.map +0 -1
  81. package/dist/chunk-VJJM2SSH.js +0 -275
  82. package/dist/chunk-VJJM2SSH.js.map +0 -1
  83. package/dist/chunk-VSYHZECT.js.map +0 -1
  84. package/dist/chunk-WC7WULGV.js.map +0 -1
  85. package/dist/chunk-XKUMNGBW.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-H6RXJMHP.js +0 -15
  93. package/dist/extract-H6RXJMHP.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-QVPK3ZCU.js +0 -12
  100. package/dist/layouts-QVPK3ZCU.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 -457
  111. package/src/__tests__/docxImport.test.ts +0 -410
  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 -435
  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 -1267
  143. package/src/docx/import.ts +0 -995
  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 -196
  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 +0,0 @@
1
- {"version":3,"sources":["../../src/container/index.ts"],"sourcesContent":["/**\n * Container ZIP serialization — convert between ContentContainer and ZIP archives.\n *\n * Uses JSZip (already a formats dependency) to serialize a ContentContainer to\n * a ZIP blob and to deserialize a ZIP blob into a MemoryContentContainer.\n *\n * ZIP structure mirrors the container's flat path hierarchy directly:\n * index.md\n * images/hero.jpg\n * audio/narration.mp3\n * timing.json\n */\n\nimport JSZip from 'jszip';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport { MemoryContentContainer } from '@bendyline/squisq/storage';\nimport {\n assertSafeZipPath,\n openBoundedZipArchive,\n ZipSafetyError,\n type ZipSafetyLimits,\n type ZipSafetyErrorCode,\n type ZipSafetyErrorOptions,\n} from '../shared/zipSafety.js';\n\nexport type ZipToContainerOptions = ZipSafetyLimits;\nexport interface ContainerToZipOptions {\n /** STORE avoids producing archives that violate strict import compression-ratio policies. */\n compression?: 'STORE' | 'DEFLATE';\n /** JSZip DEFLATE level; ignored for STORE. */\n compressionLevel?: number;\n}\nexport { ZipSafetyError };\nexport type { ZipSafetyLimits, ZipSafetyErrorCode, ZipSafetyErrorOptions };\n\n/**\n * Serialize a ContentContainer to a ZIP blob.\n *\n * All files in the container are written to the ZIP archive preserving\n * their path structure. The resulting blob can be saved as a .zip file.\n *\n * @param container — The container to serialize\n * @returns A Blob containing the ZIP archive\n */\nexport async function containerToZip(\n container: ContentContainer,\n options: ContainerToZipOptions = {},\n): Promise<Blob> {\n const zip = new JSZip();\n const entries = await container.listFiles();\n\n for (const entry of entries) {\n assertSafeZipPath(entry.path);\n const data = await container.readFile(entry.path);\n if (data) {\n zip.file(entry.path, new Uint8Array(data));\n }\n }\n\n const compression = options.compression ?? 'DEFLATE';\n const compressionLevel = options.compressionLevel ?? 6;\n if (!Number.isSafeInteger(compressionLevel) || compressionLevel < 1 || compressionLevel > 9) {\n throw new Error('ZIP compression level must be an integer between 1 and 9');\n }\n return zip.generateAsync({\n type: 'blob',\n compression,\n ...(compression === 'DEFLATE' ? { compressionOptions: { level: compressionLevel } } : {}),\n });\n}\n\n/**\n * Deserialize a ZIP archive into a MemoryContentContainer.\n *\n * Reads all files from the ZIP and writes them into a new MemoryContentContainer.\n * Directory entries are skipped. The resulting container can be used immediately\n * for rendering, editing, or saving to persistent storage.\n *\n * @param zipData — The ZIP archive as ArrayBuffer, Uint8Array, or Blob\n * @returns A MemoryContentContainer populated with the ZIP's contents\n */\nexport async function zipToContainer(\n zipData: ArrayBuffer | Uint8Array | Blob,\n options: ZipToContainerOptions = {},\n): Promise<MemoryContentContainer> {\n const archive = await openBoundedZipArchive(zipData, options);\n const container = new MemoryContentContainer();\n for (const { path } of archive.entries) {\n const data = await archive.read(path);\n if (data) await container.writeFile(path, data);\n archive.release(path);\n }\n return container;\n}\n"],"mappings":";;;;;;;AAaA,OAAO,WAAW;AAElB,SAAS,8BAA8B;AA6BvC,eAAsB,eACpB,WACA,UAAiC,CAAC,GACnB;AACf,QAAM,MAAM,IAAI,MAAM;AACtB,QAAM,UAAU,MAAM,UAAU,UAAU;AAE1C,aAAW,SAAS,SAAS;AAC3B,sBAAkB,MAAM,IAAI;AAC5B,UAAM,OAAO,MAAM,UAAU,SAAS,MAAM,IAAI;AAChD,QAAI,MAAM;AACR,UAAI,KAAK,MAAM,MAAM,IAAI,WAAW,IAAI,CAAC;AAAA,IAC3C;AAAA,EACF;AAEA,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,mBAAmB,QAAQ,oBAAoB;AACrD,MAAI,CAAC,OAAO,cAAc,gBAAgB,KAAK,mBAAmB,KAAK,mBAAmB,GAAG;AAC3F,UAAM,IAAI,MAAM,0DAA0D;AAAA,EAC5E;AACA,SAAO,IAAI,cAAc;AAAA,IACvB,MAAM;AAAA,IACN;AAAA,IACA,GAAI,gBAAgB,YAAY,EAAE,oBAAoB,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;AAAA,EACzF,CAAC;AACH;AAYA,eAAsB,eACpB,SACA,UAAiC,CAAC,GACD;AACjC,QAAM,UAAU,MAAM,sBAAsB,SAAS,OAAO;AAC5D,QAAM,YAAY,IAAI,uBAAuB;AAC7C,aAAW,EAAE,KAAK,KAAK,QAAQ,SAAS;AACtC,UAAM,OAAO,MAAM,QAAQ,KAAK,IAAI;AACpC,QAAI,KAAM,OAAM,UAAU,UAAU,MAAM,IAAI;AAC9C,YAAQ,QAAQ,IAAI;AAAA,EACtB;AACA,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,15 +0,0 @@
1
- import {
2
- extractDocxTheme,
3
- extractPptxTheme,
4
- extractXlsxTheme
5
- } from "./chunk-2KPARF2P.js";
6
- import "./chunk-XKUMNGBW.js";
7
- import "./chunk-YRT7GQ5Y.js";
8
- import "./chunk-WC7WULGV.js";
9
- import "./chunk-FE6OJV6O.js";
10
- export {
11
- extractDocxTheme,
12
- extractPptxTheme,
13
- extractXlsxTheme
14
- };
15
- //# sourceMappingURL=extract-H6RXJMHP.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,7 +0,0 @@
1
- import {
2
- extToMime
3
- } from "./chunk-O3GVVND4.js";
4
- export {
5
- extToMime
6
- };
7
- //# sourceMappingURL=images-7FBWPKE3.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,12 +0,0 @@
1
- import {
2
- analyzePptxLayouts,
3
- inspectPptxLayouts
4
- } from "./chunk-VSYHZECT.js";
5
- import "./chunk-YRT7GQ5Y.js";
6
- import "./chunk-WC7WULGV.js";
7
- import "./chunk-FE6OJV6O.js";
8
- export {
9
- analyzePptxLayouts,
10
- inspectPptxLayouts
11
- };
12
- //# sourceMappingURL=layouts-QVPK3ZCU.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,230 +0,0 @@
1
- /**
2
- * Tests for container ZIP serialization: containerToZip, zipToContainer.
3
- *
4
- * Verifies round-trip fidelity: write files to a container, serialize to ZIP,
5
- * deserialize back, and confirm all files are intact.
6
- */
7
-
8
- import { describe, it, expect } from 'vitest';
9
- import JSZip from 'jszip';
10
- import { MemoryContentContainer } from '@bendyline/squisq/storage';
11
- import { containerToZip, zipToContainer } from '../container/index';
12
-
13
- // ============================================
14
- // Helpers
15
- // ============================================
16
-
17
- /** Helper to read a Blob as ArrayBuffer (works in jsdom) */
18
- async function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
19
- return new Promise((resolve, reject) => {
20
- const reader = new FileReader();
21
- reader.onload = () => resolve(reader.result as ArrayBuffer);
22
- reader.onerror = () => reject(reader.error);
23
- reader.readAsArrayBuffer(blob);
24
- });
25
- }
26
-
27
- // ============================================
28
- // containerToZip
29
- // ============================================
30
-
31
- describe('containerToZip', () => {
32
- it('creates a valid ZIP from an empty container', async () => {
33
- const container = new MemoryContentContainer();
34
- const blob = await containerToZip(container);
35
- expect(blob).toBeInstanceOf(Blob);
36
- expect(blob.size).toBeGreaterThan(0);
37
-
38
- // Should be parseable as a ZIP with no files
39
- const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
40
- expect(Object.keys(zip.files)).toHaveLength(0);
41
- });
42
-
43
- it('includes all files from the container', async () => {
44
- const container = new MemoryContentContainer();
45
- await container.writeDocument('# Hello World');
46
- await container.writeFile('images/hero.jpg', new Uint8Array([0xff, 0xd8, 0xff]), 'image/jpeg');
47
- await container.writeFile('timing.json', new TextEncoder().encode('{"duration":10}'));
48
-
49
- const blob = await containerToZip(container);
50
- const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
51
-
52
- const filenames = Object.keys(zip.files)
53
- .filter((f) => !zip.files[f].dir)
54
- .sort();
55
- expect(filenames).toEqual(['images/hero.jpg', 'index.md', 'timing.json']);
56
- });
57
-
58
- it('preserves file content', async () => {
59
- const container = new MemoryContentContainer();
60
- const markdown = '# Test Document\n\nSome content.';
61
- await container.writeDocument(markdown);
62
-
63
- const blob = await containerToZip(container);
64
- const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
65
-
66
- const content = await zip.file('index.md')!.async('string');
67
- expect(content).toBe(markdown);
68
- });
69
-
70
- it('preserves binary content', async () => {
71
- const container = new MemoryContentContainer();
72
- const binaryData = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
73
- await container.writeFile('image.png', binaryData, 'image/png');
74
-
75
- const blob = await containerToZip(container);
76
- const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
77
-
78
- const data = await zip.file('image.png')!.async('uint8array');
79
- expect(data).toEqual(binaryData);
80
- });
81
-
82
- it('refuses to create a ZIP with an unsafe container path', async () => {
83
- const container = new MemoryContentContainer();
84
- await container.writeFile('../outside.txt', new TextEncoder().encode('nope'));
85
-
86
- await expect(containerToZip(container)).rejects.toThrow(/path is unsafe/i);
87
- });
88
- });
89
-
90
- // ============================================
91
- // zipToContainer
92
- // ============================================
93
-
94
- describe('zipToContainer', () => {
95
- it('loads files from a ZIP', async () => {
96
- const zip = new JSZip();
97
- zip.file('index.md', '# Hello');
98
- zip.file('images/photo.jpg', new Uint8Array([0xff, 0xd8]));
99
- const zipBlob = await zip.generateAsync({ type: 'blob' });
100
-
101
- const container = await zipToContainer(zipBlob);
102
- const entries = await container.listFiles();
103
- expect(entries.map((e) => e.path).sort()).toEqual(['images/photo.jpg', 'index.md']);
104
- });
105
-
106
- it('preserves text content', async () => {
107
- const zip = new JSZip();
108
- zip.file('index.md', '# My Document\n\nParagraph.');
109
- const zipBlob = await zip.generateAsync({ type: 'blob' });
110
-
111
- const container = await zipToContainer(zipBlob);
112
- expect(await container.readDocument()).toBe('# My Document\n\nParagraph.');
113
- });
114
-
115
- it('preserves binary content', async () => {
116
- const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
117
- const zip = new JSZip();
118
- zip.file('data.bin', original);
119
- const zipBlob = await zip.generateAsync({ type: 'blob' });
120
-
121
- const container = await zipToContainer(zipBlob);
122
- const data = await container.readFile('data.bin');
123
- expect(new Uint8Array(data!)).toEqual(original);
124
- });
125
-
126
- it('skips directory entries', async () => {
127
- const zip = new JSZip();
128
- zip.folder('images');
129
- zip.file('images/photo.jpg', new Uint8Array([0xff]));
130
- const zipBlob = await zip.generateAsync({ type: 'blob' });
131
-
132
- const container = await zipToContainer(zipBlob);
133
- const entries = await container.listFiles();
134
- // Only the file, not the directory
135
- expect(entries).toHaveLength(1);
136
- expect(entries[0].path).toBe('images/photo.jpg');
137
- });
138
-
139
- it('works with ArrayBuffer input', async () => {
140
- const zip = new JSZip();
141
- zip.file('readme.md', '# Read me');
142
- const ab = await zip.generateAsync({ type: 'arraybuffer' });
143
-
144
- const container = await zipToContainer(ab);
145
- expect(await container.readDocument()).toBe('# Read me');
146
- });
147
-
148
- it('rejects archives that exceed the configured entry limit', async () => {
149
- const zip = new JSZip();
150
- zip.file('one.txt', '1');
151
- zip.file('two.txt', '2');
152
- const data = await zip.generateAsync({ type: 'arraybuffer' });
153
- await expect(zipToContainer(data, { maxEntries: 1 })).rejects.toThrow(/archive has 2 files/);
154
- });
155
-
156
- it('rejects archives that exceed the configured uncompressed byte limit', async () => {
157
- const zip = new JSZip();
158
- zip.file('large.txt', '12345');
159
- const data = await zip.generateAsync({ type: 'arraybuffer' });
160
- await expect(zipToContainer(data, { maxUncompressedBytes: 4 })).rejects.toThrow(
161
- /uncompressed content exceeds 4 byte limit/,
162
- );
163
- });
164
-
165
- it('rejects traversal paths even when JSZip sanitizes their loaded key', async () => {
166
- const zip = new JSZip();
167
- zip.file('../outside.txt', 'nope');
168
- const data = await zip.generateAsync({ type: 'arraybuffer' });
169
-
170
- await expect(zipToContainer(data)).rejects.toThrow(/unsafe path.*\.\.\/outside\.txt/i);
171
- });
172
-
173
- it('rejects absolute and Windows-style archive paths', async () => {
174
- for (const path of ['/absolute.txt', 'C:\\outside.txt']) {
175
- const zip = new JSZip();
176
- zip.file(path, 'nope');
177
- const data = await zip.generateAsync({ type: 'arraybuffer' });
178
- await expect(zipToContainer(data)).rejects.toThrow(/unsafe path/i);
179
- }
180
- });
181
- });
182
-
183
- // ============================================
184
- // Round-trip
185
- // ============================================
186
-
187
- describe('containerToZip / zipToContainer round-trip', () => {
188
- it('round-trips a container with markdown and media', async () => {
189
- // Create original container
190
- const original = new MemoryContentContainer();
191
- const markdown = '# Round Trip Test\n\n![hero](images/hero.jpg)\n\nSome text.';
192
- await original.writeDocument(markdown);
193
- await original.writeFile(
194
- 'images/hero.jpg',
195
- new Uint8Array([0xff, 0xd8, 0xff, 0xe0]),
196
- 'image/jpeg',
197
- );
198
- await original.writeFile(
199
- 'audio/narration.mp3',
200
- new Uint8Array([0x49, 0x44, 0x33]),
201
- 'audio/mpeg',
202
- );
203
- await original.writeFile('timing.json', new TextEncoder().encode('{"segments":[]}'));
204
-
205
- // Serialize to ZIP
206
- const zipBlob = await containerToZip(original);
207
-
208
- // Deserialize back
209
- const restored = await zipToContainer(zipBlob);
210
-
211
- // Verify document
212
- expect(await restored.readDocument()).toBe(markdown);
213
- expect(await restored.getDocumentPath()).toBe('index.md');
214
-
215
- // Verify all files present
216
- const entries = (await restored.listFiles()).map((e) => e.path).sort();
217
- expect(entries).toEqual(['audio/narration.mp3', 'images/hero.jpg', 'index.md', 'timing.json']);
218
-
219
- // Verify binary content
220
- const heroData = new Uint8Array((await restored.readFile('images/hero.jpg'))!);
221
- expect(heroData).toEqual(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]));
222
-
223
- const audioData = new Uint8Array((await restored.readFile('audio/narration.mp3'))!);
224
- expect(audioData).toEqual(new Uint8Array([0x49, 0x44, 0x33]));
225
-
226
- // Verify JSON sidecar
227
- const timingData = new TextDecoder().decode((await restored.readFile('timing.json'))!);
228
- expect(timingData).toBe('{"segments":[]}');
229
- });
230
- });