@office-kit/pptx-editor 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1304 @@
1
+ import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
2
+ //#region ../../src/internal/opc/zip.ts
3
+ const STORE_BY_EXTENSION = new Set([
4
+ "png",
5
+ "jpg",
6
+ "jpeg",
7
+ "gif",
8
+ "bmp",
9
+ "tif",
10
+ "tiff",
11
+ "webp",
12
+ "mp3",
13
+ "mp4",
14
+ "m4a",
15
+ "m4v",
16
+ "wav",
17
+ "wmv",
18
+ "mov",
19
+ "avi",
20
+ "webm",
21
+ "ogg",
22
+ "wma",
23
+ "pdf",
24
+ "zip",
25
+ "gz",
26
+ "7z",
27
+ "rar"
28
+ ]);
29
+ const extensionOf = (name) => {
30
+ const dot = name.lastIndexOf(".");
31
+ return dot < 0 ? "" : name.slice(dot + 1).toLowerCase();
32
+ };
33
+ /**
34
+ * Returns true if the entry should be stored uncompressed. PPTX writers
35
+ * conventionally STORE already-compressed payloads to avoid wasting CPU on
36
+ * negligible savings.
37
+ */
38
+ const shouldStore = (name) => STORE_BY_EXTENSION.has(extensionOf(name));
39
+ /**
40
+ * Reads a ZIP archive (a `.pptx` file is a ZIP) and returns entries in
41
+ * central-directory order. Throws if `input` is not a valid ZIP.
42
+ */
43
+ const readZip = (input) => {
44
+ const obj = unzipSync(input);
45
+ const entries = [];
46
+ for (const name of Object.keys(obj)) {
47
+ const data = obj[name];
48
+ if (data === void 0) continue;
49
+ entries.push({
50
+ name,
51
+ data
52
+ });
53
+ }
54
+ return { entries };
55
+ };
56
+ const writeZip = (entries, options = {}) => {
57
+ const defaultLevel = options.level ?? 6;
58
+ const obj = {};
59
+ for (const e of entries) {
60
+ const level = shouldStore(e.name) ? 0 : defaultLevel;
61
+ obj[e.name] = [e.data, { level }];
62
+ }
63
+ return zipSync(obj);
64
+ };
65
+ //#endregion
66
+ //#region ../../src/internal/opc/part-name.ts
67
+ /**
68
+ * Asserts the given string is a syntactically valid OPC part name and returns
69
+ * it as a branded `PartName`. Throws otherwise.
70
+ *
71
+ * Rules enforced (subset; see ECMA-376 Part 2 §9.1.1.1-9.1.1.6):
72
+ *
73
+ * - Must start with `/`.
74
+ * - Must not end with `/`.
75
+ * - Must not contain `//` (empty segments).
76
+ * - Each segment must be non-empty and must not be `.` or `..`.
77
+ */
78
+ const partName = (raw) => {
79
+ if (raw === "") throw new Error("part name is empty");
80
+ if (!raw.startsWith("/")) throw new Error(`part name must start with "/": ${raw}`);
81
+ if (raw.length > 1 && raw.endsWith("/")) throw new Error(`part name must not end with "/": ${raw}`);
82
+ const segments = raw.slice(1).split("/");
83
+ for (const s of segments) {
84
+ if (s === "") throw new Error(`part name contains empty segment: ${raw}`);
85
+ if (s === "." || s === "..") throw new Error(`part name contains "." or ".." segment: ${raw}`);
86
+ }
87
+ return raw;
88
+ };
89
+ /** Convert a part name to the corresponding ZIP entry path (no leading slash). */
90
+ const toZipPath = (name) => name.slice(1);
91
+ /** Convert a ZIP entry path to a part name (adds leading slash). */
92
+ const fromZipPath = (zipPath) => partName(`/${zipPath}`);
93
+ /**
94
+ * Returns the directory portion of a part name (everything before the final
95
+ * `/`). The result keeps the leading slash but has no trailing slash, except
96
+ * for the root which returns `/`.
97
+ */
98
+ const dirname = (name) => {
99
+ const idx = name.lastIndexOf("/");
100
+ if (idx <= 0) return "/";
101
+ return name.slice(0, idx);
102
+ };
103
+ /**
104
+ * Resolves a relationship Target (which may be relative) against a base part
105
+ * name. Returns the absolute part name (always starts with `/`).
106
+ *
107
+ * `target` is from a Relationship element's `Target` attribute when
108
+ * `TargetMode="Internal"`. External targets must not be passed through here.
109
+ */
110
+ const resolveTarget = (basePart, target) => {
111
+ if (target === "") throw new Error("relationship target is empty");
112
+ if (target.startsWith("/")) return partName(target);
113
+ const baseDir = dirname(basePart);
114
+ const stack = baseDir === "/" ? [] : baseDir.slice(1).split("/");
115
+ for (const segment of target.split("/")) {
116
+ if (segment === "" || segment === ".") continue;
117
+ if (segment === "..") {
118
+ if (stack.length === 0) throw new Error(`relationship target "${target}" escapes the package root`);
119
+ stack.pop();
120
+ continue;
121
+ }
122
+ stack.push(segment);
123
+ }
124
+ return partName(`/${stack.join("/")}`);
125
+ };
126
+ //#endregion
127
+ //#region ../../src/internal/xml/ast.ts
128
+ const qname = (prefix, localName, namespaceURI) => ({
129
+ prefix,
130
+ localName,
131
+ namespaceURI
132
+ });
133
+ const attr = (name, value) => ({
134
+ name,
135
+ value
136
+ });
137
+ const elem = (name, init = {}) => ({
138
+ kind: "element",
139
+ name,
140
+ attrs: init.attrs ?? [],
141
+ prefixDecls: init.prefixDecls ?? /* @__PURE__ */ new Map(),
142
+ children: init.children ?? []
143
+ });
144
+ const text = (data) => ({
145
+ kind: "text",
146
+ data
147
+ });
148
+ const cdata = (data) => ({
149
+ kind: "cdata",
150
+ data
151
+ });
152
+ const comment = (data) => ({
153
+ kind: "comment",
154
+ data
155
+ });
156
+ const pi = (target, data) => ({
157
+ kind: "pi",
158
+ target,
159
+ data
160
+ });
161
+ //#endregion
162
+ //#region ../../src/internal/xml/namespaces.ts
163
+ const NS = {
164
+ contentTypes: "http://schemas.openxmlformats.org/package/2006/content-types",
165
+ relationships: "http://schemas.openxmlformats.org/package/2006/relationships",
166
+ officeDocRels: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
167
+ pml: "http://schemas.openxmlformats.org/presentationml/2006/main",
168
+ dml: "http://schemas.openxmlformats.org/drawingml/2006/main",
169
+ diagram: "http://schemas.openxmlformats.org/drawingml/2006/diagram",
170
+ chart: "http://schemas.openxmlformats.org/drawingml/2006/chart",
171
+ chartDrawing: "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing",
172
+ sml: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
173
+ wml: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
174
+ mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
175
+ a14: "http://schemas.microsoft.com/office/drawing/2010/main",
176
+ a15: "http://schemas.microsoft.com/office/drawing/2012/main",
177
+ a16: "http://schemas.microsoft.com/office/drawing/2014/main",
178
+ p14: "http://schemas.microsoft.com/office/powerpoint/2010/main",
179
+ p15: "http://schemas.microsoft.com/office/powerpoint/2012/main",
180
+ p16: "http://schemas.microsoft.com/office/powerpoint/2015/main",
181
+ p159: "http://schemas.microsoft.com/office/powerpoint/2015/09/main",
182
+ xml: "http://www.w3.org/XML/1998/namespace",
183
+ xmlns: "http://www.w3.org/2000/xmlns/"
184
+ };
185
+ NS.contentTypes, NS.relationships, NS.officeDocRels, NS.pml, NS.dml, NS.diagram, NS.chart, NS.chartDrawing, NS.sml, NS.wml, NS.mc, NS.a14, NS.a15, NS.a16, NS.p14, NS.p15, NS.p16, NS.p159, NS.xml;
186
+ //#endregion
187
+ //#region ../../src/internal/xml/parse.ts
188
+ var XmlParseError = class extends Error {
189
+ offset;
190
+ line;
191
+ column;
192
+ constructor(message, offset, line, column) {
193
+ super(`${message} at line ${line}:${column} (offset ${offset})`);
194
+ this.offset = offset;
195
+ this.line = line;
196
+ this.column = column;
197
+ this.name = "XmlParseError";
198
+ }
199
+ };
200
+ const isNameStartChar = (c) => {
201
+ const code = c.charCodeAt(0);
202
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code === 58;
203
+ };
204
+ const isNameChar = (c) => {
205
+ if (isNameStartChar(c)) return true;
206
+ const code = c.charCodeAt(0);
207
+ return code >= 48 && code <= 57 || code === 45 || code === 46;
208
+ };
209
+ const isWhitespace = (c) => c === " " || c === " " || c === "\n" || c === "\r";
210
+ const lineColOf = (src, pos) => {
211
+ let line = 1;
212
+ let column = 1;
213
+ for (let i = 0; i < pos && i < src.length; i++) if (src.charCodeAt(i) === 10) {
214
+ line++;
215
+ column = 1;
216
+ } else column++;
217
+ return {
218
+ line,
219
+ column
220
+ };
221
+ };
222
+ const fail = (cur, message) => {
223
+ const { line, column } = lineColOf(cur.src, cur.pos);
224
+ throw new XmlParseError(message, cur.pos, line, column);
225
+ };
226
+ const expect = (cur, literal) => {
227
+ if (cur.src.slice(cur.pos, cur.pos + literal.length) !== literal) fail(cur, `expected "${literal}"`);
228
+ cur.pos += literal.length;
229
+ };
230
+ const accept = (cur, literal) => {
231
+ if (cur.src.slice(cur.pos, cur.pos + literal.length) !== literal) return false;
232
+ cur.pos += literal.length;
233
+ return true;
234
+ };
235
+ const skipWhitespace = (cur) => {
236
+ while (cur.pos < cur.src.length && isWhitespace(cur.src[cur.pos] ?? "")) cur.pos++;
237
+ };
238
+ const readName = (cur) => {
239
+ const start = cur.pos;
240
+ if (cur.pos >= cur.src.length || !isNameStartChar(cur.src[cur.pos] ?? "")) fail(cur, "expected XML name");
241
+ cur.pos++;
242
+ while (cur.pos < cur.src.length && isNameChar(cur.src[cur.pos] ?? "")) cur.pos++;
243
+ return cur.src.slice(start, cur.pos);
244
+ };
245
+ const splitQName = (raw) => {
246
+ const idx = raw.indexOf(":");
247
+ if (idx < 0) return {
248
+ prefix: "",
249
+ localName: raw
250
+ };
251
+ return {
252
+ prefix: raw.slice(0, idx),
253
+ localName: raw.slice(idx + 1)
254
+ };
255
+ };
256
+ const decodeRefs = (cur, raw) => {
257
+ let out = "";
258
+ let i = 0;
259
+ while (i < raw.length) {
260
+ if (raw.charCodeAt(i) !== 38) {
261
+ out += raw[i];
262
+ i++;
263
+ continue;
264
+ }
265
+ const end = raw.indexOf(";", i + 1);
266
+ if (end < 0) {
267
+ cur.pos -= raw.length - i;
268
+ fail(cur, "unterminated entity reference");
269
+ }
270
+ const ref = raw.slice(i + 1, end);
271
+ if (ref === "amp") out += "&";
272
+ else if (ref === "lt") out += "<";
273
+ else if (ref === "gt") out += ">";
274
+ else if (ref === "quot") out += "\"";
275
+ else if (ref === "apos") out += "'";
276
+ else if (ref.startsWith("#x") || ref.startsWith("#X")) {
277
+ const code = Number.parseInt(ref.slice(2), 16);
278
+ if (!Number.isFinite(code)) fail(cur, `invalid hex numeric reference: &${ref};`);
279
+ out += String.fromCodePoint(code);
280
+ } else if (ref.startsWith("#")) {
281
+ const code = Number.parseInt(ref.slice(1), 10);
282
+ if (!Number.isFinite(code)) fail(cur, `invalid decimal numeric reference: &${ref};`);
283
+ out += String.fromCodePoint(code);
284
+ } else fail(cur, `unknown entity reference: &${ref};`);
285
+ i = end + 1;
286
+ }
287
+ return out;
288
+ };
289
+ const readAttrValue = (cur) => {
290
+ const quote = cur.src[cur.pos];
291
+ if (quote !== "\"" && quote !== "'") fail(cur, "expected attribute value");
292
+ cur.pos++;
293
+ const start = cur.pos;
294
+ while (cur.pos < cur.src.length && cur.src[cur.pos] !== quote) {
295
+ if (cur.src.charCodeAt(cur.pos) === 60) fail(cur, "\"<\" is not allowed inside an attribute value");
296
+ cur.pos++;
297
+ }
298
+ if (cur.pos >= cur.src.length) fail(cur, "unterminated attribute value");
299
+ const raw = cur.src.slice(start, cur.pos);
300
+ cur.pos++;
301
+ return decodeRefs(cur, raw);
302
+ };
303
+ const xmlScope = {
304
+ parent: null,
305
+ bindings: new Map([["xml", NS.xml]])
306
+ };
307
+ const resolvePrefix = (scope, prefix) => {
308
+ let cursor = scope;
309
+ while (cursor) {
310
+ const found = cursor.bindings.get(prefix);
311
+ if (found !== void 0) return found;
312
+ cursor = cursor.parent;
313
+ }
314
+ return null;
315
+ };
316
+ const resolveQName = (cur, scope, raw, isAttribute) => {
317
+ const { prefix, localName } = splitQName(raw);
318
+ let namespaceURI = "";
319
+ if (prefix !== "") {
320
+ const found = resolvePrefix(scope, prefix);
321
+ if (found === null) return fail(cur, `unbound namespace prefix "${prefix}"`);
322
+ namespaceURI = found;
323
+ } else if (!isAttribute) namespaceURI = resolvePrefix(scope, "") ?? "";
324
+ return qname(prefix, localName, namespaceURI);
325
+ };
326
+ const parseDeclaration = (cur) => {
327
+ if (!accept(cur, "<?xml")) return null;
328
+ const end = cur.src.indexOf("?>", cur.pos);
329
+ if (end < 0) fail(cur, "unterminated XML declaration");
330
+ const body = cur.src.slice(cur.pos, end);
331
+ cur.pos = end + 2;
332
+ const get = (key) => {
333
+ return body.match(new RegExp(`\\b${key}\\s*=\\s*(['"])(.*?)\\1`))?.[2];
334
+ };
335
+ const version = get("version");
336
+ if (version === void 0) return fail(cur, "XML declaration missing \"version\"");
337
+ const encoding = get("encoding");
338
+ const standaloneRaw = get("standalone");
339
+ let standalone;
340
+ if (standaloneRaw === "yes" || standaloneRaw === "no") standalone = standaloneRaw;
341
+ else if (standaloneRaw !== void 0) return fail(cur, `invalid standalone value "${standaloneRaw}"`);
342
+ return {
343
+ version,
344
+ ...encoding !== void 0 ? { encoding } : {},
345
+ ...standalone !== void 0 ? { standalone } : {}
346
+ };
347
+ };
348
+ const parseComment = (cur) => {
349
+ expect(cur, "<!--");
350
+ const end = cur.src.indexOf("-->", cur.pos);
351
+ if (end < 0) fail(cur, "unterminated comment");
352
+ const data = cur.src.slice(cur.pos, end);
353
+ if (data.includes("--")) fail(cur, "\"--\" is not allowed inside an XML comment");
354
+ cur.pos = end + 3;
355
+ return comment(data);
356
+ };
357
+ const parsePI = (cur) => {
358
+ expect(cur, "<?");
359
+ const targetStart = cur.pos;
360
+ while (cur.pos < cur.src.length && !isWhitespace(cur.src[cur.pos] ?? "") && cur.src.slice(cur.pos, cur.pos + 2) !== "?>") cur.pos++;
361
+ const target = cur.src.slice(targetStart, cur.pos);
362
+ if (target.toLowerCase() === "xml") fail(cur, "PI target may not be \"xml\"");
363
+ skipWhitespace(cur);
364
+ const end = cur.src.indexOf("?>", cur.pos);
365
+ if (end < 0) fail(cur, "unterminated processing instruction");
366
+ const data = cur.src.slice(cur.pos, end);
367
+ cur.pos = end + 2;
368
+ return pi(target, data);
369
+ };
370
+ const parseCData = (cur) => {
371
+ expect(cur, "<![CDATA[");
372
+ const end = cur.src.indexOf("]]>", cur.pos);
373
+ if (end < 0) fail(cur, "unterminated CDATA section");
374
+ const data = cur.src.slice(cur.pos, end);
375
+ cur.pos = end + 3;
376
+ return cdata(data);
377
+ };
378
+ const skipDoctype = (cur) => {
379
+ expect(cur, "<!DOCTYPE");
380
+ while (cur.pos < cur.src.length) {
381
+ const c = cur.src[cur.pos];
382
+ if (c === "[") fail(cur, "DOCTYPE internal subset is not supported");
383
+ if (c === ">") {
384
+ cur.pos++;
385
+ return;
386
+ }
387
+ cur.pos++;
388
+ }
389
+ fail(cur, "unterminated DOCTYPE");
390
+ };
391
+ const parseStartOrEmptyTag = (cur, parentScope) => {
392
+ expect(cur, "<");
393
+ const rawName = readName(cur);
394
+ const ownAttrs = [];
395
+ const prefixDecls = /* @__PURE__ */ new Map();
396
+ let empty = false;
397
+ while (true) {
398
+ const wsStart = cur.pos;
399
+ skipWhitespace(cur);
400
+ if (cur.pos === wsStart && cur.src[cur.pos] !== "/" && cur.src[cur.pos] !== ">") fail(cur, "expected whitespace before next attribute");
401
+ if (accept(cur, "/>")) {
402
+ empty = true;
403
+ break;
404
+ }
405
+ if (accept(cur, ">")) break;
406
+ const attrRawName = readName(cur);
407
+ skipWhitespace(cur);
408
+ expect(cur, "=");
409
+ skipWhitespace(cur);
410
+ const value = readAttrValue(cur);
411
+ if (attrRawName === "xmlns") prefixDecls.set("", value);
412
+ else if (attrRawName.startsWith("xmlns:")) {
413
+ const p = attrRawName.slice(6);
414
+ if (p === "") fail(cur, "empty namespace prefix in xmlns:");
415
+ if (p === "xmlns") fail(cur, "prefix \"xmlns\" cannot be redeclared");
416
+ prefixDecls.set(p, value);
417
+ } else ownAttrs.push({
418
+ raw: attrRawName,
419
+ rawValue: value
420
+ });
421
+ }
422
+ const scope = {
423
+ parent: parentScope,
424
+ bindings: new Map(prefixDecls)
425
+ };
426
+ return {
427
+ element: elem(resolveQName(cur, scope, rawName, false), {
428
+ attrs: ownAttrs.map(({ raw, rawValue }) => attr(resolveQName(cur, scope, raw, true), rawValue)),
429
+ prefixDecls,
430
+ children: []
431
+ }),
432
+ scope,
433
+ empty
434
+ };
435
+ };
436
+ const parseEndTag = (cur) => {
437
+ expect(cur, "</");
438
+ const name = readName(cur);
439
+ skipWhitespace(cur);
440
+ expect(cur, ">");
441
+ return name;
442
+ };
443
+ const parseTextContent = (cur) => {
444
+ const start = cur.pos;
445
+ while (cur.pos < cur.src.length && cur.src.charCodeAt(cur.pos) !== 60) cur.pos++;
446
+ return decodeRefs(cur, cur.src.slice(start, cur.pos));
447
+ };
448
+ const parseElement = (cur, parentScope) => {
449
+ const { element, scope, empty } = parseStartOrEmptyTag(cur, parentScope);
450
+ if (empty) return element;
451
+ while (cur.pos < cur.src.length) if (cur.src.charCodeAt(cur.pos) === 60) {
452
+ if (cur.src.startsWith("</", cur.pos)) {
453
+ const closingRaw = parseEndTag(cur);
454
+ const expectedRaw = element.name.prefix === "" ? element.name.localName : `${element.name.prefix}:${element.name.localName}`;
455
+ if (closingRaw !== expectedRaw) fail(cur, `mismatched closing tag: expected </${expectedRaw}>, got </${closingRaw}>`);
456
+ return element;
457
+ }
458
+ if (cur.src.startsWith("<![CDATA[", cur.pos)) element.children.push(parseCData(cur));
459
+ else if (cur.src.startsWith("<!--", cur.pos)) element.children.push(parseComment(cur));
460
+ else if (cur.src.startsWith("<?", cur.pos)) element.children.push(parsePI(cur));
461
+ else element.children.push(parseElement(cur, scope));
462
+ } else {
463
+ const t = parseTextContent(cur);
464
+ if (t.length > 0) element.children.push(text(t));
465
+ }
466
+ return fail(cur, `unterminated element <${element.name.localName}>`);
467
+ };
468
+ const parseXml = (src) => {
469
+ const cur = {
470
+ src: (src.charCodeAt(0) === 65279 ? src.slice(1) : src).replace(/\r\n?/g, "\n"),
471
+ pos: 0
472
+ };
473
+ const decl = parseDeclaration(cur);
474
+ skipWhitespace(cur);
475
+ const prolog = [];
476
+ while (cur.pos < cur.src.length) {
477
+ skipWhitespace(cur);
478
+ if (cur.src.startsWith("<!--", cur.pos)) prolog.push(parseComment(cur));
479
+ else if (cur.src.startsWith("<!DOCTYPE", cur.pos)) skipDoctype(cur);
480
+ else if (cur.src.startsWith("<?", cur.pos)) prolog.push(parsePI(cur));
481
+ else break;
482
+ }
483
+ skipWhitespace(cur);
484
+ if (cur.pos >= cur.src.length || cur.src[cur.pos] !== "<") fail(cur, "expected root element");
485
+ const root = parseElement(cur, xmlScope);
486
+ const epilog = [];
487
+ while (cur.pos < cur.src.length) {
488
+ skipWhitespace(cur);
489
+ if (cur.pos >= cur.src.length) break;
490
+ if (cur.src.startsWith("<!--", cur.pos)) epilog.push(parseComment(cur));
491
+ else if (cur.src.startsWith("<?", cur.pos)) epilog.push(parsePI(cur));
492
+ else fail(cur, "unexpected content after root element");
493
+ }
494
+ return {
495
+ kind: "document",
496
+ decl,
497
+ prolog,
498
+ root,
499
+ epilog
500
+ };
501
+ };
502
+ //#endregion
503
+ //#region ../../src/internal/xml/query.ts
504
+ const isElement = (n) => n.kind === "element";
505
+ /**
506
+ * Walks the element tree depth-first, yielding every element including the
507
+ * root. The visitor returns `false` to skip descending into the current
508
+ * element's children. Returning nothing (or `true`) descends as usual.
509
+ */
510
+ const walkElements = (root, visit) => {
511
+ const stack = [root];
512
+ while (stack.length > 0) {
513
+ const node = stack.pop();
514
+ if (!node) continue;
515
+ if (visit(node) === false) continue;
516
+ for (let i = node.children.length - 1; i >= 0; i--) {
517
+ const c = node.children[i];
518
+ if (c && isElement(c)) stack.push(c);
519
+ }
520
+ }
521
+ };
522
+ //#endregion
523
+ //#region ../../src/internal/xml/serialize.ts
524
+ const rejectForbiddenChar = (c) => {
525
+ const control = c < 32 && c !== 9 && c !== 10 && c !== 13;
526
+ if (control || c >= 55296 && c <= 57343 || c === 65534 || c === 65535) {
527
+ const hex = c.toString(16).toUpperCase().padStart(4, "0");
528
+ throw new Error(`XML-illegal ${control ? "control character" : "character"} U+${hex} in text; strip ${control ? "control" : "illegal"} characters before authoring`);
529
+ }
530
+ };
531
+ const escapeAttr = (s) => {
532
+ let out = "";
533
+ for (const char of s) {
534
+ const c = char.codePointAt(0);
535
+ rejectForbiddenChar(c);
536
+ if (c === 38) out += "&amp;";
537
+ else if (c === 60) out += "&lt;";
538
+ else if (c === 62) out += "&gt;";
539
+ else if (c === 34) out += "&quot;";
540
+ else if (c === 9) out += "&#9;";
541
+ else if (c === 10) out += "&#10;";
542
+ else if (c === 13) out += "&#13;";
543
+ else out += char;
544
+ }
545
+ return out;
546
+ };
547
+ const escapeText = (s) => {
548
+ let out = "";
549
+ for (const char of s) {
550
+ const c = char.codePointAt(0);
551
+ rejectForbiddenChar(c);
552
+ if (c === 38) out += "&amp;";
553
+ else if (c === 60) out += "&lt;";
554
+ else if (c === 62) out += "&gt;";
555
+ else if (c === 13) out += "&#13;";
556
+ else out += char;
557
+ }
558
+ return out;
559
+ };
560
+ const rawName = (prefix, localName) => prefix === "" ? localName : `${prefix}:${localName}`;
561
+ const writeAttr = (a) => {
562
+ return ` ${rawName(a.name.prefix, a.name.localName)}="${escapeAttr(a.value)}"`;
563
+ };
564
+ const writeElement = (e, parts) => {
565
+ const name = rawName(e.name.prefix, e.name.localName);
566
+ parts.push("<");
567
+ parts.push(name);
568
+ for (const [prefix, uri] of e.prefixDecls) {
569
+ const decl = prefix === "" ? "xmlns" : `xmlns:${prefix}`;
570
+ parts.push(` ${decl}="${escapeAttr(uri)}"`);
571
+ }
572
+ for (const a of e.attrs) parts.push(writeAttr(a));
573
+ if (e.children.length === 0) {
574
+ parts.push("/>");
575
+ return;
576
+ }
577
+ parts.push(">");
578
+ for (const child of e.children) writeNode(child, parts);
579
+ parts.push("</");
580
+ parts.push(name);
581
+ parts.push(">");
582
+ };
583
+ const writeNode = (n, parts) => {
584
+ switch (n.kind) {
585
+ case "element":
586
+ writeElement(n, parts);
587
+ return;
588
+ case "text":
589
+ parts.push(escapeText(n.data));
590
+ return;
591
+ case "cdata":
592
+ parts.push("<![CDATA[");
593
+ parts.push(n.data.split("]]>").join("]]]]><![CDATA[>"));
594
+ parts.push("]]>");
595
+ return;
596
+ case "comment":
597
+ parts.push("<!--");
598
+ parts.push(n.data);
599
+ parts.push("-->");
600
+ return;
601
+ case "pi":
602
+ parts.push("<?");
603
+ parts.push(n.target);
604
+ if (n.data.length > 0) {
605
+ parts.push(" ");
606
+ parts.push(n.data);
607
+ }
608
+ parts.push("?>");
609
+ return;
610
+ }
611
+ };
612
+ const writeDeclaration = (doc, parts) => {
613
+ if (!doc.decl) return;
614
+ parts.push("<?xml version=\"");
615
+ parts.push(escapeAttr(doc.decl.version));
616
+ parts.push("\"");
617
+ if (doc.decl.encoding !== void 0) {
618
+ parts.push(" encoding=\"");
619
+ parts.push(escapeAttr(doc.decl.encoding));
620
+ parts.push("\"");
621
+ }
622
+ if (doc.decl.standalone !== void 0) {
623
+ parts.push(" standalone=\"");
624
+ parts.push(doc.decl.standalone);
625
+ parts.push("\"");
626
+ }
627
+ parts.push("?>");
628
+ };
629
+ const serializeXml = (doc, options = {}) => {
630
+ const parts = [];
631
+ if (doc.decl) writeDeclaration(doc, parts);
632
+ else if (options.standaloneDeclaration ?? true) parts.push("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
633
+ for (const p of doc.prolog) writeNode(p, parts);
634
+ writeElement(doc.root, parts);
635
+ for (const e of doc.epilog) writeNode(e, parts);
636
+ return parts.join("");
637
+ };
638
+ const serializeFragment = (element) => {
639
+ const parts = [];
640
+ writeElement(element, parts);
641
+ return parts.join("");
642
+ };
643
+ //#endregion
644
+ //#region ../../src/internal/opc/content-types.ts
645
+ const CT_NS = NS.contentTypes;
646
+ const NAME_TYPES = qname("", "Types", CT_NS);
647
+ const NAME_DEFAULT = qname("", "Default", CT_NS);
648
+ const NAME_OVERRIDE = qname("", "Override", CT_NS);
649
+ const ATTR_EXTENSION = qname("", "Extension", "");
650
+ const ATTR_CONTENT_TYPE = qname("", "ContentType", "");
651
+ const ATTR_PART_NAME = qname("", "PartName", "");
652
+ const getAttrValueRaw = (e, local) => {
653
+ for (const a of e.attrs) if (a.name.localName === local) return a.value;
654
+ return null;
655
+ };
656
+ const parseContentTypes = (xml) => {
657
+ const doc = parseXml(xml);
658
+ if (doc.root.name.namespaceURI !== CT_NS || doc.root.name.localName !== "Types") throw new Error(`expected root <Types xmlns="${CT_NS}">, got <${doc.root.name.localName}>`);
659
+ const defaults = [];
660
+ const overrides = [];
661
+ for (const child of doc.root.children) {
662
+ if (child.kind !== "element") continue;
663
+ if (child.name.namespaceURI !== CT_NS) continue;
664
+ if (child.name.localName === "Default") {
665
+ const ext = getAttrValueRaw(child, "Extension");
666
+ const ct = getAttrValueRaw(child, "ContentType");
667
+ if (ext === null || ct === null) throw new Error("Content_Types Default missing Extension or ContentType");
668
+ defaults.push({
669
+ extension: ext.toLowerCase(),
670
+ contentType: ct
671
+ });
672
+ } else if (child.name.localName === "Override") {
673
+ const pn = getAttrValueRaw(child, "PartName");
674
+ const ct = getAttrValueRaw(child, "ContentType");
675
+ if (pn === null || ct === null) throw new Error("Content_Types Override missing PartName or ContentType");
676
+ overrides.push({
677
+ partName: partName(pn),
678
+ contentType: ct
679
+ });
680
+ }
681
+ }
682
+ return {
683
+ defaults,
684
+ overrides
685
+ };
686
+ };
687
+ const serializeContentTypes = (ct) => {
688
+ const children = [...ct.defaults.map((d) => elem(NAME_DEFAULT, { attrs: [{
689
+ name: ATTR_EXTENSION,
690
+ value: d.extension
691
+ }, {
692
+ name: ATTR_CONTENT_TYPE,
693
+ value: d.contentType
694
+ }] })), ...ct.overrides.map((o) => elem(NAME_OVERRIDE, { attrs: [{
695
+ name: ATTR_PART_NAME,
696
+ value: o.partName
697
+ }, {
698
+ name: ATTR_CONTENT_TYPE,
699
+ value: o.contentType
700
+ }] }))];
701
+ return serializeXml({
702
+ kind: "document",
703
+ decl: {
704
+ version: "1.0",
705
+ encoding: "UTF-8",
706
+ standalone: "yes"
707
+ },
708
+ prolog: [],
709
+ root: elem(NAME_TYPES, {
710
+ prefixDecls: new Map([["", CT_NS]]),
711
+ children
712
+ }),
713
+ epilog: []
714
+ });
715
+ };
716
+ //#endregion
717
+ //#region ../../src/internal/opc/rels.ts
718
+ const REL_NS = NS.relationships;
719
+ const NAME_RELATIONSHIPS = qname("", "Relationships", REL_NS);
720
+ const NAME_RELATIONSHIP = qname("", "Relationship", REL_NS);
721
+ const ATTR_ID = qname("", "Id", "");
722
+ const ATTR_TYPE = qname("", "Type", "");
723
+ const ATTR_TARGET = qname("", "Target", "");
724
+ const ATTR_TARGET_MODE = qname("", "TargetMode", "");
725
+ const findAttr = (e, local) => {
726
+ for (const a of e.attrs) if (a.name.localName === local) return a.value;
727
+ return null;
728
+ };
729
+ const parseRels = (xml) => {
730
+ const doc = parseXml(xml);
731
+ if (doc.root.name.namespaceURI !== REL_NS || doc.root.name.localName !== "Relationships") throw new Error(`expected root <Relationships xmlns="${REL_NS}">, got <${doc.root.name.localName}>`);
732
+ const items = [];
733
+ for (const child of doc.root.children) {
734
+ if (child.kind !== "element") continue;
735
+ if (child.name.namespaceURI !== REL_NS || child.name.localName !== "Relationship") continue;
736
+ const id = findAttr(child, "Id");
737
+ const type = findAttr(child, "Type");
738
+ const target = findAttr(child, "Target");
739
+ if (id === null || type === null || target === null) throw new Error("Relationship missing Id, Type, or Target");
740
+ const modeRaw = findAttr(child, "TargetMode");
741
+ let targetMode = "Internal";
742
+ if (modeRaw !== null) {
743
+ if (modeRaw !== "Internal" && modeRaw !== "External") throw new Error(`invalid TargetMode "${modeRaw}"`);
744
+ targetMode = modeRaw;
745
+ }
746
+ items.push({
747
+ id,
748
+ type,
749
+ target,
750
+ targetMode
751
+ });
752
+ }
753
+ return { items };
754
+ };
755
+ const serializeRels = (rels) => {
756
+ const children = rels.items.map((r) => {
757
+ const attrs = [
758
+ {
759
+ name: ATTR_ID,
760
+ value: r.id
761
+ },
762
+ {
763
+ name: ATTR_TYPE,
764
+ value: r.type
765
+ },
766
+ {
767
+ name: ATTR_TARGET,
768
+ value: r.target
769
+ }
770
+ ];
771
+ if (r.targetMode === "External") attrs.push({
772
+ name: ATTR_TARGET_MODE,
773
+ value: "External"
774
+ });
775
+ return elem(NAME_RELATIONSHIP, { attrs });
776
+ });
777
+ return serializeXml({
778
+ kind: "document",
779
+ decl: {
780
+ version: "1.0",
781
+ encoding: "UTF-8",
782
+ standalone: "yes"
783
+ },
784
+ prolog: [],
785
+ root: elem(NAME_RELATIONSHIPS, {
786
+ prefixDecls: new Map([["", REL_NS]]),
787
+ children
788
+ }),
789
+ epilog: []
790
+ });
791
+ };
792
+ /**
793
+ * Convenience: an empty Relationships value with no items.
794
+ */
795
+ const emptyRels = () => ({ items: [] });
796
+ //#endregion
797
+ //#region src/merge/deck-merge.ts
798
+ const CORE_PROPERTIES = "docProps/core.xml";
799
+ const CORE_TIMESTAMPS = /<(dcterms:(?:created|modified))\b[^>]*>[\s\S]*?<\/\1>/g;
800
+ /**
801
+ * The part as the merge compares it. Every build stamps fresh creation and
802
+ * modification times into the core properties, so those are left out.
803
+ */
804
+ function comparablePart(name, bytes) {
805
+ if (name !== CORE_PROPERTIES) return bytes;
806
+ return strToU8(strFromU8(bytes).replace(CORE_TIMESTAMPS, ""));
807
+ }
808
+ const CONTENT_TYPES = "[Content_Types].xml";
809
+ const PRESENTATION = "ppt/presentation.xml";
810
+ const SHAPE_TREE_PART = /^ppt\/(?:slides|slideLayouts|slideMasters|notesSlides|notesMasters|handoutMasters)\/[^/]+\.xml$/;
811
+ const RELATIONSHIPS_PART = /(?:^|\/)_rels\/[^/]*\.rels$/;
812
+ const SLIDE_RELATIONSHIP = `${NS.officeDocRels}/slide`;
813
+ const NOTES_RELATIONSHIP = `${NS.officeDocRels}/notesSlide`;
814
+ const SHAPE_TREE_PROPERTIES = new Set([
815
+ "nvGrpSpPr",
816
+ "grpSpPr",
817
+ "extLst"
818
+ ]);
819
+ function sameBytes(a, b) {
820
+ if (a.length !== b.length) return false;
821
+ for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
822
+ return true;
823
+ }
824
+ /**
825
+ * Three-way merge of keyed items. An item changed on one side only takes that
826
+ * side; an item changed differently on both sides is a conflict.
827
+ */
828
+ function mergeEntries(base, ours, theirs, options) {
829
+ const byKey = (entries) => new Map(entries.map((entry) => [entry.key, entry]));
830
+ const [b, o, t] = [
831
+ byKey(base),
832
+ byKey(ours),
833
+ byKey(theirs)
834
+ ];
835
+ const keys = new Set([
836
+ ...t.keys(),
837
+ ...o.keys(),
838
+ ...b.keys()
839
+ ]);
840
+ const chosen = /* @__PURE__ */ new Map();
841
+ const conflicts = [];
842
+ const same = (x, y) => x?.value === y?.value;
843
+ for (const key of keys) {
844
+ const [be, oe, te] = [
845
+ b.get(key),
846
+ o.get(key),
847
+ t.get(key)
848
+ ];
849
+ let pick;
850
+ if (same(oe, be)) pick = te;
851
+ else if (same(te, be) || same(oe, te)) pick = oe;
852
+ else {
853
+ const refined = be && oe && te ? options.refine?.(key, be.item, oe.item, te.item) : null;
854
+ if (refined?.ok) chosen.set(key, refined.value);
855
+ else if (refined) conflicts.push(...refined.conflicts);
856
+ else {
857
+ const reason = !be ? "both-added" : !oe || !te ? "deleted-and-changed" : "both-changed";
858
+ conflicts.push(options.conflict(key, reason, (oe ?? te ?? be).item));
859
+ }
860
+ continue;
861
+ }
862
+ if (pick) chosen.set(key, pick.item);
863
+ }
864
+ if (conflicts.length) return {
865
+ ok: false,
866
+ conflicts
867
+ };
868
+ const order = mergeOrder(base.map((entry) => entry.key), ours.map((entry) => entry.key), theirs.map((entry) => entry.key), new Set(chosen.keys()), options.ordering);
869
+ if (!order) return {
870
+ ok: false,
871
+ conflicts: [options.conflict("", "both-changed")]
872
+ };
873
+ return {
874
+ ok: true,
875
+ value: order.map((key) => chosen.get(key))
876
+ };
877
+ }
878
+ /** True when the keys both lists share appear in the same order in each. */
879
+ function sameRelativeOrder(a, b) {
880
+ const [inA, inB] = [new Set(a), new Set(b)];
881
+ const sharedA = a.filter((key) => inB.has(key));
882
+ const sharedB = b.filter((key) => inA.has(key));
883
+ return sharedA.every((key, index) => key === sharedB[index]);
884
+ }
885
+ /**
886
+ * Orders the kept keys. The side that reordered (or the source when neither
887
+ * did) gives the order; the other side's additions follow their nearest
888
+ * preceding sibling. `null` means the order cannot be decided.
889
+ */
890
+ function mergeOrder(base, ours, theirs, keep, ordering) {
891
+ const oursMoved = ordering !== "none" && !sameRelativeOrder(base, ours);
892
+ const theirsMoved = ordering !== "none" && !sameRelativeOrder(base, theirs);
893
+ if (oursMoved && theirsMoved && !sameRelativeOrder(ours, theirs)) return null;
894
+ const [primary, secondary] = oursMoved ? [ours, theirs] : [theirs, ours];
895
+ const kept = primary.filter((key) => keep.has(key));
896
+ const placed = new Set(kept);
897
+ const inSecondary = new Set(secondary);
898
+ const next = new Map(kept.map((key, index) => [index === 0 ? null : kept[index - 1], key]));
899
+ if (kept.length) next.set(kept.at(-1), void 0);
900
+ const after = /* @__PURE__ */ new Map();
901
+ let anchor = null;
902
+ let chained = false;
903
+ for (const key of secondary) {
904
+ if (!keep.has(key)) continue;
905
+ if (placed.has(key)) {
906
+ anchor = key;
907
+ chained = false;
908
+ continue;
909
+ }
910
+ if (!chained) {
911
+ const following = next.get(anchor);
912
+ if (ordering === "schema" && following !== void 0 && !inSecondary.has(following)) return null;
913
+ }
914
+ after.set(anchor, key);
915
+ anchor = key;
916
+ chained = true;
917
+ }
918
+ const result = [];
919
+ const emit = (key) => {
920
+ for (let at = key; at !== void 0; at = after.get(at)) if (at !== null) result.push(at);
921
+ };
922
+ emit(null);
923
+ for (const key of kept) emit(key);
924
+ return result;
925
+ }
926
+ const partConflict = (context, reason, shape = null) => ({
927
+ part: context.part,
928
+ slide: null,
929
+ shape,
930
+ reason
931
+ });
932
+ function shapeIdentity(element) {
933
+ let found;
934
+ walkElements(element, (child) => {
935
+ if (found) return false;
936
+ if (child.name.localName === "cNvPr") found = child;
937
+ });
938
+ if (!found) return null;
939
+ const value = (name) => found.attrs.find((item) => item.name.localName === name)?.value;
940
+ const id = value("id");
941
+ return id === void 0 ? null : {
942
+ id,
943
+ name: value("name") ?? ""
944
+ };
945
+ }
946
+ const elementKey = (element) => `${element.name.namespaceURI} ${element.name.localName}`;
947
+ /** Keys an element's children, or `null` when they cannot be told apart. */
948
+ function childEntries(element, kind) {
949
+ const entries = [];
950
+ const keys = /* @__PURE__ */ new Set();
951
+ const occurrences = /* @__PURE__ */ new Map();
952
+ for (const child of element.children) {
953
+ if (child.kind !== "element") {
954
+ if (child.kind === "text" && !child.data.trim()) continue;
955
+ return null;
956
+ }
957
+ let key;
958
+ if (kind === "spTree" && !(child.name.namespaceURI === NS.pml && SHAPE_TREE_PROPERTIES.has(child.name.localName))) {
959
+ const identity = shapeIdentity(child);
960
+ if (!identity) return null;
961
+ key = `shape ${identity.id}`;
962
+ } else {
963
+ const name = elementKey(child);
964
+ const occurrence = occurrences.get(name) ?? 0;
965
+ occurrences.set(name, occurrence + 1);
966
+ key = `${name} ${occurrence}`;
967
+ }
968
+ if (keys.has(key)) return null;
969
+ keys.add(key);
970
+ entries.push({
971
+ key,
972
+ value: serializeFragment(child),
973
+ item: child
974
+ });
975
+ }
976
+ return entries;
977
+ }
978
+ const attributesValue = (attrs) => JSON.stringify(attrs.map((item) => [
979
+ item.name.namespaceURI,
980
+ item.name.localName,
981
+ item.value
982
+ ]));
983
+ const NESTED = {
984
+ root: {
985
+ localName: "cSld",
986
+ kind: "cSld"
987
+ },
988
+ cSld: {
989
+ localName: "spTree",
990
+ kind: "spTree"
991
+ }
992
+ };
993
+ /** Merges a slide-like element: its attributes as one unit, its children by key. */
994
+ function mergeContainer(context, base, ours, theirs, kind) {
995
+ const conflicts = [];
996
+ const [ba, oa, ta] = [
997
+ base.attrs,
998
+ ours.attrs,
999
+ theirs.attrs
1000
+ ].map(attributesValue);
1001
+ let attrs = theirs.attrs;
1002
+ if (oa !== ba && (ta === ba || oa === ta)) attrs = ours.attrs;
1003
+ else if (oa !== ba) conflicts.push(partConflict(context, "both-changed"));
1004
+ const prefixDecls = new Map(theirs.prefixDecls);
1005
+ for (const [prefix, uri] of ours.prefixDecls) {
1006
+ const existing = prefixDecls.get(prefix);
1007
+ if (existing === void 0) prefixDecls.set(prefix, uri);
1008
+ else if (existing !== uri) conflicts.push(partConflict(context, "both-changed"));
1009
+ }
1010
+ const entries = [
1011
+ base,
1012
+ ours,
1013
+ theirs
1014
+ ].map((element) => childEntries(element, kind));
1015
+ if (entries.some((list) => !list)) conflicts.push(partConflict(context, "both-changed"));
1016
+ if (conflicts.length) return {
1017
+ ok: false,
1018
+ conflicts
1019
+ };
1020
+ const nested = NESTED[kind];
1021
+ const children = mergeEntries(entries[0], entries[1], entries[2], {
1022
+ ordering: kind === "spTree" ? "free" : "schema",
1023
+ refine: (_key, b, o, t) => nested && t.name.namespaceURI === NS.pml && t.name.localName === nested.localName ? mergeContainer(context, b, o, t, nested.kind) : null,
1024
+ conflict: (key, reason, sample) => partConflict(context, reason, sample && key.startsWith("shape ") ? shapeIdentity(sample) : null)
1025
+ });
1026
+ if (!children.ok) return children;
1027
+ return {
1028
+ ok: true,
1029
+ value: elem(theirs.name, {
1030
+ attrs,
1031
+ prefixDecls,
1032
+ children: children.value
1033
+ })
1034
+ };
1035
+ }
1036
+ function mergeShapeTreePart(context, base, ours, theirs) {
1037
+ const [b, o, t] = [
1038
+ base,
1039
+ ours,
1040
+ theirs
1041
+ ].map((bytes) => parseXml(strFromU8(bytes)));
1042
+ const root = mergeContainer(context, b.root, o.root, t.root, "root");
1043
+ if (!root.ok) return root;
1044
+ return {
1045
+ ok: true,
1046
+ value: strToU8(serializeXml({
1047
+ ...t,
1048
+ root: root.value
1049
+ }))
1050
+ };
1051
+ }
1052
+ function relationshipEntries(bytes) {
1053
+ return (bytes ? parseRels(strFromU8(bytes)) : emptyRels()).items.map((item) => ({
1054
+ key: item.id,
1055
+ value: JSON.stringify([
1056
+ item.type,
1057
+ item.target,
1058
+ item.targetMode
1059
+ ]),
1060
+ item
1061
+ }));
1062
+ }
1063
+ function mergeRelationshipsPart(context, base, ours, theirs) {
1064
+ const items = mergeEntries(relationshipEntries(base), relationshipEntries(ours), relationshipEntries(theirs), {
1065
+ ordering: "none",
1066
+ conflict: (_key, reason) => partConflict(context, reason)
1067
+ });
1068
+ if (!items.ok) return items;
1069
+ return {
1070
+ ok: true,
1071
+ value: strToU8(serializeRels({ items: items.value }))
1072
+ };
1073
+ }
1074
+ /**
1075
+ * Content types are declarations about parts, so both sides' entries are kept
1076
+ * and only overrides for parts the merged package no longer has are dropped.
1077
+ */
1078
+ function mergeContentTypes(ours, theirs, parts) {
1079
+ const [o, t] = [ours, theirs].map((bytes) => parseContentTypes(strFromU8(bytes)));
1080
+ const merged = {
1081
+ defaults: [],
1082
+ overrides: []
1083
+ };
1084
+ const defaults = /* @__PURE__ */ new Map();
1085
+ const overrides = /* @__PURE__ */ new Map();
1086
+ const conflicts = [];
1087
+ const context = { part: CONTENT_TYPES };
1088
+ for (const types of [t, o]) {
1089
+ for (const item of types.defaults) {
1090
+ const existing = defaults.get(item.extension);
1091
+ if (existing === void 0) {
1092
+ defaults.set(item.extension, item.contentType);
1093
+ merged.defaults.push(item);
1094
+ } else if (existing !== item.contentType) conflicts.push(partConflict(context, "both-changed"));
1095
+ }
1096
+ for (const item of types.overrides) {
1097
+ const key = item.partName.toLowerCase();
1098
+ if (!parts.has(key)) continue;
1099
+ const existing = overrides.get(key);
1100
+ if (existing === void 0) {
1101
+ overrides.set(key, item.contentType);
1102
+ merged.overrides.push(item);
1103
+ } else if (existing !== item.contentType) conflicts.push(partConflict(context, "both-changed"));
1104
+ }
1105
+ }
1106
+ if (conflicts.length) return {
1107
+ ok: false,
1108
+ conflicts
1109
+ };
1110
+ return {
1111
+ ok: true,
1112
+ value: strToU8(serializeContentTypes(merged))
1113
+ };
1114
+ }
1115
+ /** Maps each slide's parts (the slide, its notes and their relationships) to its number. */
1116
+ function slideNumbers(parts) {
1117
+ const numbers = /* @__PURE__ */ new Map();
1118
+ const presentation = parts.get(PRESENTATION);
1119
+ const presentationRels = parts.get("ppt/_rels/presentation.xml.rels");
1120
+ if (!presentation || !presentationRels) return numbers;
1121
+ const targets = new Map(parseRels(strFromU8(presentationRels)).items.filter((item) => item.type === SLIDE_RELATIONSHIP).map((item) => [item.id, toZipPath(resolveTarget(fromZipPath(PRESENTATION), item.target))]));
1122
+ let number = 0;
1123
+ walkElements(parseXml(strFromU8(presentation)).root, (element) => {
1124
+ if (element.name.localName !== "sldId") return void 0;
1125
+ number++;
1126
+ const id = element.attrs.find((item) => item.name.namespaceURI === NS.officeDocRels && item.name.localName === "id")?.value;
1127
+ const slide = id === void 0 ? void 0 : targets.get(id);
1128
+ if (!slide) return false;
1129
+ const relsPart = relsName(slide);
1130
+ numbers.set(slide, number).set(relsPart, number);
1131
+ const rels = parts.get(relsPart);
1132
+ if (!rels) return false;
1133
+ for (const item of parseRels(strFromU8(rels)).items) {
1134
+ if (item.type !== NOTES_RELATIONSHIP) continue;
1135
+ const notes = toZipPath(resolveTarget(fromZipPath(slide), item.target));
1136
+ numbers.set(notes, number).set(relsName(notes), number);
1137
+ }
1138
+ return false;
1139
+ });
1140
+ return numbers;
1141
+ }
1142
+ const relsName = (part) => {
1143
+ const slash = part.lastIndexOf("/");
1144
+ return `${part.slice(0, slash + 1)}_rels/${part.slice(slash + 1)}.rels`;
1145
+ };
1146
+ /**
1147
+ * Both sides allocate the next free relationship ID, so a relationship the
1148
+ * editor added and a different one the source added often share an ID. IDs
1149
+ * are private to a part and its relationships, so the editor's is renamed
1150
+ * (in its relationships and in the part referring to it) instead of
1151
+ * reporting a conflict.
1152
+ */
1153
+ function renumberAddedRelationships(base, ours, theirs) {
1154
+ for (const [name, bytes] of ours) {
1155
+ const owner = relationshipsOwner(name);
1156
+ const theirsBytes = theirs.get(name);
1157
+ const ownerBytes = owner && ours.get(owner);
1158
+ if (!ownerBytes || !theirsBytes) continue;
1159
+ const parse = (value) => value ? parseRels(strFromU8(value)) : emptyRels();
1160
+ const [b, o, t] = [
1161
+ parse(base.get(name)),
1162
+ parse(bytes),
1163
+ parse(theirsBytes)
1164
+ ];
1165
+ const inBase = new Set(b.items.map((item) => item.id));
1166
+ const theirsById = new Map(t.items.map((item) => [item.id, item]));
1167
+ const used = new Set([
1168
+ ...inBase,
1169
+ ...theirsById.keys(),
1170
+ ...o.items.map((item) => item.id)
1171
+ ]);
1172
+ const renamed = /* @__PURE__ */ new Map();
1173
+ let next = 1;
1174
+ for (const item of o.items) {
1175
+ const other = theirsById.get(item.id);
1176
+ if (inBase.has(item.id) || !other || sameRelationship(item, other)) continue;
1177
+ while (used.has(`rId${next}`)) next++;
1178
+ renamed.set(item.id, `rId${next}`);
1179
+ used.add(`rId${next}`);
1180
+ item.id = `rId${next}`;
1181
+ }
1182
+ if (!renamed.size) continue;
1183
+ ours.set(name, strToU8(serializeRels(o)));
1184
+ const document = parseXml(strFromU8(ownerBytes));
1185
+ walkElements(document.root, (element) => {
1186
+ element.attrs = element.attrs.map((item) => item.name.namespaceURI === NS.officeDocRels && renamed.has(item.value) ? {
1187
+ name: item.name,
1188
+ value: renamed.get(item.value)
1189
+ } : item);
1190
+ });
1191
+ ours.set(owner, strToU8(serializeXml(document)));
1192
+ }
1193
+ }
1194
+ const sameRelationship = (a, b) => a.type === b.type && a.target === b.target && a.targetMode === b.targetMode;
1195
+ /** The part whose relationships `name` holds; `null` for the package's own. */
1196
+ function relationshipsOwner(name) {
1197
+ const match = /^(.*?)_rels\/([^/]+)\.rels$/.exec(name);
1198
+ return match ? `${match[1]}${match[2]}` : null;
1199
+ }
1200
+ function unzip(bytes) {
1201
+ return new Map(readZip(bytes).entries.map((entry) => [entry.name, entry.data]));
1202
+ }
1203
+ /** True when both packages have the same parts with the same content, as the merge compares them. */
1204
+ function sameDeck(a, b) {
1205
+ const [x, y] = [unzip(a), unzip(b)];
1206
+ if (x.size !== y.size) return false;
1207
+ for (const [name, bytes] of x) {
1208
+ const other = y.get(name);
1209
+ if (!other || !sameBytes(comparablePart(name, bytes), comparablePart(name, other))) return false;
1210
+ }
1211
+ return true;
1212
+ }
1213
+ /**
1214
+ * Three-way merge of presentation packages: `base` is the version the other two
1215
+ * were both made from, `ours` the editor's copy and `theirs` the other copy (a
1216
+ * new source build, an agent's version). Changes on different parts, shapes or
1217
+ * relationships combine; the same item changed differently on both sides is
1218
+ * reported as a conflict.
1219
+ */
1220
+ function mergeDecks(base, ours, theirs) {
1221
+ const [b, o, t] = [
1222
+ unzip(base),
1223
+ unzip(ours),
1224
+ unzip(theirs)
1225
+ ];
1226
+ renumberAddedRelationships(b, o, t);
1227
+ const names = new Set([
1228
+ ...t.keys(),
1229
+ ...o.keys(),
1230
+ ...b.keys()
1231
+ ]);
1232
+ const result = /* @__PURE__ */ new Map();
1233
+ const conflicts = [];
1234
+ const same = (name, x, y) => x === void 0 || y === void 0 ? x === y : sameBytes(comparablePart(name, x), comparablePart(name, y));
1235
+ for (const name of names) {
1236
+ if (name === CONTENT_TYPES) continue;
1237
+ const [bp, op, tp] = [
1238
+ b.get(name),
1239
+ o.get(name),
1240
+ t.get(name)
1241
+ ];
1242
+ let pick;
1243
+ if (same(name, op, bp)) pick = tp;
1244
+ else if (same(name, tp, bp) || same(name, op, tp)) pick = op;
1245
+ else {
1246
+ const context = { part: name };
1247
+ let merged = null;
1248
+ if (op && tp && RELATIONSHIPS_PART.test(name)) merged = mergeRelationshipsPart(context, bp, op, tp);
1249
+ else if (bp && op && tp && SHAPE_TREE_PART.test(name)) merged = mergeShapeTreePart(context, bp, op, tp);
1250
+ if (merged?.ok) result.set(name, merged.value);
1251
+ else if (merged) conflicts.push(...merged.conflicts);
1252
+ else conflicts.push(partConflict(context, !bp ? "both-added" : !op || !tp ? "deleted-and-changed" : "both-changed"));
1253
+ continue;
1254
+ }
1255
+ if (pick) result.set(name, pick);
1256
+ }
1257
+ const [bt, ot, tt] = [
1258
+ b,
1259
+ o,
1260
+ t
1261
+ ].map((parts) => parts.get(CONTENT_TYPES));
1262
+ let contentTypes;
1263
+ if (same(CONTENT_TYPES, ot, bt)) contentTypes = tt;
1264
+ else if (same(CONTENT_TYPES, tt, bt) || same(CONTENT_TYPES, ot, tt)) contentTypes = ot;
1265
+ else {
1266
+ const merged = mergeContentTypes(ot, tt, new Set([...result.keys()].map((name) => `/${name}`.toLowerCase())));
1267
+ if (merged.ok) contentTypes = merged.value;
1268
+ else conflicts.push(...merged.conflicts);
1269
+ }
1270
+ if (conflicts.length) {
1271
+ const numbers = [
1272
+ b,
1273
+ t,
1274
+ o
1275
+ ].reduce((all, parts) => new Map([...all, ...slideNumbers(parts)]), /* @__PURE__ */ new Map());
1276
+ return {
1277
+ ok: false,
1278
+ conflicts: conflicts.map((conflict) => ({
1279
+ ...conflict,
1280
+ slide: numbers.get(conflict.part) ?? null
1281
+ }))
1282
+ };
1283
+ }
1284
+ return {
1285
+ ok: true,
1286
+ bytes: writeZip([{
1287
+ name: CONTENT_TYPES,
1288
+ data: contentTypes
1289
+ }, ...[...result].map(([name, data]) => ({
1290
+ name,
1291
+ data
1292
+ }))])
1293
+ };
1294
+ }
1295
+ /** One line per conflict, for the CLI and server messages. */
1296
+ function describeConflict(conflict) {
1297
+ const where = conflict.slide === null ? conflict.part : `Slide ${conflict.slide}`;
1298
+ const what = conflict.shape ? `${where}: ${conflict.shape.name || `shape ${conflict.shape.id}`}` : where;
1299
+ return conflict.reason === "deleted-and-changed" ? `${what} was deleted on one side and changed on the other.` : `${what} was changed both in the editor and in the source.`;
1300
+ }
1301
+ //#endregion
1302
+ export { sameDeck as i, describeConflict as n, mergeDecks as r, comparablePart as t };
1303
+
1304
+ //# sourceMappingURL=deck-merge-XZc2oNxd.js.map