@bendyline/squisq-formats 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -11
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-4VUWTSGM.js.map +0 -1
  62. package/dist/chunk-6M7Z25LA.js +0 -46
  63. package/dist/chunk-6M7Z25LA.js.map +0 -1
  64. package/dist/chunk-74GO3FVS.js.map +0 -1
  65. package/dist/chunk-7ARKUCQT.js.map +0 -1
  66. package/dist/chunk-DTDF6QDP.js.map +0 -1
  67. package/dist/chunk-EW54IRRS.js.map +0 -1
  68. package/dist/chunk-FE6OJV6O.js.map +0 -1
  69. package/dist/chunk-GVS2XXV6.js.map +0 -1
  70. package/dist/chunk-KURGXM4I.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-NNHKUXKA.js.map +0 -1
  74. package/dist/chunk-O3GVVND4.js.map +0 -1
  75. package/dist/chunk-ODL3SSPT.js.map +0 -1
  76. package/dist/chunk-PN52A5AA.js.map +0 -1
  77. package/dist/chunk-QFLDYKCR.js.map +0 -1
  78. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  79. package/dist/chunk-RLU7UFYU.js.map +0 -1
  80. package/dist/chunk-SC67HYQJ.js.map +0 -1
  81. package/dist/chunk-SSUPBUF5.js.map +0 -1
  82. package/dist/chunk-U4MRIFKL.js.map +0 -1
  83. package/dist/chunk-UGYF5AZE.js +0 -275
  84. package/dist/chunk-UGYF5AZE.js.map +0 -1
  85. package/dist/chunk-WQSHGBLN.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-OJ7ZQV6P.js +0 -15
  93. package/dist/extract-OJ7ZQV6P.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-5VDIRPIJ.js +0 -12
  100. package/dist/layouts-5VDIRPIJ.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -491
  111. package/src/__tests__/docxImport.test.ts +0 -531
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -439
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1375
  143. package/src/docx/import.ts +0 -1250
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -217
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -1,197 +0,0 @@
1
- /**
2
- * OOXML theme reader — parses a `theme1.xml` part (DrawingML `a:theme`)
3
- * into normalized color and font schemes.
4
- *
5
- * Shared by DOCX/PPTX/XLSX theme inference: all three formats carry the same
6
- * theme part shape, differing only in where the part hangs (document/master/
7
- * workbook relationships). This module never fails hard — unreadable slots
8
- * degrade to fallbacks or absence, with a warning per degradation.
9
- */
10
-
11
- import type { OoxmlPackage } from './types.js';
12
- import { getPartRelationships, getPartXml } from './reader.js';
13
- import { NS_DRAWINGML, REL_THEME } from './namespaces.js';
14
- import { baseDirOf, findRelByType, resolveTarget } from './readUtils.js';
15
-
16
- /**
17
- * The clrScheme slots as normalized `#rrggbb` strings. The four surface/text
18
- * slots always resolve (falling back to black/white when degraded); accent and
19
- * hyperlink slots are omitted when unresolvable.
20
- */
21
- export interface OoxmlColorScheme {
22
- dk1: string;
23
- lt1: string;
24
- dk2: string;
25
- lt2: string;
26
- accent1?: string;
27
- accent2?: string;
28
- accent3?: string;
29
- accent4?: string;
30
- accent5?: string;
31
- accent6?: string;
32
- hlink?: string;
33
- folHlink?: string;
34
- }
35
-
36
- export interface OoxmlFontScheme {
37
- /** `a:fontScheme > a:majorFont > a:latin@typeface` (headings). */
38
- majorLatin?: string;
39
- /** `a:fontScheme > a:minorFont > a:latin@typeface` (body). */
40
- minorLatin?: string;
41
- }
42
-
43
- export interface OoxmlTheme {
44
- /** `a:theme@name` (e.g. "Office Theme", "Ion"). */
45
- name?: string;
46
- /** Absent when the theme carries no parseable `a:clrScheme`. */
47
- colors?: OoxmlColorScheme;
48
- /** Absent when the theme carries no `a:fontScheme` latin typefaces. */
49
- fonts?: OoxmlFontScheme;
50
- warnings: string[];
51
- }
52
-
53
- const HEX6_RE = /^[0-9a-fA-F]{6}$/;
54
-
55
- /** Well-known sysClr values that resolve without a `lastClr` hint. */
56
- const SYS_COLOR_DEFAULTS: Record<string, string> = {
57
- windowText: '000000',
58
- window: 'ffffff',
59
- };
60
-
61
- /**
62
- * Resolve one clrScheme slot element to a `#rrggbb` string, or null.
63
- * Only the base color matters for theming — trailing transform children
64
- * (`a:lumMod`, `a:tint`, …) are ignored.
65
- */
66
- function resolveSchemeColor(slot: Element): string | null {
67
- for (let i = 0; i < slot.childNodes.length; i++) {
68
- const child = slot.childNodes[i];
69
- if (!child || child.nodeType !== 1) continue;
70
- const el = child as Element;
71
- if (el.localName === 'srgbClr') {
72
- const val = el.getAttribute('val');
73
- return val && HEX6_RE.test(val) ? `#${val.toLowerCase()}` : null;
74
- }
75
- if (el.localName === 'sysClr') {
76
- const lastClr = el.getAttribute('lastClr');
77
- if (lastClr && HEX6_RE.test(lastClr)) return `#${lastClr.toLowerCase()}`;
78
- const val = el.getAttribute('val');
79
- const known = val ? SYS_COLOR_DEFAULTS[val] : undefined;
80
- return known ? `#${known}` : null;
81
- }
82
- // Any other color form (schemeClr self-reference, prstClr, …) is
83
- // unresolvable without a full DrawingML color pipeline.
84
- return null;
85
- }
86
- return null;
87
- }
88
-
89
- function firstNS(parent: Element | Document, local: string): Element | null {
90
- const els = parent.getElementsByTagNameNS(NS_DRAWINGML, local);
91
- return els.length > 0 ? els[0]! : null;
92
- }
93
-
94
- function parseColorScheme(clrScheme: Element, warnings: string[]): OoxmlColorScheme {
95
- const resolve = (slotName: string): string | null => {
96
- const slot = firstNS(clrScheme, slotName);
97
- if (!slot) return null;
98
- return resolveSchemeColor(slot);
99
- };
100
-
101
- const dk1 = resolve('dk1');
102
- const lt1 = resolve('lt1');
103
- if (!dk1) warnings.push('theme: dk1 color could not be resolved; using #000000');
104
- if (!lt1) warnings.push('theme: lt1 color could not be resolved; using #ffffff');
105
-
106
- const dk2 = resolve('dk2');
107
- const lt2 = resolve('lt2');
108
-
109
- const colors: OoxmlColorScheme = {
110
- dk1: dk1 ?? '#000000',
111
- lt1: lt1 ?? '#ffffff',
112
- dk2: dk2 ?? dk1 ?? '#000000',
113
- lt2: lt2 ?? lt1 ?? '#ffffff',
114
- };
115
-
116
- for (const slot of ['accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6'] as const) {
117
- const hex = resolve(slot);
118
- if (hex) colors[slot] = hex;
119
- else warnings.push(`theme: ${slot} color could not be resolved; dropped`);
120
- }
121
- for (const slot of ['hlink', 'folHlink'] as const) {
122
- const hex = resolve(slot);
123
- if (hex) colors[slot] = hex;
124
- }
125
- return colors;
126
- }
127
-
128
- function parseFontScheme(fontScheme: Element, warnings: string[]): OoxmlFontScheme | undefined {
129
- const readLatin = (fontName: 'majorFont' | 'minorFont'): string | undefined => {
130
- const font = firstNS(fontScheme, fontName);
131
- if (!font) return undefined;
132
- const latin = firstNS(font, 'latin');
133
- const typeface = latin?.getAttribute('typeface')?.trim();
134
- if (!typeface) {
135
- warnings.push(`theme: ${fontName} has no latin typeface`);
136
- return undefined;
137
- }
138
- return typeface;
139
- };
140
-
141
- const majorLatin = readLatin('majorFont');
142
- const minorLatin = readLatin('minorFont');
143
- if (!majorLatin && !minorLatin) return undefined;
144
- return {
145
- ...(majorLatin ? { majorLatin } : {}),
146
- ...(minorLatin ? { minorLatin } : {}),
147
- };
148
- }
149
-
150
- /** Parse an already-loaded theme part Document. */
151
- export function parseThemeXml(themeDoc: Document): OoxmlTheme {
152
- const warnings: string[] = [];
153
-
154
- const root = firstNS(themeDoc, 'theme');
155
- const name = root?.getAttribute('name')?.trim() || undefined;
156
-
157
- const clrScheme = firstNS(themeDoc, 'clrScheme');
158
- let colors: OoxmlColorScheme | undefined;
159
- if (clrScheme) {
160
- colors = parseColorScheme(clrScheme, warnings);
161
- } else {
162
- warnings.push('theme: no clrScheme found');
163
- }
164
-
165
- const fontScheme = firstNS(themeDoc, 'fontScheme');
166
- let fonts: OoxmlFontScheme | undefined;
167
- if (fontScheme) {
168
- fonts = parseFontScheme(fontScheme, warnings);
169
- } else {
170
- warnings.push('theme: no fontScheme found');
171
- }
172
-
173
- return {
174
- ...(name ? { name } : {}),
175
- ...(colors ? { colors } : {}),
176
- ...(fonts ? { fonts } : {}),
177
- warnings,
178
- };
179
- }
180
-
181
- /**
182
- * Resolve the theme part related from `fromPartPath` (via `REL_THEME`) and
183
- * parse it. Returns null when the part has no theme relationship or the
184
- * target part is missing — callers decide whether that's an error.
185
- */
186
- export async function readThemePart(
187
- pkg: OoxmlPackage,
188
- fromPartPath: string,
189
- ): Promise<OoxmlTheme | null> {
190
- const rels = await getPartRelationships(pkg, fromPartPath);
191
- const rel = findRelByType(rels, REL_THEME);
192
- if (!rel) return null;
193
- const themePath = resolveTarget(baseDirOf(fromPartPath), rel.target);
194
- const doc = await getPartXml(pkg, themePath);
195
- if (!doc) return null;
196
- return parseThemeXml(doc);
197
- }
@@ -1,103 +0,0 @@
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
- * relationships, content types, and core properties.
7
- */
8
-
9
- // ============================================
10
- // Package
11
- // ============================================
12
-
13
- /**
14
- * An opened OOXML package. Instances are created only by `openPackage`; the
15
- * archive remains private so every part read goes through the bounded reader.
16
- */
17
- declare const ooxmlPackageBrand: unique symbol;
18
- export interface OoxmlPackage {
19
- /** Nominal brand: callers cannot manually construct a safety-bypassing package. */
20
- readonly [ooxmlPackageBrand]: never;
21
- /** Parsed [Content_Types].xml entries */
22
- readonly contentTypes: ContentTypeMap;
23
- /** Root-level relationships (_rels/.rels) */
24
- readonly rootRelationships: readonly Relationship[];
25
- }
26
-
27
- /**
28
- * Content type map built from [Content_Types].xml.
29
- * Maps part paths → content type strings plus extension defaults.
30
- */
31
- export interface ContentTypeMap {
32
- /** Explicit overrides: partName → contentType */
33
- overrides: Map<string, string>;
34
- /** Default extensions: extension → contentType */
35
- defaults: Map<string, string>;
36
- }
37
-
38
- // ============================================
39
- // Relationships
40
- // ============================================
41
-
42
- /**
43
- * An OOXML relationship entry (from any _rels/*.rels file).
44
- */
45
- export interface Relationship {
46
- /** Relationship ID (e.g., "rId1") */
47
- id: string;
48
- /** Relationship type URI (e.g., "http://...officedocument/...") */
49
- type: string;
50
- /** Target path or URL */
51
- target: string;
52
- /** "Internal" (default) or "External" for hyperlinks */
53
- targetMode?: 'Internal' | 'External';
54
- }
55
-
56
- // ============================================
57
- // Core Properties
58
- // ============================================
59
-
60
- /**
61
- * Document core properties from docProps/core.xml (Dublin Core metadata).
62
- */
63
- export interface CoreProperties {
64
- title?: string;
65
- subject?: string;
66
- creator?: string;
67
- keywords?: string;
68
- description?: string;
69
- lastModifiedBy?: string;
70
- revision?: string;
71
- created?: string;
72
- modified?: string;
73
- }
74
-
75
- // ============================================
76
- // Package Builder
77
- // ============================================
78
-
79
- /**
80
- * A part (file) that will be written into an OOXML package.
81
- */
82
- export interface PackagePart {
83
- /** Path within the ZIP (e.g., "word/document.xml") */
84
- path: string;
85
- /** XML or text content (mutually exclusive with binaryContent) */
86
- content?: string;
87
- /** Binary content (mutually exclusive with content) */
88
- binaryContent?: ArrayBuffer | Uint8Array;
89
- /** MIME content type for [Content_Types].xml */
90
- contentType: string;
91
- }
92
-
93
- /**
94
- * A relationship to be written into a _rels/*.rels file.
95
- * `sourcePart` identifies which part the relationship belongs to.
96
- * Use "" (empty string) for root-level relationships (_rels/.rels).
97
- */
98
- export interface PendingRelationship {
99
- /** The part this relationship belongs to ("" for root) */
100
- sourcePart: string;
101
- /** The relationship entry */
102
- relationship: Relationship;
103
- }
@@ -1,339 +0,0 @@
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
- import { assertSafeZipPath } from '../shared/zipSafety.js';
24
-
25
- // ============================================
26
- // Package Builder
27
- // ============================================
28
-
29
- /**
30
- * Mutable builder for constructing an OOXML package.
31
- *
32
- * Add parts, relationships, and core properties, then call `toBlob()`
33
- * or `toArrayBuffer()` to produce the final ZIP archive.
34
- *
35
- * @example
36
- * ```ts
37
- * const builder = createPackage();
38
- * builder.addPart('word/document.xml', documentXml, CONTENT_TYPE_DOCX_DOCUMENT);
39
- * builder.addRelationship('', {
40
- * id: 'rId1',
41
- * type: REL_OFFICE_DOCUMENT,
42
- * target: 'word/document.xml',
43
- * });
44
- * const blob = await builder.toBlob();
45
- * ```
46
- */
47
- export interface OoxmlPackageBuilder {
48
- /**
49
- * Add an XML or text part to the package.
50
- *
51
- * @param path - Path within the archive (e.g., "word/document.xml")
52
- * @param content - XML string content
53
- * @param contentType - MIME content type for [Content_Types].xml
54
- */
55
- addPart(path: string, content: string, contentType: string): void;
56
-
57
- /**
58
- * Add a binary part to the package (e.g., an image).
59
- *
60
- * @param path - Path within the archive (e.g., "word/media/image1.png")
61
- * @param data - Binary content
62
- * @param contentType - MIME content type (e.g., "image/png")
63
- */
64
- addBinaryPart(path: string, data: ArrayBuffer | Uint8Array, contentType: string): void;
65
-
66
- /**
67
- * Register a relationship.
68
- *
69
- * @param sourcePart - The part this relationship belongs to (e.g., "word/document.xml").
70
- * Use "" (empty string) for root-level relationships (_rels/.rels).
71
- * @param rel - The relationship entry
72
- */
73
- addRelationship(sourcePart: string, rel: Relationship): void;
74
-
75
- /**
76
- * Set core document properties (docProps/core.xml).
77
- * Calling this multiple times overwrites previous values.
78
- */
79
- setCoreProperties(props: CoreProperties): void;
80
-
81
- /**
82
- * Assemble the final OOXML package as a Blob.
83
- * Generates [Content_Types].xml and all _rels/*.rels files automatically.
84
- */
85
- toBlob(): Promise<Blob>;
86
-
87
- /**
88
- * Assemble the final OOXML package as an ArrayBuffer.
89
- */
90
- toArrayBuffer(): Promise<ArrayBuffer>;
91
- }
92
-
93
- /**
94
- * Create a new OOXML package builder.
95
- */
96
- export function createPackage(): OoxmlPackageBuilder {
97
- const parts: PackagePart[] = [];
98
- const relationships: PendingRelationship[] = [];
99
- let coreProps: CoreProperties | undefined;
100
-
101
- return {
102
- addPart(path, content, contentType) {
103
- assertSafeZipPath(path);
104
- parts.push({ path, content, contentType });
105
- },
106
-
107
- addBinaryPart(path, data, contentType) {
108
- assertSafeZipPath(path);
109
- parts.push({ path, binaryContent: data, contentType });
110
- },
111
-
112
- addRelationship(sourcePart, rel) {
113
- if (sourcePart) assertSafeZipPath(sourcePart);
114
- relationships.push({ sourcePart, relationship: rel });
115
- },
116
-
117
- setCoreProperties(props) {
118
- coreProps = props;
119
- },
120
-
121
- async toBlob() {
122
- const zip = assemble(parts, relationships, coreProps);
123
- return zip.generateAsync({ type: 'blob' });
124
- },
125
-
126
- async toArrayBuffer() {
127
- const zip = assemble(parts, relationships, coreProps);
128
- return zip.generateAsync({ type: 'arraybuffer' });
129
- },
130
- };
131
- }
132
-
133
- // ============================================
134
- // Assembly
135
- // ============================================
136
-
137
- /**
138
- * Assemble a JSZip archive from parts, relationships, and core properties.
139
- */
140
- function assemble(
141
- parts: PackagePart[],
142
- relationships: PendingRelationship[],
143
- coreProps?: CoreProperties,
144
- ): JSZip {
145
- const zip = new JSZip();
146
- // Assembly must be idempotent: callers may request both Blob and
147
- // ArrayBuffer representations from one builder. Never append generated
148
- // relationships to the builder-owned array.
149
- const assembledRelationships = relationships.slice();
150
-
151
- // Write content parts
152
- for (const part of parts) {
153
- if (part.content !== undefined) {
154
- zip.file(part.path, part.content);
155
- } else if (part.binaryContent !== undefined) {
156
- zip.file(part.path, part.binaryContent);
157
- }
158
- }
159
-
160
- // Add core properties if set
161
- if (coreProps) {
162
- const coreXml = buildCorePropertiesXml(coreProps);
163
- zip.file('docProps/core.xml', coreXml);
164
-
165
- // Add the generated relationship unless the caller already supplied it.
166
- if (
167
- !assembledRelationships.some(
168
- ({ sourcePart, relationship }) =>
169
- sourcePart === '' && relationship.type === REL_CORE_PROPERTIES,
170
- )
171
- ) {
172
- const ids = new Set(assembledRelationships.map(({ relationship }) => relationship.id));
173
- let nextId = 100;
174
- while (ids.has(`rId${nextId}`)) nextId++;
175
- assembledRelationships.push({
176
- sourcePart: '',
177
- relationship: {
178
- id: `rId${nextId}`,
179
- type: REL_CORE_PROPERTIES,
180
- target: 'docProps/core.xml',
181
- },
182
- });
183
- }
184
- }
185
-
186
- // Build and write [Content_Types].xml
187
- zip.file('[Content_Types].xml', buildContentTypesXml(parts, coreProps));
188
-
189
- // Build and write _rels/*.rels files
190
- const relsBySource = groupRelationshipsBySource(assembledRelationships);
191
- for (const [sourcePart, rels] of relsBySource) {
192
- const relsPath = sourcePart === '' ? '_rels/.rels' : buildRelsPath(sourcePart);
193
- zip.file(relsPath, buildRelationshipsXml(rels));
194
- }
195
-
196
- return zip;
197
- }
198
-
199
- // ============================================
200
- // [Content_Types].xml
201
- // ============================================
202
-
203
- function buildContentTypesXml(parts: PackagePart[], coreProps?: CoreProperties): string {
204
- const lines: string[] = [xmlDeclaration()];
205
- lines.push(`<Types xmlns="${NS_CONTENT_TYPES}">`);
206
-
207
- // Default extension types
208
- const extensionTypes = new Map<string, string>();
209
- extensionTypes.set('rels', CONTENT_TYPE_RELATIONSHIPS);
210
- extensionTypes.set('xml', 'application/xml');
211
-
212
- // Collect extensions from binary parts (e.g., png, jpeg)
213
- for (const part of parts) {
214
- if (part.binaryContent !== undefined) {
215
- const ext = getExtension(part.path);
216
- if (ext && !extensionTypes.has(ext)) {
217
- extensionTypes.set(ext, part.contentType);
218
- }
219
- }
220
- }
221
-
222
- for (const [ext, ct] of extensionTypes) {
223
- lines.push(` <Default Extension="${escapeXml(ext)}" ContentType="${escapeXml(ct)}"/>`);
224
- }
225
-
226
- // Override entries for each XML part
227
- for (const part of parts) {
228
- if (part.content !== undefined) {
229
- lines.push(
230
- ` <Override PartName="/${escapeXml(part.path)}" ContentType="${escapeXml(part.contentType)}"/>`,
231
- );
232
- }
233
- }
234
-
235
- // Core properties override
236
- if (coreProps) {
237
- lines.push(
238
- ` <Override PartName="/docProps/core.xml" ContentType="${CONTENT_TYPE_CORE_PROPERTIES}"/>`,
239
- );
240
- }
241
-
242
- lines.push('</Types>');
243
- return lines.join('\n');
244
- }
245
-
246
- // ============================================
247
- // Relationships XML
248
- // ============================================
249
-
250
- function buildRelationshipsXml(rels: Relationship[]): string {
251
- const lines: string[] = [xmlDeclaration()];
252
- lines.push(`<Relationships xmlns="${NS_RELATIONSHIPS}">`);
253
-
254
- for (const rel of rels) {
255
- const attrs: string[] = [
256
- `Id="${escapeXml(rel.id)}"`,
257
- `Type="${escapeXml(rel.type)}"`,
258
- `Target="${escapeXml(rel.target)}"`,
259
- ];
260
- if (rel.targetMode) {
261
- attrs.push(`TargetMode="${escapeXml(rel.targetMode)}"`);
262
- }
263
- lines.push(` <Relationship ${attrs.join(' ')}/>`);
264
- }
265
-
266
- lines.push('</Relationships>');
267
- return lines.join('\n');
268
- }
269
-
270
- // ============================================
271
- // Core Properties XML
272
- // ============================================
273
-
274
- function buildCorePropertiesXml(props: CoreProperties): string {
275
- const lines: string[] = [xmlDeclaration()];
276
- lines.push(
277
- `<cp:coreProperties` +
278
- ` xmlns:cp="${NS_CORE_PROPERTIES}"` +
279
- ` xmlns:dc="${NS_DC}"` +
280
- ` xmlns:dcterms="${NS_DCTERMS}"` +
281
- ` xmlns:xsi="${NS_XSI}">`,
282
- );
283
-
284
- if (props.title) lines.push(` <dc:title>${escapeXml(props.title)}</dc:title>`);
285
- if (props.subject) lines.push(` <dc:subject>${escapeXml(props.subject)}</dc:subject>`);
286
- if (props.creator) lines.push(` <dc:creator>${escapeXml(props.creator)}</dc:creator>`);
287
- if (props.description)
288
- lines.push(` <dc:description>${escapeXml(props.description)}</dc:description>`);
289
- if (props.keywords) lines.push(` <cp:keywords>${escapeXml(props.keywords)}</cp:keywords>`);
290
- if (props.lastModifiedBy)
291
- lines.push(` <cp:lastModifiedBy>${escapeXml(props.lastModifiedBy)}</cp:lastModifiedBy>`);
292
- if (props.revision) lines.push(` <cp:revision>${escapeXml(props.revision)}</cp:revision>`);
293
- if (props.created) {
294
- lines.push(
295
- ` <dcterms:created xsi:type="dcterms:W3CDTF">${escapeXml(props.created)}</dcterms:created>`,
296
- );
297
- }
298
- if (props.modified) {
299
- lines.push(
300
- ` <dcterms:modified xsi:type="dcterms:W3CDTF">${escapeXml(props.modified)}</dcterms:modified>`,
301
- );
302
- }
303
-
304
- lines.push('</cp:coreProperties>');
305
- return lines.join('\n');
306
- }
307
-
308
- // ============================================
309
- // Helpers
310
- // ============================================
311
-
312
- function getExtension(path: string): string | undefined {
313
- const dot = path.lastIndexOf('.');
314
- if (dot === -1) return undefined;
315
- return path.substring(dot + 1).toLowerCase();
316
- }
317
-
318
- function buildRelsPath(partPath: string): string {
319
- const lastSlash = partPath.lastIndexOf('/');
320
- if (lastSlash === -1) {
321
- return `_rels/${partPath}.rels`;
322
- }
323
- const dir = partPath.substring(0, lastSlash);
324
- const file = partPath.substring(lastSlash + 1);
325
- return `${dir}/_rels/${file}.rels`;
326
- }
327
-
328
- function groupRelationshipsBySource(pending: PendingRelationship[]): Map<string, Relationship[]> {
329
- const grouped = new Map<string, Relationship[]>();
330
- for (const { sourcePart, relationship } of pending) {
331
- let list = grouped.get(sourcePart);
332
- if (!list) {
333
- list = [];
334
- grouped.set(sourcePart, list);
335
- }
336
- list.push(relationship);
337
- }
338
- return grouped;
339
- }