@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,168 @@
1
+ /**
2
+ * HTML Export Module — @bendyline/squisq-formats/html
3
+ *
4
+ * Exports squisq documents as self-contained HTML files or ZIP archives.
5
+ *
6
+ * Two export modes:
7
+ *
8
+ * 1. **Single HTML** (`docToHtml`) — Everything embedded in one file:
9
+ * - SquisqPlayer IIFE bundle inlined in `<script>`
10
+ * - Images base64-encoded as data URIs
11
+ * - Timer-based playback (no audio — too large for inline)
12
+ *
13
+ * 2. **ZIP Archive** (`docToHtmlZip`) — Multi-file package:
14
+ * - `index.html` referencing external `squisq-player.js`
15
+ * - `squisq-player.js` — the IIFE bundle
16
+ * - `images/` — extracted image files
17
+ * - `audio/` — optional audio segment files (enables full playback)
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';
22
+ * import { docToHtml, docToHtmlZip } from '@bendyline/squisq-formats/html';
23
+ *
24
+ * // Single HTML file
25
+ * const html = docToHtml(doc, { playerScript: PLAYER_BUNDLE, images });
26
+ *
27
+ * // ZIP archive
28
+ * const zipBlob = await docToHtmlZip(doc, { playerScript: PLAYER_BUNDLE, images, audio });
29
+ * ```
30
+ */
31
+
32
+ import type { Doc } from '@bendyline/squisq/schemas';
33
+ import JSZip from 'jszip';
34
+ import {
35
+ generateInlineHtml,
36
+ generateExternalHtml,
37
+ type HtmlExportOptions,
38
+ } from './htmlTemplate.js';
39
+ import { extractFilename } from './imageUtils.js';
40
+
41
+ // ── Public Types ───────────────────────────────────────────────────
42
+
43
+ export type { HtmlExportOptions };
44
+
45
+ export interface HtmlZipExportOptions extends HtmlExportOptions {
46
+ /**
47
+ * Map of audio segment identifiers to binary audio data.
48
+ * Keys should match the audio segment `name` or `url` fields in the Doc.
49
+ * When provided, audio files are included in the ZIP and full playback is enabled.
50
+ */
51
+ audio?: Map<string, ArrayBuffer>;
52
+ }
53
+
54
+ // ── Single HTML Export ─────────────────────────────────────────────
55
+
56
+ /**
57
+ * Export a Doc as a single, self-contained HTML file.
58
+ *
59
+ * The player JS is embedded inline, images are base64-encoded as data URIs,
60
+ * and playback uses a timer (no audio) for compact file size.
61
+ *
62
+ * @param doc - The Doc to export
63
+ * @param options - Export options (must include `playerScript`)
64
+ * @returns Complete HTML document as a string
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const html = docToHtml(myDoc, {
69
+ * playerScript: PLAYER_BUNDLE,
70
+ * images: imageMap,
71
+ * title: 'My Document',
72
+ * mode: 'slideshow',
73
+ * });
74
+ * const blob = new Blob([html], { type: 'text/html' });
75
+ * ```
76
+ */
77
+ export function docToHtml(doc: Doc, options: HtmlExportOptions): string {
78
+ return generateInlineHtml(doc, options);
79
+ }
80
+
81
+ // ── ZIP Archive Export ─────────────────────────────────────────────
82
+
83
+ /**
84
+ * Export a Doc as a ZIP archive containing HTML, JS, images, and optionally audio.
85
+ *
86
+ * The archive structure:
87
+ * ```
88
+ * document.zip
89
+ * ├── index.html # HTML page referencing squisq-player.js
90
+ * ├── squisq-player.js # Standalone IIFE bundle
91
+ * ├── images/ # Extracted image files
92
+ * │ ├── hero.jpg
93
+ * │ └── ...
94
+ * └── audio/ # Optional audio segment files
95
+ * ├── intro.mp3
96
+ * └── ...
97
+ * ```
98
+ *
99
+ * @param doc - The Doc to export
100
+ * @param options - Export options (must include `playerScript`)
101
+ * @returns A Promise resolving to a ZIP Blob
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const blob = await docToHtmlZip(myDoc, {
106
+ * playerScript: PLAYER_BUNDLE,
107
+ * images: imageMap,
108
+ * audio: audioMap,
109
+ * });
110
+ * // Trigger browser download
111
+ * const url = URL.createObjectURL(blob);
112
+ * ```
113
+ */
114
+ export async function docToHtmlZip(doc: Doc, options: HtmlZipExportOptions): Promise<Blob> {
115
+ const { playerScript, images, audio, mode = 'slideshow', title, autoPlay } = options;
116
+
117
+ const zip = new JSZip();
118
+
119
+ // 1. Add player JS as a separate file
120
+ zip.file('squisq-player.js', playerScript);
121
+
122
+ // 2. Add images to images/ folder and build path mapping
123
+ const imagePathMap: Record<string, string> = {};
124
+ if (images) {
125
+ for (const [originalPath, buffer] of images.entries()) {
126
+ const filename = extractFilename(originalPath);
127
+ const zipPath = `images/${filename}`;
128
+ zip.file(zipPath, buffer);
129
+ imagePathMap[originalPath] = zipPath;
130
+ }
131
+ }
132
+
133
+ // 3. Add audio to audio/ folder and build path mapping
134
+ const audioPathMap: Record<string, string> = {};
135
+ if (audio) {
136
+ for (const [segmentKey, buffer] of audio.entries()) {
137
+ const filename = extractFilename(segmentKey);
138
+ // Ensure .mp3 extension
139
+ const finalName = filename.includes('.') ? filename : `${filename}.mp3`;
140
+ const zipPath = `audio/${finalName}`;
141
+ zip.file(zipPath, buffer);
142
+ audioPathMap[segmentKey] = zipPath;
143
+ }
144
+ }
145
+
146
+ // 4. Generate HTML that references external files
147
+ const html = generateExternalHtml(doc, {
148
+ playerScriptPath: 'squisq-player.js',
149
+ imagePathMap: Object.keys(imagePathMap).length > 0 ? imagePathMap : undefined,
150
+ audioPathMap: Object.keys(audioPathMap).length > 0 ? audioPathMap : undefined,
151
+ mode,
152
+ title,
153
+ autoPlay,
154
+ });
155
+ zip.file('index.html', html);
156
+
157
+ // 5. Generate ZIP blob
158
+ return zip.generateAsync({
159
+ type: 'blob',
160
+ compression: 'DEFLATE',
161
+ compressionOptions: { level: 6 },
162
+ });
163
+ }
164
+
165
+ // ── Re-exports ─────────────────────────────────────────────────────
166
+
167
+ export { collectImagePaths } from './htmlTemplate.js';
168
+ export { inferMimeType, arrayBufferToBase64DataUrl, extractFilename } from './imageUtils.js';
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @bendyline/squisq-formats
3
+ *
4
+ * Format converters for squisq documents. Converts between squisq's
5
+ * MarkdownDocument / Doc and various file formats via Office Open XML.
6
+ *
7
+ * Supported formats:
8
+ * - **DOCX** — Microsoft Word (import + export) ✅
9
+ * - **PDF** — Portable Document Format (import + export) ✅
10
+ * - **PPTX** — Microsoft PowerPoint (planned)
11
+ * - **XLSX** — Microsoft Excel (planned)
12
+ *
13
+ * All converters run in the browser — no server or native binaries required.
14
+ * The shared `ooxml/` subpath export provides reusable OOXML infrastructure.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Import from root
19
+ * import { markdownDocToDocx, docxToMarkdownDoc } from '@bendyline/squisq-formats';
20
+ *
21
+ * // Or import from subpath
22
+ * import { markdownDocToDocx } from '@bendyline/squisq-formats/docx';
23
+ * import { createPackage } from '@bendyline/squisq-formats/ooxml';
24
+ * ```
25
+ */
26
+
27
+ // DOCX (fully implemented)
28
+ export { markdownDocToDocx, docToDocx, docxToMarkdownDoc, docxToDoc } from './docx/index.js';
29
+ export type { DocxExportOptions, DocxImportOptions } from './docx/index.js';
30
+
31
+ // PPTX (stub)
32
+ export { markdownDocToPptx, docToPptx, pptxToMarkdownDoc, pptxToDoc } from './pptx/index.js';
33
+ export type { PptxExportOptions, PptxImportOptions } from './pptx/index.js';
34
+
35
+ // XLSX (stub)
36
+ export { markdownDocToXlsx, docToXlsx, xlsxToMarkdownDoc, xlsxToDoc } from './xlsx/index.js';
37
+ export type { XlsxExportOptions, XlsxImportOptions } from './xlsx/index.js';
38
+
39
+ // PDF (fully implemented)
40
+ export {
41
+ markdownDocToPdf,
42
+ docToPdf,
43
+ pdfToMarkdownDoc,
44
+ pdfToDoc,
45
+ configurePdfWorker,
46
+ } from './pdf/index.js';
47
+ export type { PdfExportOptions, PdfImportOptions } from './pdf/index.js';
48
+
49
+ // HTML (fully implemented)
50
+ export { docToHtml, docToHtmlZip, collectImagePaths } from './html/index.js';
51
+ export type { HtmlExportOptions, HtmlZipExportOptions } from './html/index.js';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @bendyline/squisq-formats OOXML Module
3
+ *
4
+ * Shared infrastructure for reading and writing Office Open XML packages
5
+ * (DOCX, PPTX, XLSX). Provides ZIP archive handling, relationship management,
6
+ * content type assembly, XML utilities, and core property parsing.
7
+ *
8
+ * This module is consumed by the format-specific modules (docx/, pptx/, xlsx/)
9
+ * and can also be imported directly by advanced consumers.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { createPackage, openPackage } from '@bendyline/squisq-formats/ooxml';
14
+ * ```
15
+ */
16
+
17
+ // Types
18
+ export type {
19
+ OoxmlPackage,
20
+ ContentTypeMap,
21
+ Relationship,
22
+ CoreProperties,
23
+ PackagePart,
24
+ PendingRelationship,
25
+ } from './types.js';
26
+
27
+ // Reader
28
+ export {
29
+ openPackage,
30
+ getPartRelationships,
31
+ getPartXml,
32
+ getPartBinary,
33
+ getCoreProperties,
34
+ } from './reader.js';
35
+
36
+ // Writer
37
+ export type { OoxmlPackageBuilder } from './writer.js';
38
+ export { createPackage } from './writer.js';
39
+
40
+ // XML Utilities
41
+ export {
42
+ xmlDeclaration,
43
+ escapeXml,
44
+ attrString,
45
+ selfClosingElement,
46
+ xmlElement,
47
+ textElement,
48
+ } from './xmlUtils.js';
49
+
50
+ // Namespace constants
51
+ export {
52
+ NS_RELATIONSHIPS,
53
+ NS_CONTENT_TYPES,
54
+ REL_OFFICE_DOCUMENT,
55
+ REL_CORE_PROPERTIES,
56
+ REL_EXTENDED_PROPERTIES,
57
+ REL_STYLES,
58
+ REL_NUMBERING,
59
+ REL_FONT_TABLE,
60
+ REL_SETTINGS,
61
+ REL_HYPERLINK,
62
+ REL_IMAGE,
63
+ REL_FOOTNOTES,
64
+ REL_THEME,
65
+ NS_WML,
66
+ NS_PML,
67
+ NS_SML,
68
+ NS_DRAWINGML,
69
+ NS_WP_DRAWING,
70
+ NS_PICTURE,
71
+ NS_DC,
72
+ NS_DCTERMS,
73
+ NS_CORE_PROPERTIES,
74
+ NS_XSI,
75
+ NS_MC,
76
+ NS_R,
77
+ CONTENT_TYPE_RELATIONSHIPS,
78
+ CONTENT_TYPE_CORE_PROPERTIES,
79
+ CONTENT_TYPE_DOCX_DOCUMENT,
80
+ CONTENT_TYPE_DOCX_STYLES,
81
+ CONTENT_TYPE_DOCX_NUMBERING,
82
+ CONTENT_TYPE_DOCX_SETTINGS,
83
+ CONTENT_TYPE_DOCX_FONT_TABLE,
84
+ CONTENT_TYPE_DOCX_FOOTNOTES,
85
+ CONTENT_TYPE_PPTX_PRESENTATION,
86
+ CONTENT_TYPE_XLSX_WORKBOOK,
87
+ } from './namespaces.js';
@@ -0,0 +1,160 @@
1
+ /**
2
+ * OOXML Namespace Constants
3
+ *
4
+ * All Office Open XML namespace URIs used across DOCX, PPTX, and XLSX.
5
+ * Organized by category for easy reference.
6
+ */
7
+
8
+ // ============================================
9
+ // Package-level namespaces (shared by all formats)
10
+ // ============================================
11
+
12
+ /** Relationships namespace (used in _rels/*.rels files) */
13
+ export const NS_RELATIONSHIPS = 'http://schemas.openxmlformats.org/package/2006/relationships';
14
+
15
+ /** Content Types namespace ([Content_Types].xml) */
16
+ export const NS_CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
17
+
18
+ // ============================================
19
+ // Relationship type URIs
20
+ // ============================================
21
+
22
+ /** Relationship type: Office document (main part) */
23
+ export const REL_OFFICE_DOCUMENT =
24
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
25
+
26
+ /** Relationship type: Core properties */
27
+ export const REL_CORE_PROPERTIES =
28
+ 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties';
29
+
30
+ /** Relationship type: Extended properties (app.xml) */
31
+ export const REL_EXTENDED_PROPERTIES =
32
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties';
33
+
34
+ /** Relationship type: Styles */
35
+ export const REL_STYLES =
36
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
37
+
38
+ /** Relationship type: Numbering */
39
+ export const REL_NUMBERING =
40
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering';
41
+
42
+ /** Relationship type: Font table */
43
+ export const REL_FONT_TABLE =
44
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable';
45
+
46
+ /** Relationship type: Settings */
47
+ export const REL_SETTINGS =
48
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings';
49
+
50
+ /** Relationship type: Hyperlink */
51
+ export const REL_HYPERLINK =
52
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink';
53
+
54
+ /** Relationship type: Image */
55
+ export const REL_IMAGE =
56
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image';
57
+
58
+ /** Relationship type: Footnotes */
59
+ export const REL_FOOTNOTES =
60
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes';
61
+
62
+ /** Relationship type: Theme */
63
+ export const REL_THEME =
64
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme';
65
+
66
+ // ============================================
67
+ // WordprocessingML (DOCX)
68
+ // ============================================
69
+
70
+ /** WordprocessingML main namespace (w:) */
71
+ export const NS_WML = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
72
+
73
+ // ============================================
74
+ // PresentationML (PPTX)
75
+ // ============================================
76
+
77
+ /** PresentationML main namespace (p:) */
78
+ export const NS_PML = 'http://schemas.openxmlformats.org/presentationml/2006/main';
79
+
80
+ // ============================================
81
+ // SpreadsheetML (XLSX)
82
+ // ============================================
83
+
84
+ /** SpreadsheetML main namespace */
85
+ export const NS_SML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
86
+
87
+ // ============================================
88
+ // DrawingML (shared across formats)
89
+ // ============================================
90
+
91
+ /** DrawingML main namespace (a:) */
92
+ export const NS_DRAWINGML = 'http://schemas.openxmlformats.org/drawingml/2006/main';
93
+
94
+ /** DrawingML WordprocessingML drawing namespace (wp:) */
95
+ export const NS_WP_DRAWING =
96
+ 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing';
97
+
98
+ /** DrawingML picture namespace (pic:) */
99
+ export const NS_PICTURE = 'http://schemas.openxmlformats.org/drawingml/2006/picture';
100
+
101
+ // ============================================
102
+ // Dublin Core / Core Properties
103
+ // ============================================
104
+
105
+ /** Dublin Core elements namespace */
106
+ export const NS_DC = 'http://purl.org/dc/elements/1.1/';
107
+
108
+ /** Dublin Core terms namespace */
109
+ export const NS_DCTERMS = 'http://purl.org/dc/terms/';
110
+
111
+ /** Core properties namespace */
112
+ export const NS_CORE_PROPERTIES =
113
+ 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties';
114
+
115
+ /** XML Schema Instance namespace */
116
+ export const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
117
+
118
+ // ============================================
119
+ // Markup Compatibility
120
+ // ============================================
121
+
122
+ /** Markup Compatibility namespace (mc:) */
123
+ export const NS_MC = 'http://schemas.openxmlformats.org/markup-compatibility/2006';
124
+
125
+ /** Office relationships namespace (r:) */
126
+ export const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
127
+
128
+ // ============================================
129
+ // Content type strings
130
+ // ============================================
131
+
132
+ export const CONTENT_TYPE_RELATIONSHIPS =
133
+ 'application/vnd.openxmlformats-package.relationships+xml';
134
+
135
+ export const CONTENT_TYPE_CORE_PROPERTIES =
136
+ 'application/vnd.openxmlformats-package.core-properties+xml';
137
+
138
+ export const CONTENT_TYPE_DOCX_DOCUMENT =
139
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml';
140
+
141
+ export const CONTENT_TYPE_DOCX_STYLES =
142
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml';
143
+
144
+ export const CONTENT_TYPE_DOCX_NUMBERING =
145
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml';
146
+
147
+ export const CONTENT_TYPE_DOCX_SETTINGS =
148
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml';
149
+
150
+ export const CONTENT_TYPE_DOCX_FONT_TABLE =
151
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml';
152
+
153
+ export const CONTENT_TYPE_DOCX_FOOTNOTES =
154
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml';
155
+
156
+ export const CONTENT_TYPE_PPTX_PRESENTATION =
157
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml';
158
+
159
+ export const CONTENT_TYPE_XLSX_WORKBOOK =
160
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml';
@@ -0,0 +1,218 @@
1
+ /**
2
+ * OOXML Package Reader
3
+ *
4
+ * Opens OOXML archives (.docx, .pptx, .xlsx) and parses their
5
+ * structural metadata: [Content_Types].xml, relationships, and
6
+ * core properties.
7
+ *
8
+ * Uses JSZip to unzip and the browser's DOMParser to parse XML.
9
+ */
10
+
11
+ import JSZip from 'jszip';
12
+ import type { OoxmlPackage, ContentTypeMap, Relationship, CoreProperties } from './types.js';
13
+ import { NS_RELATIONSHIPS, NS_DC, NS_DCTERMS, NS_CORE_PROPERTIES } from './namespaces.js';
14
+
15
+ // ============================================
16
+ // Package Opening
17
+ // ============================================
18
+
19
+ /**
20
+ * Open an OOXML package from raw data.
21
+ *
22
+ * Parses the ZIP archive, [Content_Types].xml, and root relationships.
23
+ *
24
+ * @param data - The raw .docx/.pptx/.xlsx file as ArrayBuffer or Blob
25
+ * @returns A parsed OoxmlPackage
26
+ */
27
+ export async function openPackage(data: ArrayBuffer | Blob): Promise<OoxmlPackage> {
28
+ const zip = await JSZip.loadAsync(data);
29
+ const contentTypes = await parseContentTypes(zip);
30
+ const rootRelationships = await parseRelationships(zip, '');
31
+
32
+ return { zip, contentTypes, rootRelationships };
33
+ }
34
+
35
+ // ============================================
36
+ // Content Types
37
+ // ============================================
38
+
39
+ /**
40
+ * Parse [Content_Types].xml from the archive.
41
+ */
42
+ async function parseContentTypes(zip: JSZip): Promise<ContentTypeMap> {
43
+ const overrides = new Map<string, string>();
44
+ const defaults = new Map<string, string>();
45
+
46
+ const file = zip.file('[Content_Types].xml');
47
+ if (!file) return { overrides, defaults };
48
+
49
+ const text = await file.async('text');
50
+ const doc = new DOMParser().parseFromString(text, 'application/xml');
51
+
52
+ // Parse <Default Extension="rels" ContentType="..." />
53
+ const defaultEls = doc.getElementsByTagName('Default');
54
+ for (let i = 0; i < defaultEls.length; i++) {
55
+ const el = defaultEls[i];
56
+ const ext = el.getAttribute('Extension');
57
+ const ct = el.getAttribute('ContentType');
58
+ if (ext && ct) defaults.set(ext, ct);
59
+ }
60
+
61
+ // Parse <Override PartName="/word/document.xml" ContentType="..." />
62
+ const overrideEls = doc.getElementsByTagName('Override');
63
+ for (let i = 0; i < overrideEls.length; i++) {
64
+ const el = overrideEls[i];
65
+ const partName = el.getAttribute('PartName');
66
+ const ct = el.getAttribute('ContentType');
67
+ if (partName && ct) {
68
+ // Normalize: strip leading slash
69
+ overrides.set(partName.replace(/^\//, ''), ct);
70
+ }
71
+ }
72
+
73
+ return { overrides, defaults };
74
+ }
75
+
76
+ // ============================================
77
+ // Relationships
78
+ // ============================================
79
+
80
+ /**
81
+ * Parse relationships for a specific part.
82
+ *
83
+ * @param pkg - The OOXML package (or the zip directly)
84
+ * @param partPath - The part path (e.g., "word/document.xml").
85
+ * Use "" for root-level relationships (_rels/.rels).
86
+ * @returns Array of relationship entries
87
+ */
88
+ export async function getPartRelationships(
89
+ pkg: OoxmlPackage,
90
+ partPath: string,
91
+ ): Promise<Relationship[]> {
92
+ return parseRelationships(pkg.zip, partPath);
93
+ }
94
+
95
+ /**
96
+ * Parse a _rels/*.rels file from the ZIP.
97
+ *
98
+ * For root relationships, relsPath = "_rels/.rels".
99
+ * For part relationships, relsPath = "<dir>/_rels/<filename>.rels".
100
+ */
101
+ async function parseRelationships(zip: JSZip, partPath: string): Promise<Relationship[]> {
102
+ const relsPath = partPath === '' ? '_rels/.rels' : buildRelsPath(partPath);
103
+
104
+ const file = zip.file(relsPath);
105
+ if (!file) return [];
106
+
107
+ const text = await file.async('text');
108
+ const doc = new DOMParser().parseFromString(text, 'application/xml');
109
+ const result: Relationship[] = [];
110
+
111
+ const els = doc.getElementsByTagNameNS(NS_RELATIONSHIPS, 'Relationship');
112
+ // Fallback in case namespace isn't used (some generators omit it)
113
+ const fallbackEls = els.length > 0 ? els : doc.getElementsByTagName('Relationship');
114
+
115
+ for (let i = 0; i < fallbackEls.length; i++) {
116
+ const el = fallbackEls[i];
117
+ const id = el.getAttribute('Id');
118
+ const type = el.getAttribute('Type');
119
+ const target = el.getAttribute('Target');
120
+ if (id && type && target) {
121
+ const targetMode = el.getAttribute('TargetMode') as 'Internal' | 'External' | null;
122
+ result.push({
123
+ id,
124
+ type,
125
+ target,
126
+ ...(targetMode ? { targetMode } : {}),
127
+ });
128
+ }
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * Build the _rels path for a given part path.
136
+ *
137
+ * "word/document.xml" → "word/_rels/document.xml.rels"
138
+ * "xl/workbook.xml" → "xl/_rels/workbook.xml.rels"
139
+ */
140
+ function buildRelsPath(partPath: string): string {
141
+ const lastSlash = partPath.lastIndexOf('/');
142
+ if (lastSlash === -1) {
143
+ return `_rels/${partPath}.rels`;
144
+ }
145
+ const dir = partPath.substring(0, lastSlash);
146
+ const file = partPath.substring(lastSlash + 1);
147
+ return `${dir}/_rels/${file}.rels`;
148
+ }
149
+
150
+ // ============================================
151
+ // Part Access
152
+ // ============================================
153
+
154
+ /**
155
+ * Extract an XML part from the package and parse it as a DOM Document.
156
+ *
157
+ * @param pkg - The OOXML package
158
+ * @param partPath - Path within the archive (e.g., "word/document.xml")
159
+ * @returns Parsed XML Document, or null if the part doesn't exist
160
+ */
161
+ export async function getPartXml(pkg: OoxmlPackage, partPath: string): Promise<Document | null> {
162
+ const file = pkg.zip.file(partPath);
163
+ if (!file) return null;
164
+
165
+ const text = await file.async('text');
166
+ return new DOMParser().parseFromString(text, 'application/xml');
167
+ }
168
+
169
+ /**
170
+ * Extract a binary part from the package (e.g., an image from word/media/).
171
+ *
172
+ * @param pkg - The OOXML package
173
+ * @param partPath - Path within the archive
174
+ * @returns The binary content, or null if the part doesn't exist
175
+ */
176
+ export async function getPartBinary(
177
+ pkg: OoxmlPackage,
178
+ partPath: string,
179
+ ): Promise<ArrayBuffer | null> {
180
+ const file = pkg.zip.file(partPath);
181
+ if (!file) return null;
182
+ return file.async('arraybuffer');
183
+ }
184
+
185
+ // ============================================
186
+ // Core Properties
187
+ // ============================================
188
+
189
+ /**
190
+ * Parse core document properties from docProps/core.xml.
191
+ *
192
+ * @param pkg - The OOXML package
193
+ * @returns Parsed core properties (all fields optional)
194
+ */
195
+ export async function getCoreProperties(pkg: OoxmlPackage): Promise<CoreProperties> {
196
+ const doc = await getPartXml(pkg, 'docProps/core.xml');
197
+ if (!doc) return {};
198
+
199
+ function getText(ns: string, localName: string): string | undefined {
200
+ const els = doc!.getElementsByTagNameNS(ns, localName);
201
+ if (els.length > 0 && els[0].textContent) {
202
+ return els[0].textContent;
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ return {
208
+ title: getText(NS_DC, 'title'),
209
+ subject: getText(NS_DC, 'subject'),
210
+ creator: getText(NS_DC, 'creator'),
211
+ description: getText(NS_DC, 'description'),
212
+ keywords: getText(NS_CORE_PROPERTIES, 'keywords'),
213
+ lastModifiedBy: getText(NS_CORE_PROPERTIES, 'lastModifiedBy'),
214
+ revision: getText(NS_CORE_PROPERTIES, 'revision'),
215
+ created: getText(NS_DCTERMS, 'created'),
216
+ modified: getText(NS_DCTERMS, 'modified'),
217
+ };
218
+ }