@bendyline/squisq-formats 1.0.1 → 1.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.
@@ -0,0 +1,8 @@
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
+ export {};
8
+ //# sourceMappingURL=container.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/container.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,147 @@
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
+ import { describe, it, expect } from 'vitest';
8
+ import JSZip from 'jszip';
9
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
10
+ import { containerToZip, zipToContainer } from '../container/index';
11
+ // ============================================
12
+ // Helpers
13
+ // ============================================
14
+ /** Helper to read a Blob as ArrayBuffer (works in jsdom) */
15
+ async function blobToArrayBuffer(blob) {
16
+ return new Promise((resolve, reject) => {
17
+ const reader = new FileReader();
18
+ reader.onload = () => resolve(reader.result);
19
+ reader.onerror = () => reject(reader.error);
20
+ reader.readAsArrayBuffer(blob);
21
+ });
22
+ }
23
+ // ============================================
24
+ // containerToZip
25
+ // ============================================
26
+ describe('containerToZip', () => {
27
+ it('creates a valid ZIP from an empty container', async () => {
28
+ const container = new MemoryContentContainer();
29
+ const blob = await containerToZip(container);
30
+ expect(blob).toBeInstanceOf(Blob);
31
+ expect(blob.size).toBeGreaterThan(0);
32
+ // Should be parseable as a ZIP with no files
33
+ const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
34
+ expect(Object.keys(zip.files)).toHaveLength(0);
35
+ });
36
+ it('includes all files from the container', async () => {
37
+ const container = new MemoryContentContainer();
38
+ await container.writeDocument('# Hello World');
39
+ await container.writeFile('images/hero.jpg', new Uint8Array([0xff, 0xd8, 0xff]), 'image/jpeg');
40
+ await container.writeFile('timing.json', new TextEncoder().encode('{"duration":10}'));
41
+ const blob = await containerToZip(container);
42
+ const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
43
+ const filenames = Object.keys(zip.files)
44
+ .filter((f) => !zip.files[f].dir)
45
+ .sort();
46
+ expect(filenames).toEqual(['images/hero.jpg', 'index.md', 'timing.json']);
47
+ });
48
+ it('preserves file content', async () => {
49
+ const container = new MemoryContentContainer();
50
+ const markdown = '# Test Document\n\nSome content.';
51
+ await container.writeDocument(markdown);
52
+ const blob = await containerToZip(container);
53
+ const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
54
+ const content = await zip.file('index.md').async('string');
55
+ expect(content).toBe(markdown);
56
+ });
57
+ it('preserves binary content', async () => {
58
+ const container = new MemoryContentContainer();
59
+ const binaryData = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
60
+ await container.writeFile('image.png', binaryData, 'image/png');
61
+ const blob = await containerToZip(container);
62
+ const zip = await JSZip.loadAsync(await blobToArrayBuffer(blob));
63
+ const data = await zip.file('image.png').async('uint8array');
64
+ expect(data).toEqual(binaryData);
65
+ });
66
+ });
67
+ // ============================================
68
+ // zipToContainer
69
+ // ============================================
70
+ describe('zipToContainer', () => {
71
+ it('loads files from a ZIP', async () => {
72
+ const zip = new JSZip();
73
+ zip.file('index.md', '# Hello');
74
+ zip.file('images/photo.jpg', new Uint8Array([0xff, 0xd8]));
75
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
76
+ const container = await zipToContainer(zipBlob);
77
+ const entries = await container.listFiles();
78
+ expect(entries.map((e) => e.path).sort()).toEqual(['images/photo.jpg', 'index.md']);
79
+ });
80
+ it('preserves text content', async () => {
81
+ const zip = new JSZip();
82
+ zip.file('index.md', '# My Document\n\nParagraph.');
83
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
84
+ const container = await zipToContainer(zipBlob);
85
+ expect(await container.readDocument()).toBe('# My Document\n\nParagraph.');
86
+ });
87
+ it('preserves binary content', async () => {
88
+ const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
89
+ const zip = new JSZip();
90
+ zip.file('data.bin', original);
91
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
92
+ const container = await zipToContainer(zipBlob);
93
+ const data = await container.readFile('data.bin');
94
+ expect(new Uint8Array(data)).toEqual(original);
95
+ });
96
+ it('skips directory entries', async () => {
97
+ const zip = new JSZip();
98
+ zip.folder('images');
99
+ zip.file('images/photo.jpg', new Uint8Array([0xff]));
100
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
101
+ const container = await zipToContainer(zipBlob);
102
+ const entries = await container.listFiles();
103
+ // Only the file, not the directory
104
+ expect(entries).toHaveLength(1);
105
+ expect(entries[0].path).toBe('images/photo.jpg');
106
+ });
107
+ it('works with ArrayBuffer input', async () => {
108
+ const zip = new JSZip();
109
+ zip.file('readme.md', '# Read me');
110
+ const ab = await zip.generateAsync({ type: 'arraybuffer' });
111
+ const container = await zipToContainer(ab);
112
+ expect(await container.readDocument()).toBe('# Read me');
113
+ });
114
+ });
115
+ // ============================================
116
+ // Round-trip
117
+ // ============================================
118
+ describe('containerToZip / zipToContainer round-trip', () => {
119
+ it('round-trips a container with markdown and media', async () => {
120
+ // Create original container
121
+ const original = new MemoryContentContainer();
122
+ const markdown = '# Round Trip Test\n\n![hero](images/hero.jpg)\n\nSome text.';
123
+ await original.writeDocument(markdown);
124
+ await original.writeFile('images/hero.jpg', new Uint8Array([0xff, 0xd8, 0xff, 0xe0]), 'image/jpeg');
125
+ await original.writeFile('audio/narration.mp3', new Uint8Array([0x49, 0x44, 0x33]), 'audio/mpeg');
126
+ await original.writeFile('timing.json', new TextEncoder().encode('{"segments":[]}'));
127
+ // Serialize to ZIP
128
+ const zipBlob = await containerToZip(original);
129
+ // Deserialize back
130
+ const restored = await zipToContainer(zipBlob);
131
+ // Verify document
132
+ expect(await restored.readDocument()).toBe(markdown);
133
+ expect(await restored.getDocumentPath()).toBe('index.md');
134
+ // Verify all files present
135
+ const entries = (await restored.listFiles()).map((e) => e.path).sort();
136
+ expect(entries).toEqual(['audio/narration.mp3', 'images/hero.jpg', 'index.md', 'timing.json']);
137
+ // Verify binary content
138
+ const heroData = new Uint8Array((await restored.readFile('images/hero.jpg')));
139
+ expect(heroData).toEqual(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]));
140
+ const audioData = new Uint8Array((await restored.readFile('audio/narration.mp3')));
141
+ expect(audioData).toEqual(new Uint8Array([0x49, 0x44, 0x33]));
142
+ // Verify JSON sidecar
143
+ const timingData = new TextDecoder().decode((await restored.readFile('timing.json')));
144
+ expect(timingData).toBe('{"segments":[]}');
145
+ });
146
+ });
147
+ //# sourceMappingURL=container.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.test.js","sourceRoot":"","sources":["../../src/__tests__/container.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Container ZIP serialization — convert between ContentContainer and ZIP archives.
3
+ *
4
+ * Uses JSZip (already a formats dependency) to serialize a ContentContainer to
5
+ * a ZIP blob and to deserialize a ZIP blob into a MemoryContentContainer.
6
+ *
7
+ * ZIP structure mirrors the container's flat path hierarchy directly:
8
+ * index.md
9
+ * images/hero.jpg
10
+ * audio/narration.mp3
11
+ * timing.json
12
+ */
13
+ import type { ContentContainer } from '@bendyline/squisq/storage';
14
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
15
+ /**
16
+ * Serialize a ContentContainer to a ZIP blob.
17
+ *
18
+ * All files in the container are written to the ZIP archive preserving
19
+ * their path structure. The resulting blob can be saved as a .zip file.
20
+ *
21
+ * @param container — The container to serialize
22
+ * @returns A Blob containing the ZIP archive
23
+ */
24
+ export declare function containerToZip(container: ContentContainer): Promise<Blob>;
25
+ /**
26
+ * Deserialize a ZIP archive into a MemoryContentContainer.
27
+ *
28
+ * Reads all files from the ZIP and writes them into a new MemoryContentContainer.
29
+ * Directory entries are skipped. The resulting container can be used immediately
30
+ * for rendering, editing, or saving to persistent storage.
31
+ *
32
+ * @param zipData — The ZIP archive as ArrayBuffer, Uint8Array, or Blob
33
+ * @returns A MemoryContentContainer populated with the ZIP's contents
34
+ */
35
+ export declare function zipToContainer(zipData: ArrayBuffer | Uint8Array | Blob): Promise<MemoryContentContainer>;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/container/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAEnE;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/E;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,WAAW,GAAG,UAAU,GAAG,IAAI,GACvC,OAAO,CAAC,sBAAsB,CAAC,CAgCjC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Container ZIP serialization — convert between ContentContainer and ZIP archives.
3
+ *
4
+ * Uses JSZip (already a formats dependency) to serialize a ContentContainer to
5
+ * a ZIP blob and to deserialize a ZIP blob into a MemoryContentContainer.
6
+ *
7
+ * ZIP structure mirrors the container's flat path hierarchy directly:
8
+ * index.md
9
+ * images/hero.jpg
10
+ * audio/narration.mp3
11
+ * timing.json
12
+ */
13
+ import JSZip from 'jszip';
14
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
15
+ /**
16
+ * Serialize a ContentContainer to a ZIP blob.
17
+ *
18
+ * All files in the container are written to the ZIP archive preserving
19
+ * their path structure. The resulting blob can be saved as a .zip file.
20
+ *
21
+ * @param container — The container to serialize
22
+ * @returns A Blob containing the ZIP archive
23
+ */
24
+ export async function containerToZip(container) {
25
+ const zip = new JSZip();
26
+ const entries = await container.listFiles();
27
+ for (const entry of entries) {
28
+ const data = await container.readFile(entry.path);
29
+ if (data) {
30
+ zip.file(entry.path, new Uint8Array(data));
31
+ }
32
+ }
33
+ return zip.generateAsync({
34
+ type: 'blob',
35
+ compression: 'DEFLATE',
36
+ compressionOptions: { level: 6 },
37
+ });
38
+ }
39
+ /**
40
+ * Deserialize a ZIP archive into a MemoryContentContainer.
41
+ *
42
+ * Reads all files from the ZIP and writes them into a new MemoryContentContainer.
43
+ * Directory entries are skipped. The resulting container can be used immediately
44
+ * for rendering, editing, or saving to persistent storage.
45
+ *
46
+ * @param zipData — The ZIP archive as ArrayBuffer, Uint8Array, or Blob
47
+ * @returns A MemoryContentContainer populated with the ZIP's contents
48
+ */
49
+ export async function zipToContainer(zipData) {
50
+ const zip = await JSZip.loadAsync(zipData);
51
+ const container = new MemoryContentContainer();
52
+ const filePromises = [];
53
+ zip.forEach((relativePath, zipEntry) => {
54
+ // Skip directories
55
+ if (zipEntry.dir)
56
+ return;
57
+ // Strip leading slash if present
58
+ const entryPath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
59
+ if (!entryPath)
60
+ return;
61
+ // Path-traversal protection: reject absolute paths, backslashes, and .. segments
62
+ if (entryPath.startsWith('/') ||
63
+ entryPath.includes('\\') ||
64
+ entryPath.split('/').some((seg) => seg === '..')) {
65
+ return;
66
+ }
67
+ filePromises.push(zipEntry.async('arraybuffer').then((data) => {
68
+ return container.writeFile(entryPath, data);
69
+ }));
70
+ });
71
+ await Promise.all(filePromises);
72
+ return container;
73
+ }
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/container/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAEnE;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAA2B;IAC9D,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC;IACxB,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,SAAS,EAAE,CAAC;IAE5C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,IAAI,EAAE,CAAC;YACT,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC,aAAa,CAAC;QACvB,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,SAAS;QACtB,kBAAkB,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE;KACjC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,OAAwC;IAExC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,sBAAsB,EAAE,CAAC;IAE/C,MAAM,YAAY,GAAoB,EAAE,CAAC;IAEzC,GAAG,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,QAAQ,EAAE,EAAE;QACrC,mBAAmB;QACnB,IAAI,QAAQ,CAAC,GAAG;YAAE,OAAO;QAEzB,iCAAiC;QACjC,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;QACtF,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,iFAAiF;QACjF,IACE,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC;YACzB,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;YACxB,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,EAChD,CAAC;YACD,OAAO;QACT,CAAC;QAED,YAAY,CAAC,IAAI,CACf,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YAC1C,OAAO,SAAS,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC9C,CAAC,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAChC,OAAO,SAAS,CAAC;AACnB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -65,6 +65,11 @@
65
65
  "types": "./dist/html/index.d.ts",
66
66
  "import": "./dist/html/index.js",
67
67
  "default": "./dist/html/index.js"
68
+ },
69
+ "./container": {
70
+ "types": "./dist/container/index.d.ts",
71
+ "import": "./dist/container/index.js",
72
+ "default": "./dist/container/index.js"
68
73
  }
69
74
  },
70
75
  "scripts": {
@@ -72,7 +77,7 @@
72
77
  "typecheck": "tsc --noEmit"
73
78
  },
74
79
  "dependencies": {
75
- "@bendyline/squisq": "1.0.1",
80
+ "@bendyline/squisq": "1.1.0",
76
81
  "jszip": "^3.10.1",
77
82
  "pdf-lib": "^1.17.1",
78
83
  "pdfjs-dist": "^4.9.155"
@@ -0,0 +1,189 @@
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
+
83
+ // ============================================
84
+ // zipToContainer
85
+ // ============================================
86
+
87
+ describe('zipToContainer', () => {
88
+ it('loads files from a ZIP', async () => {
89
+ const zip = new JSZip();
90
+ zip.file('index.md', '# Hello');
91
+ zip.file('images/photo.jpg', new Uint8Array([0xff, 0xd8]));
92
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
93
+
94
+ const container = await zipToContainer(zipBlob);
95
+ const entries = await container.listFiles();
96
+ expect(entries.map((e) => e.path).sort()).toEqual(['images/photo.jpg', 'index.md']);
97
+ });
98
+
99
+ it('preserves text content', async () => {
100
+ const zip = new JSZip();
101
+ zip.file('index.md', '# My Document\n\nParagraph.');
102
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
103
+
104
+ const container = await zipToContainer(zipBlob);
105
+ expect(await container.readDocument()).toBe('# My Document\n\nParagraph.');
106
+ });
107
+
108
+ it('preserves binary content', async () => {
109
+ const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
110
+ const zip = new JSZip();
111
+ zip.file('data.bin', original);
112
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
113
+
114
+ const container = await zipToContainer(zipBlob);
115
+ const data = await container.readFile('data.bin');
116
+ expect(new Uint8Array(data!)).toEqual(original);
117
+ });
118
+
119
+ it('skips directory entries', async () => {
120
+ const zip = new JSZip();
121
+ zip.folder('images');
122
+ zip.file('images/photo.jpg', new Uint8Array([0xff]));
123
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
124
+
125
+ const container = await zipToContainer(zipBlob);
126
+ const entries = await container.listFiles();
127
+ // Only the file, not the directory
128
+ expect(entries).toHaveLength(1);
129
+ expect(entries[0].path).toBe('images/photo.jpg');
130
+ });
131
+
132
+ it('works with ArrayBuffer input', async () => {
133
+ const zip = new JSZip();
134
+ zip.file('readme.md', '# Read me');
135
+ const ab = await zip.generateAsync({ type: 'arraybuffer' });
136
+
137
+ const container = await zipToContainer(ab);
138
+ expect(await container.readDocument()).toBe('# Read me');
139
+ });
140
+ });
141
+
142
+ // ============================================
143
+ // Round-trip
144
+ // ============================================
145
+
146
+ describe('containerToZip / zipToContainer round-trip', () => {
147
+ it('round-trips a container with markdown and media', async () => {
148
+ // Create original container
149
+ const original = new MemoryContentContainer();
150
+ const markdown = '# Round Trip Test\n\n![hero](images/hero.jpg)\n\nSome text.';
151
+ await original.writeDocument(markdown);
152
+ await original.writeFile(
153
+ 'images/hero.jpg',
154
+ new Uint8Array([0xff, 0xd8, 0xff, 0xe0]),
155
+ 'image/jpeg',
156
+ );
157
+ await original.writeFile(
158
+ 'audio/narration.mp3',
159
+ new Uint8Array([0x49, 0x44, 0x33]),
160
+ 'audio/mpeg',
161
+ );
162
+ await original.writeFile('timing.json', new TextEncoder().encode('{"segments":[]}'));
163
+
164
+ // Serialize to ZIP
165
+ const zipBlob = await containerToZip(original);
166
+
167
+ // Deserialize back
168
+ const restored = await zipToContainer(zipBlob);
169
+
170
+ // Verify document
171
+ expect(await restored.readDocument()).toBe(markdown);
172
+ expect(await restored.getDocumentPath()).toBe('index.md');
173
+
174
+ // Verify all files present
175
+ const entries = (await restored.listFiles()).map((e) => e.path).sort();
176
+ expect(entries).toEqual(['audio/narration.mp3', 'images/hero.jpg', 'index.md', 'timing.json']);
177
+
178
+ // Verify binary content
179
+ const heroData = new Uint8Array((await restored.readFile('images/hero.jpg'))!);
180
+ expect(heroData).toEqual(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]));
181
+
182
+ const audioData = new Uint8Array((await restored.readFile('audio/narration.mp3'))!);
183
+ expect(audioData).toEqual(new Uint8Array([0x49, 0x44, 0x33]));
184
+
185
+ // Verify JSON sidecar
186
+ const timingData = new TextDecoder().decode((await restored.readFile('timing.json'))!);
187
+ expect(timingData).toBe('{"segments":[]}');
188
+ });
189
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Container ZIP serialization — convert between ContentContainer and ZIP archives.
3
+ *
4
+ * Uses JSZip (already a formats dependency) to serialize a ContentContainer to
5
+ * a ZIP blob and to deserialize a ZIP blob into a MemoryContentContainer.
6
+ *
7
+ * ZIP structure mirrors the container's flat path hierarchy directly:
8
+ * index.md
9
+ * images/hero.jpg
10
+ * audio/narration.mp3
11
+ * timing.json
12
+ */
13
+
14
+ import JSZip from 'jszip';
15
+ import type { ContentContainer } from '@bendyline/squisq/storage';
16
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
17
+
18
+ /**
19
+ * Serialize a ContentContainer to a ZIP blob.
20
+ *
21
+ * All files in the container are written to the ZIP archive preserving
22
+ * their path structure. The resulting blob can be saved as a .zip file.
23
+ *
24
+ * @param container — The container to serialize
25
+ * @returns A Blob containing the ZIP archive
26
+ */
27
+ export async function containerToZip(container: ContentContainer): Promise<Blob> {
28
+ const zip = new JSZip();
29
+ const entries = await container.listFiles();
30
+
31
+ for (const entry of entries) {
32
+ const data = await container.readFile(entry.path);
33
+ if (data) {
34
+ zip.file(entry.path, new Uint8Array(data));
35
+ }
36
+ }
37
+
38
+ return zip.generateAsync({
39
+ type: 'blob',
40
+ compression: 'DEFLATE',
41
+ compressionOptions: { level: 6 },
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Deserialize a ZIP archive into a MemoryContentContainer.
47
+ *
48
+ * Reads all files from the ZIP and writes them into a new MemoryContentContainer.
49
+ * Directory entries are skipped. The resulting container can be used immediately
50
+ * for rendering, editing, or saving to persistent storage.
51
+ *
52
+ * @param zipData — The ZIP archive as ArrayBuffer, Uint8Array, or Blob
53
+ * @returns A MemoryContentContainer populated with the ZIP's contents
54
+ */
55
+ export async function zipToContainer(
56
+ zipData: ArrayBuffer | Uint8Array | Blob,
57
+ ): Promise<MemoryContentContainer> {
58
+ const zip = await JSZip.loadAsync(zipData);
59
+ const container = new MemoryContentContainer();
60
+
61
+ const filePromises: Promise<void>[] = [];
62
+
63
+ zip.forEach((relativePath, zipEntry) => {
64
+ // Skip directories
65
+ if (zipEntry.dir) return;
66
+
67
+ // Strip leading slash if present
68
+ const entryPath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
69
+ if (!entryPath) return;
70
+
71
+ // Path-traversal protection: reject absolute paths, backslashes, and .. segments
72
+ if (
73
+ entryPath.startsWith('/') ||
74
+ entryPath.includes('\\') ||
75
+ entryPath.split('/').some((seg) => seg === '..')
76
+ ) {
77
+ return;
78
+ }
79
+
80
+ filePromises.push(
81
+ zipEntry.async('arraybuffer').then((data) => {
82
+ return container.writeFile(entryPath, data);
83
+ }),
84
+ );
85
+ });
86
+
87
+ await Promise.all(filePromises);
88
+ return container;
89
+ }