@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.
- package/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +1 -1
- package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
- package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
- package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
- package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
- package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
- package/dist/chunk-GX7RAUME.js +121 -0
- package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
- package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
- package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
- package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
- package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
- package/dist/chunk-OGS5VCGJ.js +446 -0
- package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
- package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
- package/dist/container/index.js +1 -2
- package/dist/csv/index.d.ts +27 -2
- package/dist/csv/index.js +1 -2
- package/dist/docx/index.d.ts +5 -1
- package/dist/docx/index.js +9 -11
- package/dist/epub/index.d.ts +2 -0
- package/dist/epub/index.js +5 -6
- package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
- package/dist/extract-MN7LA3NL.js +13 -0
- package/dist/html/index.d.ts +11 -4
- package/dist/html/index.js +3 -4
- package/dist/images-ESPQKVTW.js +6 -0
- package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
- package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +28 -26
- package/dist/infer/index.d.ts +3 -3
- package/dist/infer/index.js +7 -9
- package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
- package/dist/layouts-DRWZGSPD.js +10 -0
- package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
- package/dist/ooxml/index.d.ts +3 -3
- package/dist/ooxml/index.js +14 -13
- package/dist/pdf/index.d.ts +18 -0
- package/dist/pdf/index.js +2 -3
- package/dist/pptx/index.d.ts +4 -4
- package/dist/pptx/index.js +11 -13
- package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
- package/dist/registry/index.d.ts +21 -5
- package/dist/registry/index.js +9 -6
- package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
- package/dist/xlsx/index.d.ts +3 -3
- package/dist/xlsx/index.js +6 -7
- package/package.json +6 -3
- package/dist/chunk-4VUWTSGM.js.map +0 -1
- package/dist/chunk-6M7Z25LA.js +0 -46
- package/dist/chunk-6M7Z25LA.js.map +0 -1
- package/dist/chunk-74GO3FVS.js.map +0 -1
- package/dist/chunk-7ARKUCQT.js.map +0 -1
- package/dist/chunk-DTDF6QDP.js.map +0 -1
- package/dist/chunk-EW54IRRS.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-KURGXM4I.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.js.map +0 -1
- package/dist/chunk-NNHKUXKA.js.map +0 -1
- package/dist/chunk-O3GVVND4.js.map +0 -1
- package/dist/chunk-ODL3SSPT.js.map +0 -1
- package/dist/chunk-PN52A5AA.js.map +0 -1
- package/dist/chunk-QFLDYKCR.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-RLU7UFYU.js.map +0 -1
- package/dist/chunk-SC67HYQJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-UGYF5AZE.js +0 -275
- package/dist/chunk-UGYF5AZE.js.map +0 -1
- package/dist/chunk-WQSHGBLN.js.map +0 -1
- package/dist/chunk-YRT7GQ5Y.js +0 -28
- package/dist/chunk-YRT7GQ5Y.js.map +0 -1
- package/dist/container/index.js.map +0 -1
- package/dist/csv/index.js.map +0 -1
- package/dist/docx/index.js.map +0 -1
- package/dist/epub/index.js.map +0 -1
- package/dist/extract-OJ7ZQV6P.js +0 -15
- package/dist/extract-OJ7ZQV6P.js.map +0 -1
- package/dist/html/index.js.map +0 -1
- package/dist/images-7FBWPKE3.js +0 -7
- package/dist/images-7FBWPKE3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infer/index.js.map +0 -1
- package/dist/layouts-5VDIRPIJ.js +0 -12
- package/dist/layouts-5VDIRPIJ.js.map +0 -1
- package/dist/mapTheme-IR27S6IV.js.map +0 -1
- package/dist/ooxml/index.js.map +0 -1
- package/dist/pdf/index.js.map +0 -1
- package/dist/pptx/index.js.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/xlsx/index.js.map +0 -1
- package/src/__tests__/container.test.ts +0 -230
- package/src/__tests__/convert.test.ts +0 -495
- package/src/__tests__/csvImport.test.ts +0 -84
- package/src/__tests__/docxExport.test.ts +0 -491
- package/src/__tests__/docxImport.test.ts +0 -531
- package/src/__tests__/epub.test.ts +0 -649
- package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
- package/src/__tests__/formatRegistry.test.ts +0 -174
- package/src/__tests__/html.test.ts +0 -439
- package/src/__tests__/htmlImport.test.ts +0 -57
- package/src/__tests__/inferTheme.test.ts +0 -135
- package/src/__tests__/lossyWarnings.test.ts +0 -146
- package/src/__tests__/ooxml.test.ts +0 -271
- package/src/__tests__/ooxmlCancellation.test.ts +0 -113
- package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
- package/src/__tests__/pdfExport.test.ts +0 -322
- package/src/__tests__/pdfImport.test.ts +0 -384
- package/src/__tests__/plainHtml.test.ts +0 -417
- package/src/__tests__/plainHtmlBundle.test.ts +0 -253
- package/src/__tests__/pptxExport.test.ts +0 -138
- package/src/__tests__/pptxImport.test.ts +0 -145
- package/src/__tests__/pptxInferFixtures.ts +0 -314
- package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
- package/src/__tests__/roundTrip.test.ts +0 -201
- package/src/__tests__/roundTripAssets.test.ts +0 -50
- package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
- package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
- package/src/__tests__/roundTripMatrix.test.ts +0 -142
- package/src/__tests__/sharedContainer.test.ts +0 -41
- package/src/__tests__/sharedImages.test.ts +0 -61
- package/src/__tests__/xlsxExport.test.ts +0 -164
- package/src/__tests__/xlsxImport.test.ts +0 -80
- package/src/__tests__/zipSafety.test.ts +0 -317
- package/src/container/index.ts +0 -94
- package/src/csv/index.ts +0 -188
- package/src/docx/export.ts +0 -1375
- package/src/docx/import.ts +0 -1250
- package/src/docx/index.ts +0 -26
- package/src/docx/styles.ts +0 -145
- package/src/epub/export.ts +0 -968
- package/src/epub/index.ts +0 -20
- package/src/html/docsHtmlBundle.ts +0 -373
- package/src/html/htmlTemplate.ts +0 -385
- package/src/html/imageUtils.ts +0 -61
- package/src/html/import.ts +0 -297
- package/src/html/index.ts +0 -212
- package/src/html/plainHtml.ts +0 -790
- package/src/html/plainHtmlBundle.ts +0 -421
- package/src/index.ts +0 -109
- package/src/infer/extract.ts +0 -127
- package/src/infer/index.ts +0 -199
- package/src/infer/mapTheme.ts +0 -176
- package/src/infer/types.ts +0 -27
- package/src/ooxml/index.ts +0 -111
- package/src/ooxml/namespaces.ts +0 -217
- package/src/ooxml/readUtils.ts +0 -44
- package/src/ooxml/reader.ts +0 -318
- package/src/ooxml/themeReader.ts +0 -197
- package/src/ooxml/types.ts +0 -103
- package/src/ooxml/writer.ts +0 -339
- package/src/ooxml/xmlUtils.ts +0 -123
- package/src/pdf/export.ts +0 -1084
- package/src/pdf/import.ts +0 -1164
- package/src/pdf/index.ts +0 -29
- package/src/pdf/styles.ts +0 -180
- package/src/pptx/export.ts +0 -1184
- package/src/pptx/import.ts +0 -455
- package/src/pptx/index.ts +0 -52
- package/src/pptx/layouts.ts +0 -1222
- package/src/pptx/styles.ts +0 -96
- package/src/pptx/templates.ts +0 -187
- package/src/registry/convert.ts +0 -433
- package/src/registry/defaultFormats.ts +0 -413
- package/src/registry/errors.ts +0 -46
- package/src/registry/index.ts +0 -43
- package/src/registry/registry.ts +0 -48
- package/src/registry/types.ts +0 -170
- package/src/shared/boundedZipArchive.ts +0 -383
- package/src/shared/container.ts +0 -28
- package/src/shared/fidelity.ts +0 -130
- package/src/shared/images.ts +0 -44
- package/src/shared/inlineRuns.ts +0 -99
- package/src/shared/text.ts +0 -41
- package/src/shared/zipEntryCount.ts +0 -151
- package/src/shared/zipLimits.ts +0 -296
- package/src/shared/zipSafety.ts +0 -19
- package/src/xlsx/export.ts +0 -253
- package/src/xlsx/import.ts +0 -160
- package/src/xlsx/index.ts +0 -35
package/src/ooxml/themeReader.ts
DELETED
|
@@ -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
|
-
}
|
package/src/ooxml/types.ts
DELETED
|
@@ -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
|
-
}
|
package/src/ooxml/writer.ts
DELETED
|
@@ -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
|
-
}
|