@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,104 @@
1
+ /**
2
+ * OOXML Types
3
+ *
4
+ * Shared type definitions for all Office Open XML formats (DOCX, PPTX, XLSX).
5
+ * These model the common structural elements of the OOXML package format:
6
+ * ZIP archive, relationships, content types, and core properties.
7
+ */
8
+
9
+ import type JSZip from 'jszip';
10
+
11
+ // ============================================
12
+ // Package
13
+ // ============================================
14
+
15
+ /**
16
+ * An opened OOXML package — wraps the JSZip archive plus parsed
17
+ * structural metadata (content types, root relationships).
18
+ */
19
+ export interface OoxmlPackage {
20
+ /** The underlying JSZip archive */
21
+ zip: JSZip;
22
+ /** Parsed [Content_Types].xml entries */
23
+ contentTypes: ContentTypeMap;
24
+ /** Root-level relationships (_rels/.rels) */
25
+ rootRelationships: Relationship[];
26
+ }
27
+
28
+ /**
29
+ * Content type map built from [Content_Types].xml.
30
+ * Maps part paths → content type strings plus extension defaults.
31
+ */
32
+ export interface ContentTypeMap {
33
+ /** Explicit overrides: partName → contentType */
34
+ overrides: Map<string, string>;
35
+ /** Default extensions: extension → contentType */
36
+ defaults: Map<string, string>;
37
+ }
38
+
39
+ // ============================================
40
+ // Relationships
41
+ // ============================================
42
+
43
+ /**
44
+ * An OOXML relationship entry (from any _rels/*.rels file).
45
+ */
46
+ export interface Relationship {
47
+ /** Relationship ID (e.g., "rId1") */
48
+ id: string;
49
+ /** Relationship type URI (e.g., "http://...officedocument/...") */
50
+ type: string;
51
+ /** Target path or URL */
52
+ target: string;
53
+ /** "Internal" (default) or "External" for hyperlinks */
54
+ targetMode?: 'Internal' | 'External';
55
+ }
56
+
57
+ // ============================================
58
+ // Core Properties
59
+ // ============================================
60
+
61
+ /**
62
+ * Document core properties from docProps/core.xml (Dublin Core metadata).
63
+ */
64
+ export interface CoreProperties {
65
+ title?: string;
66
+ subject?: string;
67
+ creator?: string;
68
+ keywords?: string;
69
+ description?: string;
70
+ lastModifiedBy?: string;
71
+ revision?: string;
72
+ created?: string;
73
+ modified?: string;
74
+ }
75
+
76
+ // ============================================
77
+ // Package Builder
78
+ // ============================================
79
+
80
+ /**
81
+ * A part (file) that will be written into an OOXML package.
82
+ */
83
+ export interface PackagePart {
84
+ /** Path within the ZIP (e.g., "word/document.xml") */
85
+ path: string;
86
+ /** XML or text content (mutually exclusive with binaryContent) */
87
+ content?: string;
88
+ /** Binary content (mutually exclusive with content) */
89
+ binaryContent?: ArrayBuffer | Uint8Array;
90
+ /** MIME content type for [Content_Types].xml */
91
+ contentType: string;
92
+ }
93
+
94
+ /**
95
+ * A relationship to be written into a _rels/*.rels file.
96
+ * `sourcePart` identifies which part the relationship belongs to.
97
+ * Use "" (empty string) for root-level relationships (_rels/.rels).
98
+ */
99
+ export interface PendingRelationship {
100
+ /** The part this relationship belongs to ("" for root) */
101
+ sourcePart: string;
102
+ /** The relationship entry */
103
+ relationship: Relationship;
104
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * OOXML Package Writer
3
+ *
4
+ * Builds OOXML archives (.docx, .pptx, .xlsx) from parts.
5
+ * Handles automatic generation of [Content_Types].xml and
6
+ * _rels/*.rels files from the registered parts and relationships.
7
+ */
8
+
9
+ import JSZip from 'jszip';
10
+ import type { CoreProperties, PackagePart, PendingRelationship, Relationship } from './types.js';
11
+ import {
12
+ NS_CONTENT_TYPES,
13
+ NS_RELATIONSHIPS,
14
+ NS_CORE_PROPERTIES,
15
+ NS_DC,
16
+ NS_DCTERMS,
17
+ NS_XSI,
18
+ CONTENT_TYPE_RELATIONSHIPS,
19
+ CONTENT_TYPE_CORE_PROPERTIES,
20
+ REL_CORE_PROPERTIES,
21
+ } from './namespaces.js';
22
+ import { xmlDeclaration, escapeXml } from './xmlUtils.js';
23
+
24
+ // ============================================
25
+ // Package Builder
26
+ // ============================================
27
+
28
+ /**
29
+ * Mutable builder for constructing an OOXML package.
30
+ *
31
+ * Add parts, relationships, and core properties, then call `toBlob()`
32
+ * or `toArrayBuffer()` to produce the final ZIP archive.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const builder = createPackage();
37
+ * builder.addPart('word/document.xml', documentXml, CONTENT_TYPE_DOCX_DOCUMENT);
38
+ * builder.addRelationship('', {
39
+ * id: 'rId1',
40
+ * type: REL_OFFICE_DOCUMENT,
41
+ * target: 'word/document.xml',
42
+ * });
43
+ * const blob = await builder.toBlob();
44
+ * ```
45
+ */
46
+ export interface OoxmlPackageBuilder {
47
+ /**
48
+ * Add an XML or text part to the package.
49
+ *
50
+ * @param path - Path within the archive (e.g., "word/document.xml")
51
+ * @param content - XML string content
52
+ * @param contentType - MIME content type for [Content_Types].xml
53
+ */
54
+ addPart(path: string, content: string, contentType: string): void;
55
+
56
+ /**
57
+ * Add a binary part to the package (e.g., an image).
58
+ *
59
+ * @param path - Path within the archive (e.g., "word/media/image1.png")
60
+ * @param data - Binary content
61
+ * @param contentType - MIME content type (e.g., "image/png")
62
+ */
63
+ addBinaryPart(path: string, data: ArrayBuffer | Uint8Array, contentType: string): void;
64
+
65
+ /**
66
+ * Register a relationship.
67
+ *
68
+ * @param sourcePart - The part this relationship belongs to (e.g., "word/document.xml").
69
+ * Use "" (empty string) for root-level relationships (_rels/.rels).
70
+ * @param rel - The relationship entry
71
+ */
72
+ addRelationship(sourcePart: string, rel: Relationship): void;
73
+
74
+ /**
75
+ * Set core document properties (docProps/core.xml).
76
+ * Calling this multiple times overwrites previous values.
77
+ */
78
+ setCoreProperties(props: CoreProperties): void;
79
+
80
+ /**
81
+ * Assemble the final OOXML package as a Blob.
82
+ * Generates [Content_Types].xml and all _rels/*.rels files automatically.
83
+ */
84
+ toBlob(): Promise<Blob>;
85
+
86
+ /**
87
+ * Assemble the final OOXML package as an ArrayBuffer.
88
+ */
89
+ toArrayBuffer(): Promise<ArrayBuffer>;
90
+ }
91
+
92
+ /**
93
+ * Create a new OOXML package builder.
94
+ */
95
+ export function createPackage(): OoxmlPackageBuilder {
96
+ const parts: PackagePart[] = [];
97
+ const relationships: PendingRelationship[] = [];
98
+ let coreProps: CoreProperties | undefined;
99
+
100
+ return {
101
+ addPart(path, content, contentType) {
102
+ parts.push({ path, content, contentType });
103
+ },
104
+
105
+ addBinaryPart(path, data, contentType) {
106
+ parts.push({ path, binaryContent: data, contentType });
107
+ },
108
+
109
+ addRelationship(sourcePart, rel) {
110
+ relationships.push({ sourcePart, relationship: rel });
111
+ },
112
+
113
+ setCoreProperties(props) {
114
+ coreProps = props;
115
+ },
116
+
117
+ async toBlob() {
118
+ const zip = assemble(parts, relationships, coreProps);
119
+ return zip.generateAsync({ type: 'blob' });
120
+ },
121
+
122
+ async toArrayBuffer() {
123
+ const zip = assemble(parts, relationships, coreProps);
124
+ return zip.generateAsync({ type: 'arraybuffer' });
125
+ },
126
+ };
127
+ }
128
+
129
+ // ============================================
130
+ // Assembly
131
+ // ============================================
132
+
133
+ /**
134
+ * Assemble a JSZip archive from parts, relationships, and core properties.
135
+ */
136
+ function assemble(
137
+ parts: PackagePart[],
138
+ relationships: PendingRelationship[],
139
+ coreProps?: CoreProperties,
140
+ ): JSZip {
141
+ const zip = new JSZip();
142
+
143
+ // Write content parts
144
+ for (const part of parts) {
145
+ if (part.content !== undefined) {
146
+ zip.file(part.path, part.content);
147
+ } else if (part.binaryContent !== undefined) {
148
+ zip.file(part.path, part.binaryContent);
149
+ }
150
+ }
151
+
152
+ // Add core properties if set
153
+ if (coreProps) {
154
+ const coreXml = buildCorePropertiesXml(coreProps);
155
+ zip.file('docProps/core.xml', coreXml);
156
+
157
+ // Add relationship to core properties
158
+ relationships.push({
159
+ sourcePart: '',
160
+ relationship: {
161
+ id: `rId${relationships.length + 100}`,
162
+ type: REL_CORE_PROPERTIES,
163
+ target: 'docProps/core.xml',
164
+ },
165
+ });
166
+ }
167
+
168
+ // Build and write [Content_Types].xml
169
+ zip.file('[Content_Types].xml', buildContentTypesXml(parts, coreProps));
170
+
171
+ // Build and write _rels/*.rels files
172
+ const relsBySource = groupRelationshipsBySource(relationships);
173
+ for (const [sourcePart, rels] of relsBySource) {
174
+ const relsPath = sourcePart === '' ? '_rels/.rels' : buildRelsPath(sourcePart);
175
+ zip.file(relsPath, buildRelationshipsXml(rels));
176
+ }
177
+
178
+ return zip;
179
+ }
180
+
181
+ // ============================================
182
+ // [Content_Types].xml
183
+ // ============================================
184
+
185
+ function buildContentTypesXml(parts: PackagePart[], coreProps?: CoreProperties): string {
186
+ const lines: string[] = [xmlDeclaration()];
187
+ lines.push(`<Types xmlns="${NS_CONTENT_TYPES}">`);
188
+
189
+ // Default extension types
190
+ const extensionTypes = new Map<string, string>();
191
+ extensionTypes.set('rels', CONTENT_TYPE_RELATIONSHIPS);
192
+ extensionTypes.set('xml', 'application/xml');
193
+
194
+ // Collect extensions from binary parts (e.g., png, jpeg)
195
+ for (const part of parts) {
196
+ if (part.binaryContent !== undefined) {
197
+ const ext = getExtension(part.path);
198
+ if (ext && !extensionTypes.has(ext)) {
199
+ extensionTypes.set(ext, part.contentType);
200
+ }
201
+ }
202
+ }
203
+
204
+ for (const [ext, ct] of extensionTypes) {
205
+ lines.push(` <Default Extension="${escapeXml(ext)}" ContentType="${escapeXml(ct)}"/>`);
206
+ }
207
+
208
+ // Override entries for each XML part
209
+ for (const part of parts) {
210
+ if (part.content !== undefined) {
211
+ lines.push(
212
+ ` <Override PartName="/${escapeXml(part.path)}" ContentType="${escapeXml(part.contentType)}"/>`,
213
+ );
214
+ }
215
+ }
216
+
217
+ // Core properties override
218
+ if (coreProps) {
219
+ lines.push(
220
+ ` <Override PartName="/docProps/core.xml" ContentType="${CONTENT_TYPE_CORE_PROPERTIES}"/>`,
221
+ );
222
+ }
223
+
224
+ lines.push('</Types>');
225
+ return lines.join('\n');
226
+ }
227
+
228
+ // ============================================
229
+ // Relationships XML
230
+ // ============================================
231
+
232
+ function buildRelationshipsXml(rels: Relationship[]): string {
233
+ const lines: string[] = [xmlDeclaration()];
234
+ lines.push(`<Relationships xmlns="${NS_RELATIONSHIPS}">`);
235
+
236
+ for (const rel of rels) {
237
+ const attrs: string[] = [
238
+ `Id="${escapeXml(rel.id)}"`,
239
+ `Type="${escapeXml(rel.type)}"`,
240
+ `Target="${escapeXml(rel.target)}"`,
241
+ ];
242
+ if (rel.targetMode) {
243
+ attrs.push(`TargetMode="${escapeXml(rel.targetMode)}"`);
244
+ }
245
+ lines.push(` <Relationship ${attrs.join(' ')}/>`);
246
+ }
247
+
248
+ lines.push('</Relationships>');
249
+ return lines.join('\n');
250
+ }
251
+
252
+ // ============================================
253
+ // Core Properties XML
254
+ // ============================================
255
+
256
+ function buildCorePropertiesXml(props: CoreProperties): string {
257
+ const lines: string[] = [xmlDeclaration()];
258
+ lines.push(
259
+ `<cp:coreProperties` +
260
+ ` xmlns:cp="${NS_CORE_PROPERTIES}"` +
261
+ ` xmlns:dc="${NS_DC}"` +
262
+ ` xmlns:dcterms="${NS_DCTERMS}"` +
263
+ ` xmlns:xsi="${NS_XSI}">`,
264
+ );
265
+
266
+ if (props.title) lines.push(` <dc:title>${escapeXml(props.title)}</dc:title>`);
267
+ if (props.subject) lines.push(` <dc:subject>${escapeXml(props.subject)}</dc:subject>`);
268
+ if (props.creator) lines.push(` <dc:creator>${escapeXml(props.creator)}</dc:creator>`);
269
+ if (props.description)
270
+ lines.push(` <dc:description>${escapeXml(props.description)}</dc:description>`);
271
+ if (props.keywords) lines.push(` <cp:keywords>${escapeXml(props.keywords)}</cp:keywords>`);
272
+ if (props.lastModifiedBy)
273
+ lines.push(` <cp:lastModifiedBy>${escapeXml(props.lastModifiedBy)}</cp:lastModifiedBy>`);
274
+ if (props.revision) lines.push(` <cp:revision>${escapeXml(props.revision)}</cp:revision>`);
275
+ if (props.created) {
276
+ lines.push(
277
+ ` <dcterms:created xsi:type="dcterms:W3CDTF">${escapeXml(props.created)}</dcterms:created>`,
278
+ );
279
+ }
280
+ if (props.modified) {
281
+ lines.push(
282
+ ` <dcterms:modified xsi:type="dcterms:W3CDTF">${escapeXml(props.modified)}</dcterms:modified>`,
283
+ );
284
+ }
285
+
286
+ lines.push('</cp:coreProperties>');
287
+ return lines.join('\n');
288
+ }
289
+
290
+ // ============================================
291
+ // Helpers
292
+ // ============================================
293
+
294
+ function getExtension(path: string): string | undefined {
295
+ const dot = path.lastIndexOf('.');
296
+ if (dot === -1) return undefined;
297
+ return path.substring(dot + 1).toLowerCase();
298
+ }
299
+
300
+ function buildRelsPath(partPath: string): string {
301
+ const lastSlash = partPath.lastIndexOf('/');
302
+ if (lastSlash === -1) {
303
+ return `_rels/${partPath}.rels`;
304
+ }
305
+ const dir = partPath.substring(0, lastSlash);
306
+ const file = partPath.substring(lastSlash + 1);
307
+ return `${dir}/_rels/${file}.rels`;
308
+ }
309
+
310
+ function groupRelationshipsBySource(pending: PendingRelationship[]): Map<string, Relationship[]> {
311
+ const grouped = new Map<string, Relationship[]>();
312
+ for (const { sourcePart, relationship } of pending) {
313
+ let list = grouped.get(sourcePart);
314
+ if (!list) {
315
+ list = [];
316
+ grouped.set(sourcePart, list);
317
+ }
318
+ list.push(relationship);
319
+ }
320
+ return grouped;
321
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * XML Utility Functions
3
+ *
4
+ * Lightweight helpers for generating well-formed XML strings.
5
+ * Used by all OOXML writers (DOCX, PPTX, XLSX) to construct
6
+ * part content without a heavy DOM library.
7
+ */
8
+
9
+ /**
10
+ * XML declaration for the start of every OOXML part.
11
+ */
12
+ export function xmlDeclaration(): string {
13
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
14
+ }
15
+
16
+ /**
17
+ * Escape a string for safe inclusion in XML text content or attribute values.
18
+ *
19
+ * Handles the five predefined XML entities:
20
+ * - & → &amp;
21
+ * - < → &lt;
22
+ * - > → &gt;
23
+ * - " → &quot;
24
+ * - ' → &apos;
25
+ */
26
+ export function escapeXml(text: string): string {
27
+ return text
28
+ .replace(/&/g, '&amp;')
29
+ .replace(/</g, '&lt;')
30
+ .replace(/>/g, '&gt;')
31
+ .replace(/"/g, '&quot;')
32
+ .replace(/'/g, '&apos;');
33
+ }
34
+
35
+ /**
36
+ * Build an XML attribute string from a key-value record.
37
+ * Only includes entries where the value is defined and non-empty.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * attrString({ 'w:val': 'Heading1', 'w:eastAsia': undefined })
42
+ * // ' w:val="Heading1"'
43
+ * ```
44
+ */
45
+ export function attrString(attrs?: Record<string, string | undefined>): string {
46
+ if (!attrs) return '';
47
+ const parts: string[] = [];
48
+ for (const [key, value] of Object.entries(attrs)) {
49
+ if (value !== undefined && value !== '') {
50
+ parts.push(` ${key}="${escapeXml(value)}"`);
51
+ }
52
+ }
53
+ return parts.join('');
54
+ }
55
+
56
+ /**
57
+ * Build a self-closing XML element: `<tag attr="val"/>`.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * selfClosingElement('w:b')
62
+ * // '<w:b/>'
63
+ *
64
+ * selfClosingElement('w:pStyle', { 'w:val': 'Heading1' })
65
+ * // '<w:pStyle w:val="Heading1"/>'
66
+ * ```
67
+ */
68
+ export function selfClosingElement(
69
+ tag: string,
70
+ attrs?: Record<string, string | undefined>,
71
+ ): string {
72
+ return `<${tag}${attrString(attrs)}/>`;
73
+ }
74
+
75
+ /**
76
+ * Build an XML element with children: `<tag attr="val">...children...</tag>`.
77
+ * Children are concatenated directly (no extra whitespace).
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * xmlElement('w:p', {},
82
+ * xmlElement('w:r', {},
83
+ * xmlElement('w:t', {}, 'Hello')
84
+ * )
85
+ * )
86
+ * // '<w:p><w:r><w:t>Hello</w:t></w:r></w:p>'
87
+ * ```
88
+ */
89
+ export function xmlElement(
90
+ tag: string,
91
+ attrs?: Record<string, string | undefined>,
92
+ ...children: string[]
93
+ ): string {
94
+ const content = children.join('');
95
+ if (content === '') {
96
+ return selfClosingElement(tag, attrs);
97
+ }
98
+ return `<${tag}${attrString(attrs)}>${content}</${tag}>`;
99
+ }
100
+
101
+ /**
102
+ * Build an XML text element: `<tag attr="val">escaped text</tag>`.
103
+ * The text value is XML-escaped automatically.
104
+ *
105
+ * This is a convenience for the common pattern of wrapping text in an element,
106
+ * where you want automatic escaping (unlike `xmlElement` which takes raw children).
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * textElement('w:t', { 'xml:space': 'preserve' }, 'Hello & world')
111
+ * // '<w:t xml:space="preserve">Hello &amp; world</w:t>'
112
+ * ```
113
+ */
114
+ export function textElement(
115
+ tag: string,
116
+ attrs?: Record<string, string | undefined>,
117
+ text?: string,
118
+ ): string {
119
+ if (text === undefined || text === '') {
120
+ return selfClosingElement(tag, attrs);
121
+ }
122
+ return `<${tag}${attrString(attrs)}>${escapeXml(text)}</${tag}>`;
123
+ }