@office-open/core 0.6.4 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,337 @@
1
+ import { xml2js } from "@office-open/xml";
2
+ //#region src/patch/xml-namespace.ts
3
+ const DOCX_NS = {
4
+ paragraph: "w:p",
5
+ run: "w:r",
6
+ text: "w:t",
7
+ runProperties: "w:rPr"
8
+ };
9
+ const PPTX_NS = {
10
+ paragraph: "a:p",
11
+ run: "a:r",
12
+ text: "a:t",
13
+ runProperties: "a:rPr"
14
+ };
15
+ //#endregion
16
+ //#region src/patch/xml-patch-utils.ts
17
+ /**
18
+ * XML utility functions for patch operations.
19
+ */
20
+ const toJson = (xmlData) => {
21
+ return xml2js(xmlData, {
22
+ captureSpacesBetweenElements: true,
23
+ compact: false
24
+ });
25
+ };
26
+ /**
27
+ * Creates the inner content of a text element (`w:t` / `a:t`).
28
+ *
29
+ * Returns `[{ type: "text", text }]` for non-empty text, `[]` for empty.
30
+ * The `xml:space` attribute is handled separately by `patchSpaceAttribute`.
31
+ */
32
+ const createTextElementContents = (text) => text === "" ? [] : [{
33
+ text,
34
+ type: "text"
35
+ }];
36
+ const patchSpaceAttribute = (element) => ({
37
+ ...element,
38
+ attributes: { "xml:space": "preserve" }
39
+ });
40
+ const getFirstLevelElements = (relationships, id) => relationships.elements?.filter((e) => e.name === id)[0].elements ?? [];
41
+ //#endregion
42
+ //#region src/patch/paragraph-split-inject.ts
43
+ var TokenNotFoundError = class extends Error {
44
+ constructor(token) {
45
+ super(`Token ${token} not found`);
46
+ this.name = "TokenNotFoundError";
47
+ }
48
+ };
49
+ function createSplitInject(ns, createTextElementContents, options) {
50
+ const preserveSpace = options?.preserveSpace ?? true;
51
+ const findRunElementIndexWithToken = (paragraphElement, token) => {
52
+ for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) {
53
+ const element = paragraphElement.elements[i];
54
+ if (element.type === "element" && element.name === ns.run) {
55
+ const textElement = (element.elements ?? []).filter((e) => e.type === "element" && e.name === ns.text);
56
+ for (const text of textElement) {
57
+ if (!text.elements?.[0]) continue;
58
+ if (text.elements[0].text?.includes(token)) return i;
59
+ }
60
+ }
61
+ }
62
+ throw new TokenNotFoundError(token);
63
+ };
64
+ const splitRunElement = (runElement, token) => {
65
+ let splitIndex = -1;
66
+ const splitElements = runElement.elements?.map((e, i) => {
67
+ if (splitIndex !== -1) return e;
68
+ if (e.type === "element" && e.name === ns.text) {
69
+ const splitText = (e.elements?.[0]?.text ?? "").split(token);
70
+ const newElements = splitText.map((t) => ({
71
+ ...e,
72
+ ...preserveSpace ? patchSpaceAttribute(e) : {},
73
+ elements: createTextElementContents(t)
74
+ }));
75
+ if (splitText.length > 1) splitIndex = i;
76
+ return newElements;
77
+ } else return e;
78
+ }).flat() ?? [];
79
+ return {
80
+ left: {
81
+ ...JSON.parse(JSON.stringify(runElement)),
82
+ elements: splitElements.slice(0, splitIndex + 1)
83
+ },
84
+ right: {
85
+ ...JSON.parse(JSON.stringify(runElement)),
86
+ elements: splitElements.slice(splitIndex + 1)
87
+ }
88
+ };
89
+ };
90
+ return {
91
+ findRunElementIndexWithToken,
92
+ splitRunElement
93
+ };
94
+ }
95
+ //#endregion
96
+ //#region src/patch/paragraph-token-replacer.ts
97
+ const ReplaceMode = {
98
+ START: 0,
99
+ MIDDLE: 1,
100
+ END: 2
101
+ };
102
+ function createTokenReplacer(createTextElementContents, options) {
103
+ const preserveSpace = options?.preserveSpace ?? true;
104
+ const patchTextElement = (element, text) => {
105
+ element.elements = createTextElementContents(text);
106
+ return element;
107
+ };
108
+ return ({ paragraphElement, renderedParagraph, originalText, replacementText }) => {
109
+ const startIndex = renderedParagraph.text.indexOf(originalText);
110
+ const endIndex = startIndex + originalText.length - 1;
111
+ let replaceMode = ReplaceMode.START;
112
+ for (const run of renderedParagraph.runs) for (const { text, index, start, end } of run.parts) switch (replaceMode) {
113
+ case ReplaceMode.START:
114
+ if (startIndex >= start && startIndex <= end) {
115
+ const offsetStartIndex = startIndex - start;
116
+ const offsetEndIndex = Math.min(endIndex, end) - start;
117
+ const partToReplace = text.substring(offsetStartIndex, offsetEndIndex + 1);
118
+ if (partToReplace === "") continue;
119
+ const firstPart = text.replace(partToReplace, replacementText);
120
+ patchTextElement(paragraphElement.elements[run.index].elements[index], firstPart);
121
+ replaceMode = ReplaceMode.MIDDLE;
122
+ continue;
123
+ }
124
+ break;
125
+ case ReplaceMode.MIDDLE:
126
+ if (endIndex <= end) {
127
+ const lastPart = text.substring(endIndex - start + 1);
128
+ patchTextElement(paragraphElement.elements[run.index].elements[index], lastPart);
129
+ const currentElement = paragraphElement.elements[run.index].elements[index];
130
+ paragraphElement.elements[run.index].elements[index] = preserveSpace ? patchSpaceAttribute(currentElement) : currentElement;
131
+ replaceMode = ReplaceMode.END;
132
+ } else patchTextElement(paragraphElement.elements[run.index].elements[index], "");
133
+ break;
134
+ }
135
+ return paragraphElement;
136
+ };
137
+ }
138
+ //#endregion
139
+ //#region src/patch/run-renderer.ts
140
+ function createRunRenderer(ns) {
141
+ const renderParagraphNode = (node) => {
142
+ if (node.element.name !== ns.paragraph) throw new Error(`Invalid node type: ${node.element.name}`);
143
+ if (!node.element.elements) return {
144
+ index: -1,
145
+ pathToParagraph: [],
146
+ runs: [],
147
+ text: ""
148
+ };
149
+ let currentRunStringLength = 0;
150
+ const runs = node.element.elements.map((element, i) => ({
151
+ element,
152
+ i
153
+ })).filter(({ element }) => element.name === ns.run).map(({ element, i }) => {
154
+ const renderedRunNode = renderRunNode(element, i, currentRunStringLength);
155
+ currentRunStringLength += renderedRunNode.text.length;
156
+ return renderedRunNode;
157
+ }).filter((e) => Boolean(e));
158
+ const text = runs.reduce((acc, curr) => acc + curr.text, "");
159
+ return {
160
+ index: node.index,
161
+ pathToParagraph: buildNodePath(node),
162
+ runs,
163
+ text
164
+ };
165
+ };
166
+ const renderRunNode = (node, index, currentRunStringIndex) => {
167
+ if (!node.elements) return {
168
+ end: currentRunStringIndex,
169
+ index: -1,
170
+ parts: [],
171
+ start: currentRunStringIndex,
172
+ text: ""
173
+ };
174
+ let currentTextStringIndex = currentRunStringIndex;
175
+ const parts = node.elements.map((element, i) => element.name === ns.text && element.elements && element.elements.length > 0 ? (() => {
176
+ const partStart = currentTextStringIndex;
177
+ currentTextStringIndex += (element.elements[0].text?.toString() ?? "").length;
178
+ return {
179
+ end: currentTextStringIndex - 1,
180
+ index: i,
181
+ start: partStart,
182
+ text: element.elements[0].text?.toString() ?? ""
183
+ };
184
+ })() : void 0).filter((e) => Boolean(e)).map((e) => e);
185
+ const text = parts.reduce((acc, curr) => acc + curr.text, "");
186
+ return {
187
+ end: currentTextStringIndex - 1,
188
+ index,
189
+ parts,
190
+ start: currentRunStringIndex,
191
+ text
192
+ };
193
+ };
194
+ return renderParagraphNode;
195
+ }
196
+ const buildNodePath = (node) => node.parent ? [...buildNodePath(node.parent), node.index] : [node.index];
197
+ //#endregion
198
+ //#region src/patch/xml-traverser.ts
199
+ const elementsToWrapper = (wrapper) => wrapper.element.elements?.map((e, i) => ({
200
+ element: e,
201
+ index: i,
202
+ parent: wrapper
203
+ })) ?? [];
204
+ function createTraverser(ns) {
205
+ const renderParagraphNode = createRunRenderer(ns);
206
+ const traverse = (node) => {
207
+ let renderedParagraphs = [];
208
+ const queue = [...elementsToWrapper({
209
+ element: node,
210
+ index: 0,
211
+ parent: void 0
212
+ })];
213
+ let currentNode;
214
+ while (queue.length > 0) {
215
+ currentNode = queue.shift();
216
+ if (currentNode.element.name === ns.paragraph) renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)];
217
+ queue.push(...elementsToWrapper(currentNode));
218
+ }
219
+ return renderedParagraphs;
220
+ };
221
+ const findLocationOfText = (node, text) => traverse(node).filter((p) => p.text.includes(text));
222
+ return {
223
+ traverse,
224
+ findLocationOfText
225
+ };
226
+ }
227
+ //#endregion
228
+ //#region src/patch/xml-replacer.ts
229
+ const SPLIT_TOKEN = "ɵ";
230
+ function createReplacer(config) {
231
+ const { ns, formatChild } = config;
232
+ const { findLocationOfText } = createTraverser(ns);
233
+ const replaceTokenInParagraphElement = createTokenReplacer(createTextElementContents, { preserveSpace: config.preserveSpace });
234
+ const { findRunElementIndexWithToken, splitRunElement } = createSplitInject(ns, createTextElementContents, { preserveSpace: config.preserveSpace });
235
+ const replacer = ({ json, patch, patchText, context, keepOriginalStyles = true }) => {
236
+ const renderedParagraphs = findLocationOfText(json, patchText);
237
+ if (renderedParagraphs.length === 0) return {
238
+ didFindOccurrence: false,
239
+ element: json
240
+ };
241
+ for (const renderedParagraph of renderedParagraphs) {
242
+ const textJson = patch.children.flatMap((c) => formatChild(c, context));
243
+ switch (patch.type) {
244
+ case "file": {
245
+ const parentElement = goToParentElementFromPath(json, renderedParagraph.pathToParagraph);
246
+ const elementIndex = getLastElementIndexFromPath(renderedParagraph.pathToParagraph);
247
+ parentElement.elements.splice(elementIndex, 1, ...textJson);
248
+ break;
249
+ }
250
+ default: {
251
+ const paragraphElement = goToElementFromPath(json, renderedParagraph.pathToParagraph);
252
+ replaceTokenInParagraphElement({
253
+ originalText: patchText,
254
+ paragraphElement,
255
+ renderedParagraph,
256
+ replacementText: SPLIT_TOKEN
257
+ });
258
+ const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN);
259
+ const runElementToBeReplaced = paragraphElement.elements[index];
260
+ const { left, right } = splitRunElement(runElementToBeReplaced, SPLIT_TOKEN);
261
+ let newRunElements = textJson;
262
+ let patchedRightElement = right;
263
+ if (keepOriginalStyles) {
264
+ const runElementNonTextualElements = runElementToBeReplaced.elements.filter((e) => e.type === "element" && e.name === ns.runProperties);
265
+ newRunElements = textJson.map((e) => {
266
+ if (e.type !== "element" || e.name !== ns.run || e.elements?.some((c) => c.type === "element" && c.name === ns.runProperties)) return e;
267
+ return {
268
+ ...e,
269
+ elements: [...runElementNonTextualElements, ...e.elements ?? []]
270
+ };
271
+ });
272
+ patchedRightElement = {
273
+ ...right,
274
+ elements: [...runElementNonTextualElements, ...right.elements]
275
+ };
276
+ }
277
+ paragraphElement.elements.splice(index, 1, left, ...newRunElements, patchedRightElement);
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ return {
283
+ didFindOccurrence: true,
284
+ element: json
285
+ };
286
+ };
287
+ return replacer;
288
+ }
289
+ const goToElementFromPath = (json, path) => {
290
+ let element = json;
291
+ for (let i = 1; i < path.length; i++) {
292
+ const index = path[i];
293
+ element = element.elements[index];
294
+ }
295
+ return element;
296
+ };
297
+ const goToParentElementFromPath = (json, path) => goToElementFromPath(json, path.slice(0, -1));
298
+ const getLastElementIndexFromPath = (path) => path[path.length - 1];
299
+ //#endregion
300
+ //#region src/patch/content-types-manager.ts
301
+ const appendContentType = (element, contentType, extension) => {
302
+ const relationshipElements = getFirstLevelElements(element, "Types");
303
+ if (relationshipElements.some((el) => el.type === "element" && el.name === "Default" && el?.attributes?.ContentType === contentType && el?.attributes?.Extension === extension)) return;
304
+ relationshipElements.push({
305
+ attributes: {
306
+ ContentType: contentType,
307
+ Extension: extension
308
+ },
309
+ name: "Default",
310
+ type: "element"
311
+ });
312
+ };
313
+ //#endregion
314
+ //#region src/patch/relationship-manager.ts
315
+ const getIdFromRelationshipId = (relationshipId) => {
316
+ const output = parseInt(relationshipId.substring(3), 10);
317
+ return isNaN(output) ? 0 : output;
318
+ };
319
+ const getNextRelationshipIndex = (relationships) => {
320
+ return getFirstLevelElements(relationships, "Relationships").map((e) => getIdFromRelationshipId(e.attributes?.Id?.toString() ?? "")).reduce((acc, curr) => Math.max(acc, curr), 0) + 1;
321
+ };
322
+ const appendRelationship = (relationships, id, type, target, targetMode) => {
323
+ const relationshipElements = getFirstLevelElements(relationships, "Relationships");
324
+ relationshipElements.push({
325
+ attributes: {
326
+ Id: `rId${id}`,
327
+ Target: target,
328
+ TargetMode: targetMode,
329
+ Type: type
330
+ },
331
+ name: "Relationship",
332
+ type: "element"
333
+ });
334
+ return relationshipElements;
335
+ };
336
+ //#endregion
337
+ export { createTraverser as a, TokenNotFoundError as c, getFirstLevelElements as d, patchSpaceAttribute as f, PPTX_NS as h, createReplacer as i, createSplitInject as l, DOCX_NS as m, getNextRelationshipIndex as n, createRunRenderer as o, toJson as p, appendContentType as r, createTokenReplacer as s, appendRelationship as t, createTextElementContents as u };
@@ -1,2 +1,2 @@
1
- import { a as Point, c as Connection, d as getLayoutXml, f as getStyleXml, h as STYLE_CATEGORIES, i as SmartArtData, l as DEFAULT_DRAWING_XML, m as LAYOUT_CATEGORIES, n as createDataModel, o as TransPoint, p as COLOR_CATEGORIES, r as SmartArtCollection, s as DataModel, t as TreeNode, u as getColorXml } from "../index-DigYTiB_.mjs";
1
+ import { a as Point, c as Connection, d as getLayoutXml, f as getStyleXml, h as STYLE_CATEGORIES, i as SmartArtData, l as DEFAULT_DRAWING_XML, m as LAYOUT_CATEGORIES, n as createDataModel, o as TransPoint, p as COLOR_CATEGORIES, r as SmartArtCollection, s as DataModel, t as TreeNode, u as getColorXml } from "../index-CZxcE4Q6.mjs";
2
2
  export { COLOR_CATEGORIES, Connection, DEFAULT_DRAWING_XML, DataModel, LAYOUT_CATEGORIES, Point, STYLE_CATEGORIES, SmartArtCollection, SmartArtData, TransPoint, TreeNode, createDataModel, getColorXml, getLayoutXml, getStyleXml };
package/dist/values.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as uCharHexNumber, S as twipsMeasureValue, T as unsignedDecimalNumber, _ as pointMeasureValue, a as ThemeColor, b as signedHpsMeasureValue, c as dateTimeValue, d as hexBinary, f as hexColorValue, g as percentageValue, h as measurementOrPercentValue, i as RelativeMeasure, l as decimalNumber, m as longHexNumber, n as PositivePercentage, o as ThemeFont, p as hpsMeasureValue, r as PositiveUniversalMeasure, s as UniversalMeasure, t as Percentage, u as eighthPointMeasureValue, v as positiveUniversalMeasureValue, w as universalMeasureValue, x as signedTwipsMeasureValue, y as shortHexNumber } from "./values-CIh0bdS1.mjs";
1
+ import { C as uCharHexNumber, S as twipsMeasureValue, T as unsignedDecimalNumber, _ as pointMeasureValue, a as ThemeColor, b as signedHpsMeasureValue, c as dateTimeValue, d as hexBinary, f as hexColorValue, g as percentageValue, h as measurementOrPercentValue, i as RelativeMeasure, l as decimalNumber, m as longHexNumber, n as PositivePercentage, o as ThemeFont, p as hpsMeasureValue, r as PositiveUniversalMeasure, s as UniversalMeasure, t as Percentage, u as eighthPointMeasureValue, v as positiveUniversalMeasureValue, w as universalMeasureValue, x as signedTwipsMeasureValue, y as shortHexNumber } from "./values-QyWq4U4A.mjs";
2
2
  export { Percentage, PositivePercentage, PositiveUniversalMeasure, RelativeMeasure, ThemeColor, ThemeFont, UniversalMeasure, dateTimeValue, decimalNumber, eighthPointMeasureValue, hexBinary, hexColorValue, hpsMeasureValue, longHexNumber, measurementOrPercentValue, percentageValue, pointMeasureValue, positiveUniversalMeasureValue, shortHexNumber, signedHpsMeasureValue, signedTwipsMeasureValue, twipsMeasureValue, uCharHexNumber, universalMeasureValue, unsignedDecimalNumber };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-open/core",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Shared OOXML infrastructure: XmlComponent, value validators, unit converters",
5
5
  "keywords": [
6
6
  "core",
@@ -49,17 +49,13 @@
49
49
  "./drawingml": {
50
50
  "types": "./dist/drawingml/index.d.mts",
51
51
  "import": "./dist/drawingml/index.mjs"
52
- },
53
- "./archive": {
54
- "types": "./dist/archive.d.mts",
55
- "import": "./dist/archive.mjs"
56
52
  }
57
53
  },
58
54
  "dependencies": {
59
55
  "fflate": "0.8.3",
60
56
  "hash.js": "1.1.7",
61
57
  "nanoid": "5.1.11",
62
- "@office-open/xml": "0.6.4"
58
+ "@office-open/xml": "0.6.5"
63
59
  },
64
60
  "scripts": {
65
61
  "dev": "basis build --stub",
@@ -1,58 +0,0 @@
1
- import { Element } from "@office-open/xml";
2
-
3
- //#region src/archive.d.ts
4
- /**
5
- * Unzip an OOXML file (.docx, .pptx) into a Map of path → Uint8Array.
6
- */
7
- declare function unzipToMap(data: Uint8Array): Map<string, Uint8Array>;
8
- /**
9
- * Read a file from the zip as a UTF-8 string.
10
- */
11
- declare function readTextFromZip(zip: Map<string, Uint8Array>, path: string): string | undefined;
12
- /**
13
- * Parse an XML file from the zip into an Element tree.
14
- */
15
- declare function readXmlFromZip(zip: Map<string, Uint8Array>, path: string): Element | undefined;
16
- /**
17
- * Read a binary file from the zip.
18
- */
19
- declare function readBinaryFromZip(zip: Map<string, Uint8Array>, path: string): Uint8Array | undefined;
20
- /**
21
- * Parse all XML files in the zip into Element trees.
22
- * Skips media files, binary files, and the main document/presentation file.
23
- */
24
- declare function readAllXmlParts(zip: Map<string, Uint8Array>, options?: {
25
- skipPaths?: string[];
26
- }): Record<string, Element>;
27
- /**
28
- * List all files in the zip matching a prefix.
29
- */
30
- declare function listFiles(zip: Map<string, Uint8Array>, prefix: string): string[];
31
- /**
32
- * Convert Uint8Array to base64 string.
33
- */
34
- declare function uint8ToBase64(data: Uint8Array): string;
35
- /**
36
- * Determine image type from file extension.
37
- */
38
- declare function getImageType(fileName: string): string;
39
- interface Relationship {
40
- id: string;
41
- target: string;
42
- type: string;
43
- targetMode?: string;
44
- }
45
- declare function parseRels(zip: Map<string, Uint8Array>, path: string): Relationship[];
46
- declare function findRel(rels: Relationship[], id: string): Relationship | undefined;
47
- declare function findRelsByType(rels: Relationship[], typeSubstring: string): Relationship[];
48
- /**
49
- * Zip a map of path → Uint8Array/string into a ZIP buffer.
50
- * XML strings are auto-encoded to UTF-8 bytes.
51
- */
52
- declare function zipToBuffer(files: Map<string, Uint8Array | string>): Uint8Array;
53
- /**
54
- * Serialize an Element tree to an XML string.
55
- */
56
- declare function elementToXml(el: Element): string;
57
- //#endregion
58
- export { Relationship, elementToXml, findRel, findRelsByType, getImageType, listFiles, parseRels, readAllXmlParts, readBinaryFromZip, readTextFromZip, readXmlFromZip, uint8ToBase64, unzipToMap, zipToBuffer };
package/dist/archive.mjs DELETED
@@ -1,135 +0,0 @@
1
- import { attr, js2xml, xml2js } from "@office-open/xml";
2
- import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
3
- //#region src/archive.ts
4
- const XML_PARSE_OPTIONS = {
5
- nativeTypeAttributes: true,
6
- captureSpacesBetweenElements: true
7
- };
8
- /**
9
- * Unzip an OOXML file (.docx, .pptx) into a Map of path → Uint8Array.
10
- */
11
- function unzipToMap(data) {
12
- const entries = unzipSync(data);
13
- const map = /* @__PURE__ */ new Map();
14
- for (const [path, bytes] of Object.entries(entries)) map.set(path, bytes);
15
- return map;
16
- }
17
- /**
18
- * Read a file from the zip as a UTF-8 string.
19
- */
20
- function readTextFromZip(zip, path) {
21
- const data = zip.get(path);
22
- if (data === void 0) return void 0;
23
- return strFromU8(data);
24
- }
25
- /**
26
- * Parse an XML file from the zip into an Element tree.
27
- */
28
- function readXmlFromZip(zip, path) {
29
- const text = readTextFromZip(zip, path);
30
- if (text === void 0) return void 0;
31
- return xml2js(text, XML_PARSE_OPTIONS).elements?.find((e) => e.type === "element");
32
- }
33
- /**
34
- * Read a binary file from the zip.
35
- */
36
- function readBinaryFromZip(zip, path) {
37
- return zip.get(path);
38
- }
39
- /**
40
- * Parse all XML files in the zip into Element trees.
41
- * Skips media files, binary files, and the main document/presentation file.
42
- */
43
- function readAllXmlParts(zip, options) {
44
- const parts = {};
45
- const skip = new Set(options?.skipPaths ?? []);
46
- for (const path of zip.keys()) {
47
- if (skip.has(path)) continue;
48
- if (path.startsWith("word/media/") || path.startsWith("ppt/media/") || path.startsWith("xl/media/") || path.endsWith(".png") || path.endsWith(".jpg") || path.endsWith(".jpeg") || path.endsWith(".gif") || path.endsWith(".bmp") || path.endsWith(".tif") || path.endsWith(".tiff") || path.endsWith(".emf") || path.endsWith(".wmf") || path.endsWith(".svg") || path.endsWith(".wav") || path.endsWith(".mp3") || path.endsWith(".mp4") || path.endsWith(".avi") || path.endsWith(".wmv") || path.endsWith(".thmx") || path.endsWith(".bin")) continue;
49
- const el = readXmlFromZip(zip, path);
50
- if (el) parts[path] = el;
51
- }
52
- return parts;
53
- }
54
- /**
55
- * List all files in the zip matching a prefix.
56
- */
57
- function listFiles(zip, prefix) {
58
- const result = [];
59
- for (const path of zip.keys()) if (path.startsWith(prefix)) result.push(path);
60
- return result;
61
- }
62
- /**
63
- * Convert Uint8Array to base64 string.
64
- */
65
- function uint8ToBase64(data) {
66
- const chunkSize = 8192;
67
- let binary = "";
68
- for (let i = 0; i < data.length; i += chunkSize) {
69
- const chunk = data.subarray(i, Math.min(i + chunkSize, data.length));
70
- binary += String.fromCharCode(...chunk);
71
- }
72
- return btoa(binary);
73
- }
74
- /**
75
- * Determine image type from file extension.
76
- */
77
- function getImageType(fileName) {
78
- const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
79
- if ([
80
- "png",
81
- "jpg",
82
- "jpeg",
83
- "gif",
84
- "bmp",
85
- "tif",
86
- "tiff",
87
- "ico",
88
- "emf",
89
- "wmf",
90
- "svg"
91
- ].includes(ext)) return ext === "jpeg" ? "jpg" : ext;
92
- return "png";
93
- }
94
- function parseRels(zip, path) {
95
- const xml = readXmlFromZip(zip, path);
96
- if (!xml) return [];
97
- const result = [];
98
- for (const rel of xml.elements ?? []) {
99
- if (rel.name !== "Relationship") continue;
100
- const id = attr(rel, "Id");
101
- const target = attr(rel, "Target");
102
- const type = attr(rel, "Type");
103
- const targetMode = attr(rel, "TargetMode");
104
- if (id && target) result.push({
105
- id,
106
- target,
107
- type: type ?? "",
108
- ...targetMode ? { targetMode } : {}
109
- });
110
- }
111
- return result;
112
- }
113
- function findRel(rels, id) {
114
- return rels.find((r) => r.id === id);
115
- }
116
- function findRelsByType(rels, typeSubstring) {
117
- return rels.filter((r) => r.type.includes(typeSubstring));
118
- }
119
- /**
120
- * Zip a map of path → Uint8Array/string into a ZIP buffer.
121
- * XML strings are auto-encoded to UTF-8 bytes.
122
- */
123
- function zipToBuffer(files) {
124
- const entries = {};
125
- for (const [path, data] of files) entries[path] = typeof data === "string" ? strToU8(data) : data;
126
- return zipSync(entries);
127
- }
128
- /**
129
- * Serialize an Element tree to an XML string.
130
- */
131
- function elementToXml(el) {
132
- return js2xml(el);
133
- }
134
- //#endregion
135
- export { elementToXml, findRel, findRelsByType, getImageType, listFiles, parseRels, readAllXmlParts, readBinaryFromZip, readTextFromZip, readXmlFromZip, uint8ToBase64, unzipToMap, zipToBuffer };