@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199

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 (80) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  3. package/src/lib/app-utils/docs-help.generated.js +1 -0
  4. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  5. package/src/lib/app-utils/docs-index.generated.js +3 -3
  6. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  7. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  8. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  9. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  10. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  11. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  12. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  13. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  14. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  15. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  16. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  17. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  18. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  19. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  20. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  21. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  22. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  23. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  24. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  25. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  26. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  27. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  28. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  29. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  30. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  31. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  32. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  33. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  35. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  36. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  38. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  39. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  41. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  42. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  44. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  45. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  47. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  48. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  50. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  51. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  53. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  54. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  56. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  57. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  59. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  60. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  62. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  63. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  65. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  66. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  68. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  69. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  70. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  71. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  72. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  73. package/src/lib/app-utils/name-search.d.ts +12 -0
  74. package/src/lib/app-utils/name-search.js +11 -0
  75. package/src/lib/app-utils/name-search.js.map +1 -1
  76. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  77. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  78. package/src/server.d.ts +1 -0
  79. package/src/server.js +7 -0
  80. package/src/server.js.map +1 -1
@@ -0,0 +1,1058 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * The document properties of a Word, Excel or PowerPoint file (AGL-3331):
19
+ * read from, and written back into, the package's property parts.
20
+ *
21
+ * An Office file is an Open Packaging Conventions package — a ZIP whose
22
+ * parts are found through relationships (ECMA-376 Part 2). Three parts carry
23
+ * what Explorer, Finder and Office's own Info pane show:
24
+ *
25
+ * - core properties (ECMA-376 Part 2, "Core Properties"): title, subject,
26
+ * author, keywords, comments, category, last modified by, the two dates.
27
+ * Readable and writable.
28
+ * - extended properties (Part 1 §22.2): the application, the company, the
29
+ * page or slide count. Read-only — the application recomputes them.
30
+ * - custom properties (Part 1 §22.3): named, typed values. Read; a text one
31
+ * can be edited or removed, not added.
32
+ *
33
+ * Each part is found through the package relationships in `_rels/.rels`,
34
+ * never by its usual name — `docProps/core.xml` is a convention, not a rule.
35
+ *
36
+ * ## Lossless writing
37
+ *
38
+ * An edited part is changed as TEXT at the exact offsets the parser
39
+ * recorded: one element's content replaced, one element removed, one
40
+ * inserted before the root's end tag. Every other byte of the part stays as
41
+ * it was, and every other part of the package is copied through byte for
42
+ * byte by `rewriteZip` — the document, the images, the styles, the
43
+ * signatures' parts.
44
+ *
45
+ * ## The XML parser
46
+ *
47
+ * A small, strict, non-validating one (`scanXml`, `parseXml`), because the
48
+ * parts are tiny and the only safe DTD is none: OPC forbids DTD declarations
49
+ * in package XML, so a DOCTYPE here is refused outright rather than
50
+ * skipped, and no entity beyond the five predefined ones and character
51
+ * references is ever expanded. `svg.ts` reuses it, allowing an inert
52
+ * external-id DOCTYPE and nothing more.
53
+ */ import { embeddedFieldLabel, otherEmbeddedKey, parseOtherEmbeddedKey } from "../media-embedded-fields.js";
54
+ import { findZipEntry, readZip, readZipEntry, rewriteZip } from "./zip.js";
55
+ import { EmbeddedWriteError } from "./types.js";
56
+ // ---------------------------------------------------------------------------
57
+ // XML
58
+ // ---------------------------------------------------------------------------
59
+ const NS_XML = 'http://www.w3.org/XML/1998/namespace';
60
+ const NS_XMLNS = 'http://www.w3.org/2000/xmlns/';
61
+ const XML_ENTITY = /&(?:#x([0-9a-fA-F]{1,6})|#([0-9]{1,7})|(amp|lt|gt|quot|apos));/g;
62
+ const PREDEFINED = {
63
+ amp: '&',
64
+ lt: '<',
65
+ gt: '>',
66
+ quot: '"',
67
+ apos: "'"
68
+ };
69
+ /** XML 1.0 §2.2 `Char`. */ function isXmlChar(code) {
70
+ return code === 0x9 || code === 0xa || code === 0xd || code >= 0x20 && code <= 0xd7ff || code >= 0xe000 && code <= 0xfffd || code >= 0x10000 && code <= 0x10ffff;
71
+ }
72
+ /**
73
+ * Expand the five predefined entities and character references (XML 1.0
74
+ * §4.1, §4.6). Anything else — an undeclared entity, a stray `&` — is left
75
+ * as written: there is no DTD to define it, and guessing is worse.
76
+ */ function decodeEntities(raw) {
77
+ if (!raw.includes('&')) return raw;
78
+ return raw.replace(XML_ENTITY, (whole, hex, dec, name)=>{
79
+ var _PREDEFINED_name;
80
+ if (name) return (_PREDEFINED_name = PREDEFINED[name]) != null ? _PREDEFINED_name : whole;
81
+ const code = hex ? parseInt(hex, 16) : parseInt(dec != null ? dec : '', 10);
82
+ return isXmlChar(code) ? String.fromCodePoint(code) : whole;
83
+ });
84
+ }
85
+ /** Character data: line ends normalized (§2.11), then entities. */ export function decodeXmlText(raw) {
86
+ return decodeEntities(raw.includes('\r') ? raw.replace(/\r\n?/g, '\n') : raw);
87
+ }
88
+ /** An attribute value, normalized as §3.3.3 does for CDATA attributes. */ function decodeXmlAttribute(raw) {
89
+ return decodeEntities(raw.replace(/\r\n|[\t\n\r]/g, ' '));
90
+ }
91
+ /** Characters XML 1.0 cannot carry at all, even escaped. */ const NOT_XML_CHAR = // eslint-disable-next-line no-control-regex
92
+ /[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
93
+ /** Escape text content; drops what XML cannot represent. */ export function escapeXmlText(value) {
94
+ return value.replace(NOT_XML_CHAR, '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
95
+ }
96
+ /** Escape a double-quoted attribute value. */ export function escapeXmlAttribute(value) {
97
+ return escapeXmlText(value).replace(/"/g, '&quot;').replace(/\t/g, '&#9;').replace(/\n/g, '&#10;').replace(/\r/g, '&#13;');
98
+ }
99
+ const isSpace = (code)=>code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d;
100
+ /** A name runs until whitespace or markup punctuation. */ const endsName = (code)=>isSpace(code) || code === 0x2f || // /
101
+ code === 0x3e || // >
102
+ code === 0x3d || // =
103
+ code === 0x3c || // <
104
+ code === 0x22 || // "
105
+ code === 0x27 || // '
106
+ Number.isNaN(code);
107
+ function splitName(name) {
108
+ const colon = name.indexOf(':');
109
+ if (colon < 0) return {
110
+ prefix: null,
111
+ local: name
112
+ };
113
+ if (colon === 0 || colon === name.length - 1 || name.indexOf(':', colon + 1) >= 0) return null;
114
+ return {
115
+ prefix: name.slice(0, colon),
116
+ local: name.slice(colon + 1)
117
+ };
118
+ }
119
+ /**
120
+ * Walk a document start to end, reporting elements and text.
121
+ *
122
+ * Returns false — having possibly reported a prefix of the document — on
123
+ * anything that is not a well-formed, namespace-well-formed document with
124
+ * one root element: mismatched or unclosed tags, a duplicate attribute, an
125
+ * unbound prefix, text outside the root, a refused DOCTYPE or `<!ENTITY>`,
126
+ * or a bound exceeded. Never throws.
127
+ */ export function scanXml(source, handlers, options = {}) {
128
+ var _options_maxDepth, _options_maxElements;
129
+ const maxDepth = (_options_maxDepth = options.maxDepth) != null ? _options_maxDepth : 256;
130
+ const maxElements = (_options_maxElements = options.maxElements) != null ? _options_maxElements : 1000000;
131
+ const baseNamespaces = new Map([
132
+ [
133
+ 'xml',
134
+ NS_XML
135
+ ]
136
+ ]);
137
+ const stack = [];
138
+ const n = source.length;
139
+ let rootSeen = false;
140
+ let elements = 0;
141
+ // A byte-order mark decoded into the string is not content.
142
+ let i = source.charCodeAt(0) === 0xfeff ? 1 : 0;
143
+ while(i < n){
144
+ var _ref;
145
+ const lt = source.indexOf('<', i);
146
+ const textEnd = lt < 0 ? n : lt;
147
+ if (textEnd > i) {
148
+ if (stack.length) {
149
+ handlers.text == null ? void 0 : handlers.text.call(handlers, decodeXmlText(source.slice(i, textEnd)), i, textEnd);
150
+ } else {
151
+ for(let k = i; k < textEnd; k++){
152
+ if (!isSpace(source.charCodeAt(k))) return false;
153
+ }
154
+ }
155
+ }
156
+ if (lt < 0) break;
157
+ if (source.startsWith('<!--', lt)) {
158
+ const end = source.indexOf('-->', lt + 4);
159
+ if (end < 0) return false;
160
+ i = end + 3;
161
+ continue;
162
+ }
163
+ if (source.startsWith('<![CDATA[', lt)) {
164
+ if (!stack.length) return false;
165
+ const end = source.indexOf(']]>', lt + 9);
166
+ if (end < 0) return false;
167
+ handlers.text == null ? void 0 : handlers.text.call(handlers, source.slice(lt + 9, end), lt + 9, end);
168
+ i = end + 3;
169
+ continue;
170
+ }
171
+ if (source.startsWith('<!DOCTYPE', lt)) {
172
+ if (!options.allowExternalDoctype || rootSeen) return false;
173
+ let j = lt + 9;
174
+ let quote = 0;
175
+ for(; j < n; j++){
176
+ const code = source.charCodeAt(j);
177
+ if (quote) {
178
+ if (code === quote) quote = 0;
179
+ } else if (code === 0x22 || code === 0x27) {
180
+ quote = code;
181
+ } else if (code === 0x5b) {
182
+ // `[` opens an internal subset: entity definitions.
183
+ return false;
184
+ } else if (code === 0x3e) {
185
+ break;
186
+ }
187
+ }
188
+ if (j >= n) return false;
189
+ i = j + 1;
190
+ continue;
191
+ }
192
+ // `<!ENTITY`, `<!ELEMENT`, … are only legal inside a DTD.
193
+ if (source.startsWith('<!', lt)) return false;
194
+ if (source.startsWith('<?', lt)) {
195
+ const end = source.indexOf('?>', lt + 2);
196
+ if (end < 0) return false;
197
+ i = end + 2;
198
+ continue;
199
+ }
200
+ if (source.startsWith('</', lt)) {
201
+ const gt = source.indexOf('>', lt + 2);
202
+ if (gt < 0) return false;
203
+ const open = stack.pop();
204
+ if (!open || open.name !== source.slice(lt + 2, gt).trimEnd()) return false;
205
+ handlers.close == null ? void 0 : handlers.close.call(handlers, open, lt, gt + 1);
206
+ i = gt + 1;
207
+ continue;
208
+ }
209
+ // A start tag.
210
+ if (!stack.length && rootSeen) return false;
211
+ if (stack.length >= maxDepth || ++elements > maxElements) return false;
212
+ let j = lt + 1;
213
+ while(j < n && !endsName(source.charCodeAt(j)))j++;
214
+ const name = source.slice(lt + 1, j);
215
+ const parts = splitName(name);
216
+ if (!name || !parts) return false;
217
+ const raw = [];
218
+ const seen = new Set();
219
+ let selfClosing = false;
220
+ for(;;){
221
+ const before = j;
222
+ while(j < n && isSpace(source.charCodeAt(j)))j++;
223
+ if (j >= n) return false;
224
+ const code = source.charCodeAt(j);
225
+ if (code === 0x3e) {
226
+ j++;
227
+ break;
228
+ }
229
+ if (code === 0x2f) {
230
+ if (source.charCodeAt(j + 1) !== 0x3e) return false;
231
+ selfClosing = true;
232
+ j += 2;
233
+ break;
234
+ }
235
+ // Attributes are separated from the name and each other by space.
236
+ if (j === before) return false;
237
+ const nameStart = j;
238
+ while(j < n && !endsName(source.charCodeAt(j)))j++;
239
+ const attributeName = source.slice(nameStart, j);
240
+ if (!attributeName || seen.has(attributeName)) return false;
241
+ seen.add(attributeName);
242
+ while(j < n && isSpace(source.charCodeAt(j)))j++;
243
+ if (source.charCodeAt(j) !== 0x3d) return false;
244
+ j++;
245
+ while(j < n && isSpace(source.charCodeAt(j)))j++;
246
+ const quote = source[j];
247
+ if (quote !== '"' && quote !== "'") return false;
248
+ const valueEnd = source.indexOf(quote, j + 1);
249
+ if (valueEnd < 0) return false;
250
+ const value = source.slice(j + 1, valueEnd);
251
+ if (value.includes('<')) return false;
252
+ raw.push({
253
+ name: attributeName,
254
+ value,
255
+ start: j + 1,
256
+ end: valueEnd
257
+ });
258
+ j = valueEnd + 1;
259
+ }
260
+ const parent = stack.at(-1);
261
+ const inherited = (_ref = parent == null ? void 0 : parent.namespaces) != null ? _ref : baseNamespaces;
262
+ let namespaces = inherited;
263
+ for (const attribute of raw){
264
+ if (attribute.name !== 'xmlns' && !attribute.name.startsWith('xmlns:')) continue;
265
+ if (namespaces === inherited) namespaces = new Map(inherited);
266
+ const prefix = attribute.name === 'xmlns' ? '' : attribute.name.slice(6);
267
+ const uri = decodeXmlAttribute(attribute.value);
268
+ const map = namespaces;
269
+ if (uri) map.set(prefix, uri);
270
+ else if (prefix) return false;
271
+ else map.delete('');
272
+ }
273
+ const ns = parts.prefix === null ? namespaces.get('') : namespaces.get(parts.prefix);
274
+ if (parts.prefix !== null && ns === undefined) return false;
275
+ const attributes = [];
276
+ for (const attribute of raw){
277
+ const split = splitName(attribute.name);
278
+ if (!split) return false;
279
+ let attributeNs = null;
280
+ if (attribute.name === 'xmlns' || split.prefix === 'xmlns') {
281
+ attributeNs = NS_XMLNS;
282
+ } else if (split.prefix !== null) {
283
+ const bound = namespaces.get(split.prefix);
284
+ if (bound === undefined) return false;
285
+ attributeNs = bound;
286
+ }
287
+ attributes.push({
288
+ name: attribute.name,
289
+ prefix: split.prefix,
290
+ local: split.local,
291
+ ns: attributeNs,
292
+ value: decodeXmlAttribute(attribute.value),
293
+ valueStart: attribute.start,
294
+ valueEnd: attribute.end
295
+ });
296
+ }
297
+ const tag = {
298
+ name,
299
+ prefix: parts.prefix,
300
+ local: parts.local,
301
+ ns: ns != null ? ns : null,
302
+ attributes,
303
+ namespaces,
304
+ depth: stack.length,
305
+ start: lt,
306
+ openEnd: j,
307
+ selfClosing
308
+ };
309
+ rootSeen = true;
310
+ handlers.open == null ? void 0 : handlers.open.call(handlers, tag);
311
+ if (selfClosing) handlers.close == null ? void 0 : handlers.close.call(handlers, tag, j, j);
312
+ else stack.push(tag);
313
+ i = j;
314
+ }
315
+ return rootSeen && stack.length === 0;
316
+ }
317
+ /** Parse a whole document into a tree. Null when `scanXml` refuses it. */ export function parseXml(source, options = {}) {
318
+ const state = {
319
+ root: null,
320
+ stack: []
321
+ };
322
+ const ok = scanXml(source, {
323
+ open (tag) {
324
+ var _state_stack_at, _state, _root;
325
+ const parent = (_state_stack_at = state.stack.at(-1)) != null ? _state_stack_at : null;
326
+ const element = _extends({}, tag, {
327
+ parent,
328
+ children: [],
329
+ content: [],
330
+ closeStart: tag.openEnd,
331
+ end: tag.openEnd
332
+ });
333
+ parent == null ? void 0 : parent.children.push(element);
334
+ parent == null ? void 0 : parent.content.push(element);
335
+ (_root = (_state = state).root) != null ? _root : _state.root = element;
336
+ state.stack.push(element);
337
+ },
338
+ close (_tag, closeStart, end) {
339
+ const element = state.stack.pop();
340
+ if (!element) return;
341
+ element.closeStart = closeStart;
342
+ element.end = end;
343
+ },
344
+ text (text) {
345
+ var _state_stack_at;
346
+ (_state_stack_at = state.stack.at(-1)) == null ? void 0 : _state_stack_at.content.push(text);
347
+ }
348
+ }, options);
349
+ return ok ? state.root : null;
350
+ }
351
+ /** Every character of text inside an element, descendants included. */ export function xmlTextContent(element) {
352
+ return element.content.map((node)=>typeof node === 'string' ? node : xmlTextContent(node)).join('');
353
+ }
354
+ /** An attribute's value by local name and namespace (null = no namespace). */ export function xmlAttribute(element, local, ns = null) {
355
+ return element.attributes.find((attribute)=>attribute.local === local && attribute.ns === ns);
356
+ }
357
+ /** A prefix bound to `ns` in scope at `element`; `''` for the default. */ function prefixFor(element, ns) {
358
+ let fallback;
359
+ for (const [prefix, uri] of element.namespaces){
360
+ if (uri !== ns) continue;
361
+ if (prefix) return prefix;
362
+ fallback = prefix;
363
+ }
364
+ return fallback;
365
+ }
366
+ function applyEdits(source, edits) {
367
+ const ordered = [
368
+ ...edits
369
+ ].sort((a, b)=>b.start - a.start || b.end - a.end);
370
+ let out = source;
371
+ let floor = Infinity;
372
+ for (const edit of ordered){
373
+ if (edit.end > floor) {
374
+ throw new EmbeddedWriteError('The document properties could not be edited safely.');
375
+ }
376
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
377
+ floor = edit.start;
378
+ }
379
+ return out;
380
+ }
381
+ // ---------------------------------------------------------------------------
382
+ // Package
383
+ // ---------------------------------------------------------------------------
384
+ const NS_CP = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties';
385
+ const NS_DC = 'http://purl.org/dc/elements/1.1/';
386
+ const NS_DCTERMS = 'http://purl.org/dc/terms/';
387
+ const NS_DCMITYPE = 'http://purl.org/dc/dcmitype/';
388
+ const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
389
+ /** Transitional and Strict (ISO/IEC 29500) spellings of the same idea. */ const NS_EXTENDED = new Set([
390
+ 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
391
+ 'http://purl.oclc.org/ooxml/officeDocument/extendedProperties'
392
+ ]);
393
+ const NS_CUSTOM = new Set([
394
+ 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties',
395
+ 'http://purl.oclc.org/ooxml/officeDocument/customProperties'
396
+ ]);
397
+ const NS_VT = new Set([
398
+ 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes',
399
+ 'http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes'
400
+ ]);
401
+ const NS_RELATIONSHIPS = 'http://schemas.openxmlformats.org/package/2006/relationships';
402
+ const NS_CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
403
+ const CONTENT_TYPES_PART = '[Content_Types].xml';
404
+ const ROOT_RELATIONSHIPS_PART = '_rels/.rels';
405
+ const CORE_RELATIONSHIP = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties';
406
+ const CORE_CONTENT_TYPE = 'application/vnd.openxmlformats-package.core-properties+xml';
407
+ const DEFAULT_CORE_PART = 'docProps/core.xml';
408
+ /** A property part is a few KB; this is generous and still bounded. */ const PART_MAX_BYTES = 8 * 1024 * 1024;
409
+ function roleOf(type) {
410
+ const tail = type.toLowerCase().replace(/-/g, '');
411
+ if (tail.endsWith('/metadata/coreproperties')) return 'core';
412
+ if (tail.endsWith('/extendedproperties')) return 'extended';
413
+ if (tail.endsWith('/customproperties')) return 'custom';
414
+ return null;
415
+ }
416
+ /** Resolve a root relationship's target to a ZIP entry name (Part 2 §9.3). */ function resolveTarget(target) {
417
+ var _ref;
418
+ var _target_split_;
419
+ const segments = [];
420
+ for (const segment of (_ref = (_target_split_ = target.split(/[?#]/)[0]) == null ? void 0 : _target_split_.split('/')) != null ? _ref : []){
421
+ if (!segment || segment === '.') continue;
422
+ if (segment === '..') segments.pop();
423
+ else segments.push(segment);
424
+ }
425
+ return segments.join('/');
426
+ }
427
+ /**
428
+ * A part by name. Part names compare case-insensitively (Part 2 §9.1.1.1),
429
+ * and a target may be percent-encoded where the ZIP item name is not.
430
+ */ function findPart(archive, name) {
431
+ const exact = findZipEntry(archive, name);
432
+ if (exact) return exact;
433
+ let decoded = name;
434
+ try {
435
+ decoded = decodeURIComponent(name);
436
+ } catch (unused) {
437
+ // Not percent-encoded after all.
438
+ }
439
+ const wanted = [
440
+ name.toLowerCase(),
441
+ decoded.toLowerCase()
442
+ ];
443
+ return archive.entries.find((entry)=>wanted.includes(entry.name.toLowerCase()));
444
+ }
445
+ /**
446
+ * Decode a part for reading. OPC allows UTF-8 and UTF-16 (Part 2, "XML
447
+ * Usage"); the byte-order mark decides, and UTF-8 is the default.
448
+ */ function decodeForRead(bytes) {
449
+ if (bytes[0] === 0xfe && bytes[1] === 0xff) return new TextDecoder('utf-16be').decode(bytes);
450
+ if (bytes[0] === 0xff && bytes[1] === 0xfe) return new TextDecoder('utf-16le').decode(bytes);
451
+ return new TextDecoder('utf-8').decode(bytes);
452
+ }
453
+ /**
454
+ * Decode a part for editing: strict UTF-8 only, so that re-encoding the
455
+ * untouched text reproduces the untouched bytes exactly.
456
+ */ function decodeForEdit(bytes) {
457
+ if (bytes[0] === 0xfe && bytes[1] === 0xff || bytes[0] === 0xff && bytes[1] === 0xfe) {
458
+ throw new EmbeddedWriteError('This file stores its properties as UTF-16, which cannot be edited here.');
459
+ }
460
+ const bom = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
461
+ try {
462
+ return {
463
+ text: new TextDecoder('utf-8', {
464
+ fatal: true,
465
+ ignoreBOM: true
466
+ }).decode(bom ? bytes.subarray(3) : bytes),
467
+ bom
468
+ };
469
+ } catch (unused) {
470
+ throw new EmbeddedWriteError('The properties in this file are not valid text.');
471
+ }
472
+ }
473
+ function encodePart(part) {
474
+ const body = new TextEncoder().encode(part.text);
475
+ if (!part.bom) return body;
476
+ const out = new Uint8Array(body.length + 3);
477
+ out.set([
478
+ 0xef,
479
+ 0xbb,
480
+ 0xbf
481
+ ]);
482
+ out.set(body, 3);
483
+ return out;
484
+ }
485
+ function readPartText(archive, entry) {
486
+ if (!entry) return null;
487
+ const bytes = readZipEntry(archive, entry, PART_MAX_BYTES);
488
+ return bytes ? decodeForRead(bytes) : null;
489
+ }
490
+ function packageLayout(archive) {
491
+ const relationships = findPart(archive, ROOT_RELATIONSHIPS_PART);
492
+ const parts = {};
493
+ const text = readPartText(archive, relationships);
494
+ const root = text === null ? null : parseXml(text);
495
+ if (!root || root.local !== 'Relationships' || root.ns !== NS_RELATIONSHIPS) {
496
+ return {
497
+ parts,
498
+ relationships
499
+ };
500
+ }
501
+ for (const child of root.children){
502
+ var _ref;
503
+ var _xmlAttribute, _xmlAttribute1, _xmlAttribute2;
504
+ if (child.local !== 'Relationship' || child.ns !== NS_RELATIONSHIPS) continue;
505
+ if (((_xmlAttribute = xmlAttribute(child, 'TargetMode')) == null ? void 0 : _xmlAttribute.value) === 'External') continue;
506
+ const role = roleOf((_ref = (_xmlAttribute1 = xmlAttribute(child, 'Type')) == null ? void 0 : _xmlAttribute1.value) != null ? _ref : '');
507
+ const target = (_xmlAttribute2 = xmlAttribute(child, 'Target')) == null ? void 0 : _xmlAttribute2.value;
508
+ if (role && target && !parts[role]) parts[role] = resolveTarget(target);
509
+ }
510
+ return {
511
+ parts,
512
+ relationships
513
+ };
514
+ }
515
+ /** Where each canonical key lives in the core properties part. */ const CORE_FIELDS = {
516
+ title: {
517
+ ns: NS_DC,
518
+ local: 'title',
519
+ prefix: 'dc'
520
+ },
521
+ subject: {
522
+ ns: NS_DC,
523
+ local: 'subject',
524
+ prefix: 'dc'
525
+ },
526
+ creator: {
527
+ ns: NS_DC,
528
+ local: 'creator',
529
+ prefix: 'dc'
530
+ },
531
+ keywords: {
532
+ ns: NS_CP,
533
+ local: 'keywords',
534
+ prefix: 'cp'
535
+ },
536
+ description: {
537
+ ns: NS_DC,
538
+ local: 'description',
539
+ prefix: 'dc'
540
+ },
541
+ category: {
542
+ ns: NS_CP,
543
+ local: 'category',
544
+ prefix: 'cp'
545
+ },
546
+ lastModifiedBy: {
547
+ ns: NS_CP,
548
+ local: 'lastModifiedBy',
549
+ prefix: 'cp'
550
+ },
551
+ createdAt: {
552
+ ns: NS_DCTERMS,
553
+ local: 'created',
554
+ prefix: 'dcterms'
555
+ },
556
+ modifiedAt: {
557
+ ns: NS_DCTERMS,
558
+ local: 'modified',
559
+ prefix: 'dcterms'
560
+ }
561
+ };
562
+ /** The core keys an edit may set. The dates and last-modified-by are facts. */ const CORE_WRITABLE = new Set([
563
+ 'title',
564
+ 'subject',
565
+ 'creator',
566
+ 'keywords',
567
+ 'description',
568
+ 'category'
569
+ ]);
570
+ const isCoreKey = (key)=>Object.prototype.hasOwnProperty.call(CORE_FIELDS, key);
571
+ const splitList = (value, separator)=>value.split(separator).map((item)=>item.trim()).filter(Boolean);
572
+ /**
573
+ * A W3CDTF date (the profile of ISO 8601 `dcterms:W3CDTF` names) in the
574
+ * catalog's form: seconds kept, a fraction cut to milliseconds, the zone as
575
+ * `Z` or `±HH:MM` and only when one was written. Anything else is returned
576
+ * trimmed, as found.
577
+ */ function normalizeW3cdtf(raw) {
578
+ const value = raw.trim();
579
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2})?)(?:[.,](\d+))?(Z|[+-]\d{2}:\d{2})?$/.exec(value);
580
+ if (!match) return value;
581
+ const [, date, time, fraction, zone] = match;
582
+ return `${date}T${time}${fraction ? `.${fraction.slice(0, 3)}` : ''}${zone != null ? zone : ''}`;
583
+ }
584
+ function readCore(text) {
585
+ const root = parseXml(text);
586
+ if (!root || root.ns !== NS_CP || root.local !== 'coreProperties') return [];
587
+ const out = [];
588
+ const creators = [];
589
+ const keywords = [];
590
+ const seen = new Set();
591
+ for (const child of root.children){
592
+ const key = Object.keys(CORE_FIELDS).find((candidate)=>CORE_FIELDS[candidate].ns === child.ns && CORE_FIELDS[candidate].local === child.local);
593
+ if (!key) continue;
594
+ const value = xmlTextContent(child).trim();
595
+ if (!value) continue;
596
+ // Office writes several authors into one `dc:creator`, joined by `;`.
597
+ if (key === 'creator') creators.push(...splitList(value, /;/));
598
+ else if (key === 'keywords') keywords.push(...splitList(value, /[,;]/));
599
+ else if (!seen.has(key)) {
600
+ seen.add(key);
601
+ out.push({
602
+ key,
603
+ value: key === 'createdAt' || key === 'modifiedAt' ? normalizeW3cdtf(value) : value,
604
+ source: 'ooxml'
605
+ });
606
+ }
607
+ }
608
+ if (creators.length) out.push({
609
+ key: 'creator',
610
+ value: creators,
611
+ source: 'ooxml'
612
+ });
613
+ if (keywords.length) out.push({
614
+ key: 'keywords',
615
+ value: keywords,
616
+ source: 'ooxml'
617
+ });
618
+ return out;
619
+ }
620
+ function readExtended(text) {
621
+ const root = parseXml(text);
622
+ if (!root || !root.ns || !NS_EXTENDED.has(root.ns) || root.local !== 'Properties') return [];
623
+ const value = (local)=>{
624
+ const child = root.children.find((element)=>element.local === local && element.ns === root.ns);
625
+ return child ? xmlTextContent(child).trim() : '';
626
+ };
627
+ const out = [];
628
+ const application = value('Application');
629
+ if (application) out.push({
630
+ key: 'software',
631
+ value: application,
632
+ source: 'ooxml'
633
+ });
634
+ const company = value('Company');
635
+ if (company) out.push({
636
+ key: 'company',
637
+ value: company,
638
+ source: 'ooxml'
639
+ });
640
+ // Word counts pages and PowerPoint slides; a workbook has neither.
641
+ const count = [
642
+ value('Pages'),
643
+ value('Slides')
644
+ ].find((item)=>/^\d+$/.test(item));
645
+ if (count) out.push({
646
+ key: 'pageCount',
647
+ value: count,
648
+ source: 'ooxml'
649
+ });
650
+ return out;
651
+ }
652
+ /** Text-valued variant types — the only ones an edit can write (§22.4). */ const VT_TEXT = new Set([
653
+ 'lpwstr',
654
+ 'lpstr'
655
+ ]);
656
+ const VT_READ_ONLY_TEXT = new Set([
657
+ 'bstr'
658
+ ]);
659
+ const VT_NUMBER = new Set([
660
+ 'i1',
661
+ 'i2',
662
+ 'i4',
663
+ 'i8',
664
+ 'int',
665
+ 'ui1',
666
+ 'ui2',
667
+ 'ui4',
668
+ 'ui8',
669
+ 'uint',
670
+ 'r4',
671
+ 'r8',
672
+ 'decimal'
673
+ ]);
674
+ const VT_DATE = new Set([
675
+ 'filetime',
676
+ 'date'
677
+ ]);
678
+ /** A custom property's name, from its `name` attribute. */ const propertyName = (property)=>{
679
+ var _ref;
680
+ var _xmlAttribute;
681
+ return (_ref = (_xmlAttribute = xmlAttribute(property, 'name')) == null ? void 0 : _xmlAttribute.value) != null ? _ref : '';
682
+ };
683
+ /** The one `vt:` value child of a custom `property` (§22.3.2.2). */ const propertyValue = (property)=>property.children.find((child)=>child.ns !== null && NS_VT.has(child.ns));
684
+ function customProperties(root) {
685
+ return root.children.filter((child)=>child.local === 'property' && child.ns === root.ns);
686
+ }
687
+ function readCustom(text) {
688
+ const root = parseXml(text);
689
+ if (!root || !root.ns || !NS_CUSTOM.has(root.ns) || root.local !== 'Properties') return [];
690
+ const out = [];
691
+ const seen = new Set();
692
+ for (const property of customProperties(root)){
693
+ const name = propertyName(property);
694
+ const valueElement = propertyValue(property);
695
+ if (!name || !valueElement || seen.has(name)) continue;
696
+ const type = valueElement.local;
697
+ const raw = xmlTextContent(valueElement);
698
+ let value;
699
+ if (VT_TEXT.has(type) || VT_READ_ONLY_TEXT.has(type)) value = raw;
700
+ else if (VT_NUMBER.has(type)) value = raw.trim();
701
+ else if (type === 'bool') {
702
+ const flag = raw.trim().toLowerCase();
703
+ value = flag === '1' ? 'true' : flag === '0' ? 'false' : flag;
704
+ } else if (VT_DATE.has(type)) value = normalizeW3cdtf(raw);
705
+ else continue;
706
+ if (!value.trim()) continue;
707
+ seen.add(name);
708
+ out.push(_extends({
709
+ key: otherEmbeddedKey('ooxml', name),
710
+ value,
711
+ source: 'ooxml',
712
+ label: name
713
+ }, VT_TEXT.has(type) ? {} : {
714
+ editable: false
715
+ }));
716
+ }
717
+ return out;
718
+ }
719
+ /**
720
+ * Read an Office file's document properties.
721
+ *
722
+ * Null when the bytes are not an OPC package — not a ZIP, or a ZIP with no
723
+ * `[Content_Types].xml`. A package whose property parts are missing or
724
+ * unreadable reads as an empty list: a damaged part costs its own fields,
725
+ * not the others'. A part with a DOCTYPE is unreadable by design.
726
+ */ export function readOoxml(bytes) {
727
+ const archive = readZip(bytes);
728
+ if (!archive || !findPart(archive, CONTENT_TYPES_PART)) return null;
729
+ const { parts } = packageLayout(archive);
730
+ const candidates = [];
731
+ const part = (role)=>{
732
+ const name = parts[role];
733
+ return name === undefined ? null : readPartText(archive, findPart(archive, name));
734
+ };
735
+ const core = part('core');
736
+ if (core !== null) candidates.push(...readCore(core));
737
+ const extended = part('extended');
738
+ if (extended !== null) candidates.push(...readExtended(extended));
739
+ const custom = part('custom');
740
+ if (custom !== null) candidates.push(...readCustom(custom));
741
+ return {
742
+ candidates
743
+ };
744
+ }
745
+ // ---------------------------------------------------------------------------
746
+ // Writing
747
+ // ---------------------------------------------------------------------------
748
+ /** Office's own serialization of `dcterms:modified`: seconds, UTC. */ const w3cdtfNow = (now)=>now.toISOString().replace(/\.\d{3}Z$/, 'Z');
749
+ function patchText(key, value) {
750
+ if (!Array.isArray(value)) return value.trim();
751
+ const items = value.map((item)=>item.trim()).filter(Boolean);
752
+ // Office joins authors with ";" and keywords with ","; both read back.
753
+ return items.join(key === 'creator' ? '; ' : ', ');
754
+ }
755
+ function parsePart(text) {
756
+ const root = parseXml(text);
757
+ if (!root) {
758
+ throw new EmbeddedWriteError('The document properties in this file are damaged, so they cannot be edited.');
759
+ }
760
+ return root;
761
+ }
762
+ /**
763
+ * The qualified name, and any declaration it needs, for a new element or
764
+ * attribute. An attribute cannot use the default namespace — unprefixed, it
765
+ * would be in none — so it always gets a prefix.
766
+ */ function qualify(scope, ns, local, conventional, attribute = false) {
767
+ const prefix = prefixFor(scope, ns);
768
+ if (prefix !== undefined && !(attribute && prefix === '')) {
769
+ return {
770
+ name: prefix ? `${prefix}:${local}` : local,
771
+ declare: ''
772
+ };
773
+ }
774
+ return {
775
+ name: `${conventional}:${local}`,
776
+ declare: ` xmlns:${conventional}="${escapeXmlAttribute(ns)}"`
777
+ };
778
+ }
779
+ /** Replace an element's content, expanding it first if it was self-closing. */ function replaceContent(source, element, escaped) {
780
+ if (!element.selfClosing) {
781
+ return {
782
+ start: element.openEnd,
783
+ end: element.closeStart,
784
+ text: escaped
785
+ };
786
+ }
787
+ const openTag = source.slice(element.start, element.openEnd).replace(/\s*\/>$/, '>');
788
+ return {
789
+ start: element.start,
790
+ end: element.end,
791
+ text: `${openTag}${escaped}</${element.name}>`
792
+ };
793
+ }
794
+ /** Insert markup as the last content of `root`. */ function appendTo(source, root, markup) {
795
+ if (!root.selfClosing) {
796
+ return {
797
+ start: root.closeStart,
798
+ end: root.closeStart,
799
+ text: markup
800
+ };
801
+ }
802
+ const openTag = source.slice(root.start, root.openEnd).replace(/\s*\/>$/, '>');
803
+ return {
804
+ start: root.start,
805
+ end: root.end,
806
+ text: `${openTag}${markup}</${root.name}>`
807
+ };
808
+ }
809
+ function editCore(source, values, now) {
810
+ const root = parsePart(source);
811
+ if (root.ns !== NS_CP || root.local !== 'coreProperties') {
812
+ throw new EmbeddedWriteError('The document properties in this file are damaged, so they cannot be edited.');
813
+ }
814
+ const edits = [];
815
+ const appended = [];
816
+ const all = new Map(values);
817
+ all.set('modifiedAt', w3cdtfNow(now));
818
+ for (const [key, value] of all){
819
+ const field = CORE_FIELDS[key];
820
+ const found = root.children.filter((child)=>child.ns === field.ns && child.local === field.local);
821
+ const [first, ...rest] = found;
822
+ if (value === null) {
823
+ for (const element of found){
824
+ edits.push({
825
+ start: element.start,
826
+ end: element.end,
827
+ text: ''
828
+ });
829
+ }
830
+ continue;
831
+ }
832
+ const escaped = escapeXmlText(value);
833
+ if (first) {
834
+ edits.push(replaceContent(source, first, escaped));
835
+ // One value per property: a duplicate would be read instead of it.
836
+ for (const element of rest){
837
+ edits.push({
838
+ start: element.start,
839
+ end: element.end,
840
+ text: ''
841
+ });
842
+ }
843
+ continue;
844
+ }
845
+ const { name, declare } = qualify(root, field.ns, field.local, field.prefix);
846
+ let attributes = declare;
847
+ if (key === 'modifiedAt') {
848
+ // `xsi:type` names W3CDTF by QName, so its prefix must be in scope.
849
+ const xsi = qualify(root, NS_XSI, 'type', 'xsi', true);
850
+ const dctermsPrefix = name.includes(':') ? name.slice(0, name.indexOf(':')) : '';
851
+ attributes += `${xsi.declare} ${xsi.name}="${dctermsPrefix ? `${dctermsPrefix}:` : ''}W3CDTF"`;
852
+ }
853
+ appended.push(`<${name}${attributes}>${escaped}</${name}>`);
854
+ }
855
+ if (appended.length) edits.push(appendTo(source, root, appended.join('')));
856
+ return applyEdits(source, edits);
857
+ }
858
+ function editCustom(source, values) {
859
+ const root = parsePart(source);
860
+ if (!root.ns || !NS_CUSTOM.has(root.ns) || root.local !== 'Properties') {
861
+ throw new EmbeddedWriteError('The custom properties in this file are damaged, so they cannot be edited.');
862
+ }
863
+ const properties = customProperties(root);
864
+ const edits = [];
865
+ const removed = new Set();
866
+ for (const [name, value] of values){
867
+ const property = properties.find((element)=>propertyName(element) === name);
868
+ if (!property) {
869
+ throw new EmbeddedWriteError(`"${name}" is not a custom property of this file.`);
870
+ }
871
+ if (value === null) {
872
+ edits.push({
873
+ start: property.start,
874
+ end: property.end,
875
+ text: ''
876
+ });
877
+ removed.add(property);
878
+ continue;
879
+ }
880
+ const valueElement = propertyValue(property);
881
+ if (!valueElement || !VT_TEXT.has(valueElement.local)) {
882
+ throw new EmbeddedWriteError(`"${name}" is not a text property, so it cannot be edited here.`);
883
+ }
884
+ edits.push(replaceContent(source, valueElement, escapeXmlText(value)));
885
+ }
886
+ // A `pid` must be an integer of at least 2, unique in the part
887
+ // (§22.3.2.2). Removing a property cannot break that, so only a pid that
888
+ // was already broken is renumbered; a valid one never moves.
889
+ const remaining = properties.filter((property)=>!removed.has(property));
890
+ const pidOf = (property)=>{
891
+ var _ref;
892
+ var _xmlAttribute;
893
+ const raw = (_ref = (_xmlAttribute = xmlAttribute(property, 'pid')) == null ? void 0 : _xmlAttribute.value.trim()) != null ? _ref : '';
894
+ return /^\d+$/.test(raw) ? Number(raw) : NaN;
895
+ };
896
+ let next = Math.max(1, ...remaining.map(pidOf).filter((pid)=>pid >= 2));
897
+ const used = new Set();
898
+ for (const property of remaining){
899
+ const pid = pidOf(property);
900
+ if (pid >= 2 && !used.has(pid)) {
901
+ used.add(pid);
902
+ continue;
903
+ }
904
+ next += 1;
905
+ const attribute = xmlAttribute(property, 'pid');
906
+ if (attribute) {
907
+ edits.push({
908
+ start: attribute.valueStart,
909
+ end: attribute.valueEnd,
910
+ text: String(next)
911
+ });
912
+ } else {
913
+ const at = property.start + 1 + property.name.length;
914
+ edits.push({
915
+ start: at,
916
+ end: at,
917
+ text: ` pid="${next}"`
918
+ });
919
+ }
920
+ }
921
+ return applyEdits(source, edits);
922
+ }
923
+ /** Add the core part's content-type override, if it has none (Part 2 §10.1.2). */ function editContentTypes(source, partName) {
924
+ const root = parsePart(source);
925
+ if (root.ns !== NS_CONTENT_TYPES || root.local !== 'Types') {
926
+ throw new EmbeddedWriteError('The package index of this file is damaged.');
927
+ }
928
+ const wanted = `/${partName}`.toLowerCase();
929
+ const has = root.children.some((child)=>{
930
+ var _xmlAttribute;
931
+ return child.local === 'Override' && ((_xmlAttribute = xmlAttribute(child, 'PartName')) == null ? void 0 : _xmlAttribute.value.toLowerCase()) === wanted;
932
+ });
933
+ if (has) return source;
934
+ const name = root.prefix ? `${root.prefix}:Override` : 'Override';
935
+ return applyEdits(source, [
936
+ appendTo(source, root, `<${name} PartName="/${escapeXmlAttribute(partName)}" ContentType="${CORE_CONTENT_TYPE}"/>`)
937
+ ]);
938
+ }
939
+ /** Add the package relationship to a new core part (Part 2 §9.3). */ function editRelationships(source, partName) {
940
+ const root = parsePart(source);
941
+ if (root.ns !== NS_RELATIONSHIPS || root.local !== 'Relationships') {
942
+ throw new EmbeddedWriteError('The package relationships of this file are damaged.');
943
+ }
944
+ const ids = new Set(root.children.map((child)=>{
945
+ var _xmlAttribute;
946
+ return (_xmlAttribute = xmlAttribute(child, 'Id')) == null ? void 0 : _xmlAttribute.value;
947
+ }));
948
+ let index = root.children.length + 1;
949
+ while(ids.has(`rId${index}`))index++;
950
+ const name = root.prefix ? `${root.prefix}:Relationship` : 'Relationship';
951
+ return applyEdits(source, [
952
+ appendTo(source, root, `<${name} Id="rId${index}" Type="${CORE_RELATIONSHIP}" Target="${escapeXmlAttribute(partName)}"/>`)
953
+ ]);
954
+ }
955
+ /** An empty core properties part, declared the way Office declares it. */ const EMPTY_CORE = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n' + `<cp:coreProperties xmlns:cp="${NS_CP}" xmlns:dc="${NS_DC}" xmlns:dcterms="${NS_DCTERMS}" xmlns:dcmitype="${NS_DCMITYPE}" xmlns:xsi="${NS_XSI}"></cp:coreProperties>`;
956
+ function readEditable(archive, entry) {
957
+ if (entry.encrypted) {
958
+ throw new EmbeddedWriteError('This file is encrypted, so its details can only be read.');
959
+ }
960
+ const bytes = readZipEntry(archive, entry, PART_MAX_BYTES);
961
+ if (!bytes) {
962
+ throw new EmbeddedWriteError('The document properties in this file are damaged, so they cannot be edited.');
963
+ }
964
+ return decodeForEdit(bytes);
965
+ }
966
+ /**
967
+ * Write an edit into an Office file's properties.
968
+ *
969
+ * Takes the canonical keys `title`, `subject`, `creator` (joined `"; "`),
970
+ * `keywords` (joined `", "`), `description` and `category`, and
971
+ * `ooxml|<name>` for a custom text property the file already holds; `null`
972
+ * removes. `dcterms:modified` is always set to `now`. A file with no core
973
+ * part gets one — plus its content-type override and package relationship.
974
+ *
975
+ * Throws {@link EmbeddedWriteError} for any other key, a custom property the
976
+ * file lacks or that is not text, and a package it cannot edit safely.
977
+ */ export function writeOoxml(bytes, patch, options) {
978
+ const archive = readZip(bytes);
979
+ if (!archive || !findPart(archive, CONTENT_TYPES_PART)) {
980
+ throw new EmbeddedWriteError('This file is not an Office document.');
981
+ }
982
+ const core = new Map();
983
+ const custom = new Map();
984
+ for (const [key, raw] of Object.entries(patch)){
985
+ const value = raw === null ? null : patchText(key, raw) || null;
986
+ if (isCoreKey(key) && CORE_WRITABLE.has(key)) {
987
+ core.set(key, value);
988
+ continue;
989
+ }
990
+ const other = parseOtherEmbeddedKey(key);
991
+ if ((other == null ? void 0 : other.namespace) === 'ooxml') {
992
+ custom.set(other.parts.join('|'), value);
993
+ continue;
994
+ }
995
+ throw new EmbeddedWriteError(`${embeddedFieldLabel(key, 'ooxml')} cannot be written into an Office document.`);
996
+ }
997
+ const layout = packageLayout(archive);
998
+ const changes = new Map();
999
+ const verify = (name, part)=>{
1000
+ if (!parseXml(part.text)) {
1001
+ throw new EmbeddedWriteError('The edited properties did not verify, so nothing was saved.');
1002
+ }
1003
+ changes.set(name, encodePart(part));
1004
+ };
1005
+ if (custom.size) {
1006
+ const name = layout.parts.custom;
1007
+ const entry = name === undefined ? undefined : findPart(archive, name);
1008
+ if (!entry) {
1009
+ const [first] = custom.keys();
1010
+ throw new EmbeddedWriteError(`"${first}" is not a custom property of this file.`);
1011
+ }
1012
+ const part = readEditable(archive, entry);
1013
+ verify(entry.name, _extends({}, part, {
1014
+ text: editCustom(part.text, custom)
1015
+ }));
1016
+ }
1017
+ const coreName = layout.parts.core;
1018
+ const coreEntry = coreName === undefined ? undefined : findPart(archive, coreName);
1019
+ if (coreEntry) {
1020
+ const part = readEditable(archive, coreEntry);
1021
+ verify(coreEntry.name, _extends({}, part, {
1022
+ text: editCore(part.text, core, options.now)
1023
+ }));
1024
+ } else {
1025
+ // No core part: create one, declare its type, and relate it to the
1026
+ // package — or, when a relationship already names a missing part,
1027
+ // create the part it names.
1028
+ let name = coreName;
1029
+ if (name === undefined) {
1030
+ name = DEFAULT_CORE_PART;
1031
+ for(let index = 1; findPart(archive, name); index++){
1032
+ name = `docProps/core${index}.xml`;
1033
+ }
1034
+ if (!layout.relationships) {
1035
+ throw new EmbeddedWriteError('This Office file has no package relationships.');
1036
+ }
1037
+ const rels = readEditable(archive, layout.relationships);
1038
+ verify(layout.relationships.name, _extends({}, rels, {
1039
+ text: editRelationships(rels.text, name)
1040
+ }));
1041
+ }
1042
+ const types = findPart(archive, CONTENT_TYPES_PART);
1043
+ if (!types) throw new EmbeddedWriteError('This file is not an Office document.');
1044
+ const typesPart = readEditable(archive, types);
1045
+ verify(types.name, _extends({}, typesPart, {
1046
+ text: editContentTypes(typesPart.text, name)
1047
+ }));
1048
+ verify(name, {
1049
+ text: editCore(EMPTY_CORE, core, options.now),
1050
+ bom: false
1051
+ });
1052
+ }
1053
+ return rewriteZip(archive, changes, {
1054
+ now: options.now
1055
+ });
1056
+ }
1057
+
1058
+ //# sourceMappingURL=ooxml.js.map