@deckflow/deckuse-pptx 0.1.0 → 1.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.
Files changed (79) hide show
  1. package/LICENSE +662 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapter.d.ts +70 -3
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +519 -86
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/addressing.d.ts +45 -0
  8. package/dist/addressing.d.ts.map +1 -0
  9. package/dist/addressing.js +401 -0
  10. package/dist/addressing.js.map +1 -0
  11. package/dist/chart-classify.d.ts +10 -0
  12. package/dist/chart-classify.d.ts.map +1 -0
  13. package/dist/chart-classify.js +54 -0
  14. package/dist/chart-classify.js.map +1 -0
  15. package/dist/chart.d.ts +33 -0
  16. package/dist/chart.d.ts.map +1 -0
  17. package/dist/chart.js +492 -0
  18. package/dist/chart.js.map +1 -0
  19. package/dist/edition.d.ts +20 -0
  20. package/dist/edition.d.ts.map +1 -0
  21. package/dist/edition.js +46 -0
  22. package/dist/edition.js.map +1 -0
  23. package/dist/elements.d.ts.map +1 -1
  24. package/dist/elements.js +61 -46
  25. package/dist/elements.js.map +1 -1
  26. package/dist/hyperlink.d.ts +7 -0
  27. package/dist/hyperlink.d.ts.map +1 -0
  28. package/dist/hyperlink.js +83 -0
  29. package/dist/hyperlink.js.map +1 -0
  30. package/dist/index-sync.d.ts +7 -0
  31. package/dist/index-sync.d.ts.map +1 -0
  32. package/dist/index-sync.js +24 -0
  33. package/dist/index-sync.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/indexer.d.ts +4 -0
  39. package/dist/indexer.d.ts.map +1 -1
  40. package/dist/indexer.js +105 -27
  41. package/dist/indexer.js.map +1 -1
  42. package/dist/media.d.ts +15 -0
  43. package/dist/media.d.ts.map +1 -0
  44. package/dist/media.js +141 -0
  45. package/dist/media.js.map +1 -0
  46. package/dist/mutations.d.ts +8 -2
  47. package/dist/mutations.d.ts.map +1 -1
  48. package/dist/mutations.js +695 -72
  49. package/dist/mutations.js.map +1 -1
  50. package/dist/picture.d.ts.map +1 -1
  51. package/dist/picture.js +1 -3
  52. package/dist/picture.js.map +1 -1
  53. package/dist/properties.d.ts +6 -1
  54. package/dist/properties.d.ts.map +1 -1
  55. package/dist/properties.js +159 -10
  56. package/dist/properties.js.map +1 -1
  57. package/dist/resolve-properties.d.ts +17 -0
  58. package/dist/resolve-properties.d.ts.map +1 -0
  59. package/dist/resolve-properties.js +545 -0
  60. package/dist/resolve-properties.js.map +1 -0
  61. package/dist/slides.d.ts +3 -1
  62. package/dist/slides.d.ts.map +1 -1
  63. package/dist/slides.js +77 -4
  64. package/dist/slides.js.map +1 -1
  65. package/dist/table.d.ts +15 -0
  66. package/dist/table.d.ts.map +1 -0
  67. package/dist/table.js +456 -0
  68. package/dist/table.js.map +1 -0
  69. package/dist/types.d.ts +2 -1
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/workspace.d.ts +9 -9
  72. package/dist/workspace.d.ts.map +1 -1
  73. package/dist/workspace.js +67 -47
  74. package/dist/workspace.js.map +1 -1
  75. package/dist/xml.d.ts +7 -0
  76. package/dist/xml.d.ts.map +1 -1
  77. package/dist/xml.js +86 -13
  78. package/dist/xml.js.map +1 -1
  79. package/package.json +12 -4
package/dist/mutations.js CHANGED
@@ -1,57 +1,293 @@
1
1
  import { err, ok, } from '@deckflow/deckuse-core';
2
+ import { resolveTarget, resolveToRef, cNvPrIdOf } from './addressing.js';
3
+ import { assertWritable } from './edition.js';
2
4
  import { addElement, duplicateElement, updateChart } from './elements.js';
3
- import { findIndexed, matchesSelector } from './indexer.js';
4
- import { detachPictureAndCleanup, loadPictureBytes, replacePictureMedia, } from './picture.js';
5
+ import { findIndexed, matchesSelector, mergeSlides, slidesForItem } from './indexer.js';
6
+ import { detachPictureAndCleanup, loadPictureBytes, replacePictureMedia } from './picture.js';
7
+ import { detachMediaAndCleanup } from './media.js';
5
8
  import { applyShapeProperties, assertChartProperties } from './properties.js';
6
- import { addSlide, duplicateSlide, removeSlide } from './slides.js';
7
- import { attr, cNvPr, descendants, first, root, setNodeText } from './xml.js';
8
- const nodeFor = (doc, item) => {
9
+ import { mapDottedProperties } from './resolve-properties.js';
10
+ import { addSlide, duplicateSlide, ensureNotes, removeSlide } from './slides.js';
11
+ import { applyTableCellProperties, applyTableProperties } from './table.js';
12
+ import { REL, NS, attr, children, cNvPr, descendants, first, root, setNodeText } from './xml.js';
13
+ const SHAPE_LOCAL_NAMES = new Set(['sp', 'pic', 'graphicFrame', 'cxnSp', 'grpSp']);
14
+ const directChild = (node, localName) => children(node).find((child) => child.localName === localName);
15
+ export const shapeByCNvPrId = (doc, id) => descendants(doc).find((node) => node.localName != null &&
16
+ SHAPE_LOCAL_NAMES.has(node.localName) &&
17
+ attr(cNvPr(node), 'id') === id);
18
+ export const nodeFor = (doc, item) => {
9
19
  if (['slide', 'notes', 'master', 'layout', 'theme'].includes(item.kind))
10
20
  return root(doc);
11
21
  if (item.kind === 'tableCell') {
12
- const rawTableId = item.location?.['tableId'], tableId = typeof rawTableId === 'string' ? rawTableId : '', tableTail = tableId.split('.').at(-1), table = descendants(doc).find((n) => attr(cNvPr(n), 'id') === tableTail);
22
+ const rawTableId = item.location?.['tableId'], tableId = typeof rawTableId === 'string' ? rawTableId : '';
23
+ // elementId is `${slideId}:${ancestorPath}` — strip slide prefix, then take the leaf cNvPr id.
24
+ const afterSlide = tableId.includes(':') ? tableId.slice(tableId.indexOf(':') + 1) : tableId;
25
+ const tableTail = afterSlide.split('.').at(-1) ?? afterSlide;
26
+ if (!tableTail)
27
+ return undefined;
28
+ const table = shapeByCNvPrId(doc, tableTail);
13
29
  if (!table)
14
30
  return;
15
31
  const row = descendants(table, 'tr')[Number(item.location?.['row'])];
16
32
  return row ? descendants(row, 'tc')[Number(item.location?.['column'])] : undefined;
17
33
  }
18
- // elementId is `${slideId}:${ancestors.cNvPrId joined by .}`; cNvPr ids are the leaf only.
19
- const raw = item.location?.['cNvPrId'];
20
- const tail = typeof raw === 'string'
21
- ? raw
22
- : item.ref.elementId?.includes(':')
23
- ? item.ref.elementId.split(':').slice(1).join(':').split('.').at(-1)
24
- : item.ref.elementId?.split('.').at(-1);
25
- return descendants(doc).find((n) => attr(cNvPr(n), 'id') === tail);
26
- };
27
- const transform = (node, t) => {
28
- const x = first(node, 'xfrm');
29
- if (!x)
30
- throw new Error('Element has no transform');
34
+ const id = cNvPrIdOf(item);
35
+ return id ? shapeByCNvPrId(doc, id) : undefined;
36
+ };
37
+ const placeholderOf = (node) => {
38
+ const ph = first(node, 'ph');
39
+ if (!ph)
40
+ return undefined;
41
+ const type = attr(ph, 'type') ?? 'body';
42
+ const idx = attr(ph, 'idx');
43
+ return { type, ...(idx !== undefined ? { idx } : {}) };
44
+ };
45
+ const findPlaceholderXfrm = (doc, type, idx) => {
46
+ for (const shape of descendants(doc)) {
47
+ if (!shape.localName || !SHAPE_LOCAL_NAMES.has(shape.localName))
48
+ continue;
49
+ const ph = placeholderOf(shape);
50
+ if (!ph)
51
+ continue;
52
+ if (idx !== undefined && ph.idx === idx) {
53
+ const xfrm = first(shape, 'xfrm');
54
+ if (xfrm)
55
+ return xfrm;
56
+ continue;
57
+ }
58
+ if (ph.type === type && (idx === undefined || ph.idx === undefined)) {
59
+ const xfrm = first(shape, 'xfrm');
60
+ if (xfrm)
61
+ return xfrm;
62
+ }
63
+ }
64
+ return undefined;
65
+ };
66
+ /** Layout/master xfrm for placeholders that omit a local transform. */
67
+ const inheritedXfrm = (archive, slidePart, node) => {
68
+ const ph = placeholderOf(node);
69
+ if (!ph)
70
+ return undefined;
71
+ const layoutPart = archive
72
+ .getRelationships(slidePart)
73
+ .find((rel) => rel.type === REL.layout)?.resolvedTarget;
74
+ if (!layoutPart || !archive.getPart(layoutPart))
75
+ return undefined;
76
+ const fromLayout = findPlaceholderXfrm(archive.readXml(layoutPart), ph.type, ph.idx);
77
+ if (fromLayout)
78
+ return fromLayout;
79
+ const masterPart = archive
80
+ .getRelationships(layoutPart)
81
+ .find((rel) => rel.type === REL.slideMaster)?.resolvedTarget;
82
+ if (!masterPart || !archive.getPart(masterPart))
83
+ return undefined;
84
+ return findPlaceholderXfrm(archive.readXml(masterPart), ph.type, ph.idx);
85
+ };
86
+ const copyXfrmInto = (doc, seed, ns, qname) => {
87
+ const x = doc.createElementNS(ns, qname);
88
+ for (const name of ['rot', 'flipH', 'flipV']) {
89
+ const value = seed.getAttribute(name);
90
+ if (value != null)
91
+ x.setAttribute(name, value);
92
+ }
93
+ for (const [local, q] of [
94
+ ['off', 'a:off'],
95
+ ['ext', 'a:ext'],
96
+ ['chOff', 'a:chOff'],
97
+ ['chExt', 'a:chExt'],
98
+ ]) {
99
+ const src = directChild(seed, local) ?? first(seed, local);
100
+ if (!src)
101
+ continue;
102
+ const child = doc.createElementNS(NS.a, q);
103
+ for (const name of ['x', 'y', 'cx', 'cy']) {
104
+ const value = src.getAttribute(name);
105
+ if (value != null)
106
+ child.setAttribute(name, value);
107
+ }
108
+ x.appendChild(child);
109
+ }
110
+ return x;
111
+ };
112
+ const ensureSpPrForTransform = (node) => {
113
+ const existing = directChild(node, 'spPr');
114
+ if (existing)
115
+ return existing;
31
116
  const doc = node.ownerDocument;
32
117
  if (!doc)
33
118
  throw new Error('Element has no document');
34
- let off = first(x, 'off');
35
- if (!off) {
36
- off = doc.createElementNS(x.namespaceURI, 'a:off');
37
- x.appendChild(off);
38
- }
39
- let ext = first(x, 'ext');
40
- if (!ext) {
41
- ext = doc.createElementNS(x.namespaceURI, 'a:ext');
42
- x.appendChild(ext);
43
- }
44
- for (const [element, key, input, scale] of [
45
- [off, 'x', 'x', 1],
46
- [off, 'y', 'y', 1],
47
- [ext, 'cx', 'width', 1],
48
- [ext, 'cy', 'height', 1],
49
- [x, 'rot', 'rotation', 60000],
50
- ])
51
- if (typeof t[input] === 'number')
52
- element.setAttribute(key, String(Math.round(t[input] * scale)));
119
+ const prefix = node.prefix ? `${node.prefix}:` : 'p:';
120
+ const spPr = doc.createElementNS(NS.p, `${prefix}spPr`);
121
+ const after = children(node).find((child) => child.localName?.startsWith('nv'));
122
+ if (after?.nextSibling)
123
+ node.insertBefore(spPr, after.nextSibling);
124
+ else if (after)
125
+ node.appendChild(spPr);
126
+ else if (node.firstChild)
127
+ node.insertBefore(spPr, node.firstChild);
128
+ else
129
+ node.appendChild(spPr);
130
+ return spPr;
131
+ };
132
+ const ensureGrpSpPr = (node) => {
133
+ const existing = directChild(node, 'grpSpPr');
134
+ if (existing)
135
+ return existing;
136
+ const doc = node.ownerDocument;
137
+ if (!doc)
138
+ throw new Error('Element has no document');
139
+ const prefix = node.prefix ? `${node.prefix}:` : 'p:';
140
+ const grpSpPr = doc.createElementNS(NS.p, `${prefix}grpSpPr`);
141
+ const after = children(node).find((child) => child.localName?.startsWith('nv'));
142
+ if (after?.nextSibling)
143
+ node.insertBefore(grpSpPr, after.nextSibling);
144
+ else if (after)
145
+ node.appendChild(grpSpPr);
146
+ else if (node.firstChild)
147
+ node.insertBefore(grpSpPr, node.firstChild);
148
+ else
149
+ node.appendChild(grpSpPr);
150
+ return grpSpPr;
151
+ };
152
+ const ensureXfrm = (node, archive, partUri) => {
153
+ const existing = (() => {
154
+ if (node.localName === 'graphicFrame')
155
+ return directChild(node, 'xfrm');
156
+ if (node.localName === 'grpSp') {
157
+ const grpSpPr = directChild(node, 'grpSpPr');
158
+ return grpSpPr ? directChild(grpSpPr, 'xfrm') : undefined;
159
+ }
160
+ const spPr = directChild(node, 'spPr');
161
+ return spPr ? directChild(spPr, 'xfrm') : undefined;
162
+ })();
163
+ if (existing)
164
+ return existing;
165
+ const doc = node.ownerDocument;
166
+ if (!doc)
167
+ throw new Error('Element has no document');
168
+ const seed = archive && partUri ? inheritedXfrm(archive, partUri, node) : undefined;
169
+ const usePresentationXfrm = node.localName === 'graphicFrame';
170
+ const ns = usePresentationXfrm ? NS.p : NS.a;
171
+ const qname = usePresentationXfrm ? 'p:xfrm' : 'a:xfrm';
172
+ const x = seed ? copyXfrmInto(doc, seed, ns, qname) : doc.createElementNS(ns, qname);
173
+ if (node.localName === 'graphicFrame') {
174
+ const after = children(node).find((child) => child.localName?.startsWith('nv'));
175
+ if (after?.nextSibling)
176
+ node.insertBefore(x, after.nextSibling);
177
+ else if (after)
178
+ node.appendChild(x);
179
+ else if (node.firstChild)
180
+ node.insertBefore(x, node.firstChild);
181
+ else
182
+ node.appendChild(x);
183
+ return x;
184
+ }
185
+ const container = node.localName === 'grpSp' ? ensureGrpSpPr(node) : ensureSpPrForTransform(node);
186
+ if (container.firstChild)
187
+ container.insertBefore(x, container.firstChild);
188
+ else
189
+ container.appendChild(x);
190
+ return x;
191
+ };
192
+ const ensureChild = (parent, localName, qname, beforeLocalNames) => {
193
+ const existing = directChild(parent, localName);
194
+ if (existing)
195
+ return existing;
196
+ const doc = parent.ownerDocument;
197
+ if (!doc)
198
+ throw new Error('Element has no document');
199
+ const child = doc.createElementNS(NS.a, qname);
200
+ const before = children(parent).find((item) => item.localName != null && beforeLocalNames.includes(item.localName));
201
+ if (before)
202
+ parent.insertBefore(child, before);
203
+ else
204
+ parent.appendChild(child);
205
+ return child;
206
+ };
207
+ const transform = (node, t, opts) => {
208
+ const x = ensureXfrm(node, opts?.archive, opts?.partUri);
209
+ const needsOff = typeof t['x'] === 'number' || typeof t['y'] === 'number';
210
+ const needsExt = typeof t['width'] === 'number' || typeof t['height'] === 'number';
211
+ const off = needsOff
212
+ ? ensureChild(x, 'off', 'a:off', ['ext', 'chOff', 'chExt'])
213
+ : (directChild(x, 'off') ?? first(x, 'off'));
214
+ const ext = needsExt
215
+ ? ensureChild(x, 'ext', 'a:ext', ['chOff', 'chExt'])
216
+ : (directChild(x, 'ext') ?? first(x, 'ext'));
217
+ if (needsOff && off) {
218
+ if (typeof t['x'] === 'number')
219
+ off.setAttribute('x', String(Math.round(t['x'])));
220
+ else if (!off.hasAttribute('x'))
221
+ off.setAttribute('x', '0');
222
+ if (typeof t['y'] === 'number')
223
+ off.setAttribute('y', String(Math.round(t['y'])));
224
+ else if (!off.hasAttribute('y'))
225
+ off.setAttribute('y', '0');
226
+ }
227
+ if (needsExt && ext) {
228
+ if (typeof t['width'] === 'number')
229
+ ext.setAttribute('cx', String(Math.round(t['width'])));
230
+ else if (!ext.hasAttribute('cx'))
231
+ ext.setAttribute('cx', '0');
232
+ if (typeof t['height'] === 'number')
233
+ ext.setAttribute('cy', String(Math.round(t['height'])));
234
+ else if (!ext.hasAttribute('cy'))
235
+ ext.setAttribute('cy', '0');
236
+ }
237
+ if (typeof t['rotation'] === 'number')
238
+ x.setAttribute('rot', String(Math.round(t['rotation'] * 60000)));
239
+ if (typeof t['flipHorizontal'] === 'boolean' || typeof t['flipX'] === 'boolean') {
240
+ const flip = Boolean(t['flipHorizontal'] ?? t['flipX']);
241
+ if (flip)
242
+ x.setAttribute('flipH', '1');
243
+ else
244
+ x.removeAttribute('flipH');
245
+ }
246
+ if (typeof t['flipVertical'] === 'boolean' || typeof t['flipY'] === 'boolean') {
247
+ const flip = Boolean(t['flipVertical'] ?? t['flipY']);
248
+ if (flip)
249
+ x.setAttribute('flipV', '1');
250
+ else
251
+ x.removeAttribute('flipV');
252
+ }
253
+ };
254
+ const indexSlidePartForNotes = (archive, item) => {
255
+ if (!item.slideId)
256
+ return undefined;
257
+ const presentation = archive.readXml('/ppt/presentation.xml');
258
+ const rels = archive.getRelationships('/ppt/presentation.xml');
259
+ for (const sld of descendants(presentation, 'sldId')) {
260
+ const id = attr(sld, 'id');
261
+ if (id !== item.slideId)
262
+ continue;
263
+ const rid = sld.getAttributeNS(NS.r, 'id') ?? attr(sld, 'r:id');
264
+ const rel = rels.find((r) => r.id === rid);
265
+ return rel?.resolvedTarget;
266
+ }
267
+ return undefined;
268
+ };
269
+ const materializeNotes = (archive, item) => {
270
+ if (item.kind !== 'notes')
271
+ return ok(item);
272
+ if (item.partUri && archive.getPart(item.partUri))
273
+ return ok(item);
274
+ const slidePart = typeof item.location?.['slidePart'] === 'string'
275
+ ? item.location['slidePart']
276
+ : indexSlidePartForNotes(archive, item);
277
+ if (!slidePart)
278
+ return err('ELEMENT_NOT_FOUND', `Cannot locate slide for notes ${item.ref.elementId ?? ''}`);
279
+ const partUri = ensureNotes(archive, slidePart);
280
+ return ok({
281
+ ...item,
282
+ partUri,
283
+ ref: { ...item.ref, path: partUri },
284
+ location: { ...item.location, partUri, slidePart, region: 'speakerNotes' },
285
+ });
53
286
  };
54
287
  const writeText = (archive, item, text, diagnostics) => {
288
+ const gated = assertWritable(item, archive);
289
+ if (!gated.ok)
290
+ return gated;
55
291
  if (item.kind === 'chart' && typeof item.payload?.['chartPart'] === 'string') {
56
292
  const result = updateChart(archive, item.payload['chartPart'], { title: text });
57
293
  if (result.workbook)
@@ -62,13 +298,34 @@ const writeText = (archive, item, text, diagnostics) => {
62
298
  });
63
299
  return ok(undefined, diagnostics);
64
300
  }
65
- const doc = archive.readXml(item.partUri), node = nodeFor(doc, item);
301
+ const notesItem = materializeNotes(archive, item);
302
+ if (!notesItem.ok)
303
+ return notesItem;
304
+ const target = notesItem.value;
305
+ const doc = archive.readXml(target.partUri), node = nodeFor(doc, target);
66
306
  if (!node)
67
- return err('ELEMENT_NOT_FOUND', `Element XML node was not found: ${item.ref.elementId ?? item.ref.path ?? ''}`);
307
+ return err('ELEMENT_NOT_FOUND', `Element XML node was not found: ${target.ref.elementId ?? target.ref.path ?? ''}`);
68
308
  setNodeText(node, text);
69
- archive.writeXml(item.partUri, doc, archive.getPart(item.partUri)?.mediaType);
309
+ archive.writeXml(target.partUri, doc);
70
310
  return ok(undefined, diagnostics);
71
311
  };
312
+ const isDescendantOf = (item, ancestorId) => {
313
+ if (item.parentId === ancestorId)
314
+ return true;
315
+ const elementId = item.ref.elementId;
316
+ return Boolean(elementId?.startsWith(`${ancestorId}.`));
317
+ };
318
+ /** Without a selector, skip ancestor elements whose text aggregates descendant matches. */
319
+ const narrowReplaceTextCandidates = (candidates) => {
320
+ if (candidates.length <= 1)
321
+ return candidates;
322
+ return candidates.filter((item) => {
323
+ const id = item.ref.elementId;
324
+ if (!id)
325
+ return true;
326
+ return !candidates.some((other) => other.ref.elementId !== id && isDescendantOf(other, id));
327
+ });
328
+ };
72
329
  const applyReplaceText = (command, archive, index) => {
73
330
  if (command.regex) {
74
331
  try {
@@ -82,13 +339,16 @@ const applyReplaceText = (command, archive, index) => {
82
339
  const replaced = (text) => command.regex
83
340
  ? text.replace(new RegExp(command.find, 'gu'), command.replace)
84
341
  : text.split(command.find).join(command.replace);
85
- const candidates = index.elements.filter((item) => Boolean(item.text?.length) &&
342
+ let candidates = index.elements.filter((item) => Boolean(item.text?.length) &&
86
343
  (!command.selector || matchesSelector(item, command.selector)) &&
87
344
  matches(item.text ?? ''));
345
+ if (!command.selector)
346
+ candidates = narrowReplaceTextCandidates(candidates);
88
347
  const limit = command.limit ?? 1000;
89
348
  const selected = candidates.slice(0, limit);
90
349
  const diagnostics = [];
91
350
  const refs = [];
351
+ const slidePages = [];
92
352
  for (const item of selected) {
93
353
  const next = replaced(item.text ?? '');
94
354
  if (next === item.text)
@@ -97,28 +357,319 @@ const applyReplaceText = (command, archive, index) => {
97
357
  if (!written.ok)
98
358
  return written;
99
359
  refs.push(item.ref);
360
+ slidePages.push(...slidesForItem(index, item));
100
361
  }
101
- return ok({ changed: refs.length > 0, matched: refs.length, refs, diagnostics }, diagnostics);
362
+ return ok({
363
+ changed: refs.length > 0,
364
+ matched: refs.length,
365
+ refs,
366
+ slides: mergeSlides(slidePages),
367
+ diagnostics,
368
+ }, diagnostics);
102
369
  };
103
- export async function mutate(command, archive, index) {
370
+ const resolveCommandRef = (command, index) => {
104
371
  if (command.type === 'replaceText')
105
- return applyReplaceText(command, archive, index);
106
- const ref = command.type === 'add' ? command.parent : command.ref;
107
- if (ref.revision && ref.revision !== index.revision)
108
- return err('TRANSACTION_CONFLICT', `Expected revision ${ref.revision}, current ${index.revision}`);
109
- const item = findIndexed(index, ref);
372
+ return err('INVALID_COMMAND', 'replaceText does not resolve a single ref');
373
+ if (command.type === 'addSlide' || command.type === 'addShape')
374
+ return err('INVALID_COMMAND', 'addSlide/addShape resolve separately');
375
+ if (command.type === 'xfrmSet') {
376
+ const target = command.target ??
377
+ (command.slide !== undefined && command.shape !== undefined
378
+ ? `slide:${command.slide}/shape:${command.shape}`
379
+ : undefined);
380
+ if (!target)
381
+ return err('INVALID_COMMAND', 'xfrmSet requires target or slide+shape');
382
+ const resolved = resolveTarget(index, target);
383
+ if (!resolved.ok)
384
+ return resolved;
385
+ return {
386
+ ok: true,
387
+ value: {
388
+ ref: resolved.value.item.ref,
389
+ item: resolved.value.item,
390
+ target: resolved.value.target,
391
+ },
392
+ diagnostics: [],
393
+ };
394
+ }
395
+ if (command.type === 'zMove' || command.type === 'set') {
396
+ const resolved = resolveTarget(index, command.target);
397
+ if (!resolved.ok)
398
+ return resolved;
399
+ return {
400
+ ok: true,
401
+ value: {
402
+ ref: resolved.value.item.ref,
403
+ item: resolved.value.item,
404
+ target: resolved.value.target,
405
+ },
406
+ diagnostics: [],
407
+ };
408
+ }
409
+ if (command.type === 'add') {
410
+ const got = resolveToRef(index, {
411
+ ...(command.parent !== undefined ? { ref: command.parent } : {}),
412
+ ...(command.target !== undefined ? { target: command.target } : {}),
413
+ });
414
+ if (!got.ok)
415
+ return got;
416
+ const item = got.value.resolved?.item ?? findIndexed(index, got.value.ref);
417
+ if (!item)
418
+ return err('ELEMENT_NOT_FOUND', 'Element reference was not found');
419
+ return {
420
+ ok: true,
421
+ value: {
422
+ ref: item.ref,
423
+ item,
424
+ ...(got.value.resolved?.target !== undefined
425
+ ? { target: got.value.resolved.target }
426
+ : {}),
427
+ },
428
+ diagnostics: [],
429
+ };
430
+ }
431
+ const got = resolveToRef(index, {
432
+ ...('ref' in command && command.ref !== undefined ? { ref: command.ref } : {}),
433
+ ...('target' in command && command.target !== undefined ? { target: command.target } : {}),
434
+ });
435
+ if (!got.ok)
436
+ return got;
437
+ // Prefer the item from target resolution so synthetic notes (not yet in the index) work.
438
+ const item = got.value.resolved?.item ?? findIndexed(index, got.value.ref);
110
439
  if (!item)
111
440
  return err('ELEMENT_NOT_FOUND', 'Element reference was not found');
441
+ return {
442
+ ok: true,
443
+ value: {
444
+ ref: item.ref,
445
+ item,
446
+ ...(got.value.resolved?.target !== undefined ? { target: got.value.resolved.target } : {}),
447
+ },
448
+ diagnostics: [],
449
+ };
450
+ };
451
+ const shapeTypeToElement = (shapeType, fields) => {
452
+ const base = { ...fields };
453
+ switch (shapeType) {
454
+ case 'text':
455
+ return {
456
+ ...base,
457
+ kind: 'textbox',
458
+ type: 'textbox',
459
+ preset: 'rect',
460
+ text: typeof fields['text'] === 'string' ? fields['text'] : '',
461
+ };
462
+ case 'rect':
463
+ return { ...base, kind: 'shape', type: 'shape', preset: 'rect' };
464
+ case 'rounded-rect':
465
+ return { ...base, kind: 'shape', type: 'shape', preset: 'roundRect' };
466
+ case 'ellipse':
467
+ return { ...base, kind: 'shape', type: 'shape', preset: 'ellipse' };
468
+ case 'line':
469
+ return { ...base, kind: 'connector', type: 'connector' };
470
+ case 'image':
471
+ return {
472
+ ...base,
473
+ kind: 'picture',
474
+ type: 'picture',
475
+ ...(typeof fields['file'] === 'string' ? { path: fields['file'] } : {}),
476
+ };
477
+ case 'group':
478
+ return { ...base, kind: 'group', type: 'group' };
479
+ case 'table':
480
+ return {
481
+ ...base,
482
+ kind: 'table',
483
+ type: 'table',
484
+ rows: Array.isArray(fields['rows']) ? fields['rows'] : [['']],
485
+ };
486
+ case 'chart':
487
+ return {
488
+ ...base,
489
+ kind: 'chart',
490
+ type: 'chart',
491
+ ...(typeof fields['chartType'] === 'string' ? { chartType: fields['chartType'] } : {}),
492
+ ...(fields['data'] !== undefined ? { data: fields['data'] } : {}),
493
+ };
494
+ case 'video':
495
+ return {
496
+ ...base,
497
+ kind: 'video',
498
+ type: 'video',
499
+ ...(typeof fields['file'] === 'string' ? { path: fields['file'] } : {}),
500
+ };
501
+ case 'audio':
502
+ return {
503
+ ...base,
504
+ kind: 'audio',
505
+ type: 'audio',
506
+ ...(typeof fields['file'] === 'string' ? { path: fields['file'] } : {}),
507
+ };
508
+ }
509
+ };
510
+ const applyZMove = (doc, node, command, index) => {
511
+ const parent = node.parentNode;
512
+ if (!parent)
513
+ return err('INVALID_COMMAND', 'Shape has no parent for z-order move');
514
+ const siblings = children(parent).filter((child) => child.localName != null && SHAPE_LOCAL_NAMES.has(child.localName));
515
+ if (!siblings.includes(node))
516
+ return err('INVALID_COMMAND', 'Shape is not a direct child of the shape tree');
517
+ const findSibling = (target) => {
518
+ const resolved = resolveTarget(index, target);
519
+ if (!resolved.ok)
520
+ return resolved;
521
+ const other = nodeFor(doc, resolved.value.item);
522
+ if (!other || !siblings.includes(other))
523
+ return err('TARGET_NOT_FOUND', `z-order reference not on same parent: ${target}`, [], {
524
+ target,
525
+ });
526
+ return { ok: true, value: other, diagnostics: [] };
527
+ };
528
+ parent.removeChild(node);
529
+ if (command.toFront) {
530
+ parent.appendChild(node);
531
+ return ok(undefined);
532
+ }
533
+ if (command.toBack) {
534
+ const firstShape = children(parent).find((child) => child.localName != null && SHAPE_LOCAL_NAMES.has(child.localName));
535
+ if (firstShape)
536
+ parent.insertBefore(node, firstShape);
537
+ else
538
+ parent.appendChild(node);
539
+ return ok(undefined);
540
+ }
541
+ if (command.above) {
542
+ const other = findSibling(command.above);
543
+ if (!other.ok)
544
+ return other;
545
+ if (other.value.nextSibling)
546
+ parent.insertBefore(node, other.value.nextSibling);
547
+ else
548
+ parent.appendChild(node);
549
+ return ok(undefined);
550
+ }
551
+ if (command.below) {
552
+ const other = findSibling(command.below);
553
+ if (!other.ok)
554
+ return other;
555
+ parent.insertBefore(node, other.value);
556
+ return ok(undefined);
557
+ }
558
+ return err('INVALID_COMMAND', 'zMove requires --above, --below, --to-front, or --to-back');
559
+ };
560
+ export async function mutate(command, archive, index) {
561
+ if (command.type === 'replaceText')
562
+ return applyReplaceText(command, archive, index);
563
+ if (command.type === 'addSlide') {
564
+ const slides = index.elements.filter((item) => item.kind === 'slide');
565
+ let layout;
566
+ if (command.layout) {
567
+ const layouts = index.elements.filter((item) => item.kind === 'layout');
568
+ const needle = command.layout.toLowerCase();
569
+ const layoutDisplayName = (partUri) => {
570
+ try {
571
+ const cSld = first(archive.readXml(partUri), 'cSld');
572
+ return (attr(cSld, 'name') ?? '').toLowerCase();
573
+ }
574
+ catch {
575
+ return '';
576
+ }
577
+ };
578
+ const match = layouts.find((item) => {
579
+ const fileName = item.partUri.split('/').pop()?.replace(/\.xml$/, '') ?? '';
580
+ const display = layoutDisplayName(item.partUri);
581
+ // Match by cSld@name (e.g. "Blank") or part basename — never treat
582
+ // needle==="blank" as matching every layout (that picked Title Slide).
583
+ return (display === needle ||
584
+ fileName.toLowerCase() === needle ||
585
+ (needle !== 'blank' &&
586
+ (display.includes(needle) || fileName.toLowerCase().includes(needle))));
587
+ });
588
+ if (!match && needle !== 'blank')
589
+ return err('TARGET_NOT_FOUND', `Layout not found: ${command.layout}`);
590
+ layout = match?.partUri;
591
+ }
592
+ // Prefer an explicit layout; otherwise inherit the layout of the anchor slide
593
+ // (or the first slide) so blank pages still bind to a slideLayout.
594
+ if (!layout) {
595
+ const anchor = command.after !== undefined && command.after > 0
596
+ ? slides[command.after - 1]
597
+ : slides[0];
598
+ if (anchor) {
599
+ const rels = archive.getRelationships(anchor.partUri);
600
+ const layoutRel = rels.find((r) => r.type === REL.layout);
601
+ layout = layoutRel?.resolvedTarget;
602
+ }
603
+ }
604
+ const partUri = addSlide(archive, undefined, layout, command.after);
605
+ return ok({
606
+ changed: true,
607
+ partUri,
608
+ slides: [],
609
+ changedParts: [partUri, '/ppt/presentation.xml'],
610
+ });
611
+ }
612
+ if (command.type === 'addShape') {
613
+ const slides = index.elements.filter((item) => item.kind === 'slide');
614
+ const slide = slides[command.slide - 1];
615
+ if (!slide)
616
+ return err('TARGET_NOT_FOUND', `slide:${command.slide} does not exist`, [], {
617
+ target: `slide:${command.slide}`,
618
+ });
619
+ const doc = archive.readXml(slide.partUri);
620
+ const parent = first(doc, 'spTree') ?? root(doc);
621
+ const element = shapeTypeToElement(command.shapeType, {
622
+ ...(command.name !== undefined ? { name: command.name } : {}),
623
+ ...(command.role !== undefined ? { role: command.role } : {}),
624
+ ...(command.x !== undefined ? { x: command.x } : {}),
625
+ ...(command.y !== undefined ? { y: command.y } : {}),
626
+ ...(command.width !== undefined ? { width: command.width } : {}),
627
+ ...(command.height !== undefined ? { height: command.height } : {}),
628
+ ...(command.file !== undefined ? { file: command.file } : {}),
629
+ ...(command.text !== undefined ? { text: command.text } : {}),
630
+ ...(command.rows !== undefined ? { rows: command.rows } : {}),
631
+ ...(command.chartType !== undefined ? { chartType: command.chartType } : {}),
632
+ ...(command.data !== undefined ? { data: command.data } : {}),
633
+ });
634
+ const created = await addElement(archive, slide.partUri, doc, parent, element);
635
+ archive.writeXml(slide.partUri, doc);
636
+ const id = attr(cNvPr(created), 'id') ?? '?';
637
+ return ok({
638
+ changed: true,
639
+ slides: [command.slide],
640
+ changedTargets: [`slide:${command.slide}/shape:${id}`],
641
+ changedParts: [slide.partUri],
642
+ });
643
+ }
644
+ const resolved = resolveCommandRef(command, index);
645
+ if (!resolved.ok)
646
+ return resolved;
647
+ const { item, target } = resolved.value;
648
+ if (item.ref.revision && item.ref.revision !== index.revision)
649
+ return err('TRANSACTION_CONFLICT', `Expected revision ${item.ref.revision}, current ${index.revision}`);
650
+ // Edition capability gate (master/layout/theme/advanced chart writes).
651
+ // Slide add/remove/duplicate and addSlide layout binding are not gated here.
652
+ if (item.kind !== 'slide') {
653
+ const gated = assertWritable(item, archive);
654
+ if (!gated.ok)
655
+ return gated;
656
+ }
112
657
  const diagnostics = [];
113
658
  if (item.kind === 'slide') {
114
659
  if (command.type === 'remove') {
115
660
  removeSlide(archive, item.partUri);
116
- return ok({ changed: true });
661
+ return ok({
662
+ changed: true,
663
+ slides: slidesForItem(index, item),
664
+ ...(target ? { changedTargets: [target] } : {}),
665
+ changedParts: ['/ppt/presentation.xml'],
666
+ });
117
667
  }
118
668
  if (command.type === 'duplicate') {
119
669
  return ok({
120
670
  changed: true,
121
671
  partUri: duplicateSlide(archive, item.partUri),
672
+ slides: slidesForItem(index, item),
122
673
  });
123
674
  }
124
675
  if (command.type === 'add') {
@@ -129,16 +680,32 @@ export async function mutate(command, archive, index) {
129
680
  : undefined, layout = typeof command.element['layoutPart'] === 'string'
130
681
  ? command.element['layoutPart']
131
682
  : undefined;
132
- return ok({ changed: true, partUri: addSlide(archive, template, layout) });
683
+ return ok({ changed: true, partUri: addSlide(archive, template, layout), slides: [] });
133
684
  }
134
685
  }
686
+ if (command.type === 'set' || command.type === 'setProperties') {
687
+ // slide-level name/hidden not fully modeled; treat as unsupported for now except no-op properties
688
+ return err('UNSUPPORTED_CAPABILITY', 'Slide property writes beyond add/remove are limited in Phase 1a', [], target !== undefined ? { target } : {});
689
+ }
135
690
  }
136
- const doc = archive.readXml(item.partUri), node = nodeFor(doc, item);
691
+ const notesReady = materializeNotes(archive, item);
692
+ if (!notesReady.ok)
693
+ return notesReady;
694
+ const liveItem = notesReady.value;
695
+ const doc = archive.readXml(liveItem.partUri), node = nodeFor(doc, liveItem);
137
696
  if (!node)
138
- return err('ELEMENT_NOT_FOUND', `Element XML node was not found: ${item.ref.elementId ?? item.ref.path ?? ''}`);
697
+ return err('ELEMENT_NOT_FOUND', `Element XML node was not found: ${liveItem.ref.elementId ?? liveItem.ref.path ?? ''}`);
698
+ const chartPart = liveItem.kind === 'chart' && typeof liveItem.payload?.['chartPart'] === 'string'
699
+ ? liveItem.payload['chartPart']
700
+ : undefined;
701
+ let chartMutated = false;
139
702
  if (command.type === 'setText') {
140
- if (item.kind === 'chart' && typeof item.payload?.['chartPart'] === 'string') {
141
- const result = updateChart(archive, item.payload['chartPart'], { title: command.text });
703
+ const text = command.text ?? command.value;
704
+ if (text === undefined)
705
+ return err('INVALID_COMMAND', 'setText requires text or value');
706
+ if (chartPart) {
707
+ const result = updateChart(archive, chartPart, { title: text });
708
+ chartMutated = true;
142
709
  if (result.workbook)
143
710
  diagnostics.push({
144
711
  severity: 'warning',
@@ -147,19 +714,34 @@ export async function mutate(command, archive, index) {
147
714
  });
148
715
  }
149
716
  else
150
- setNodeText(node, command.text);
717
+ setNodeText(node, text);
151
718
  }
152
719
  else if (command.type === 'setTransform')
153
- transform(node, command.transform);
154
- else if (command.type === 'setProperties') {
155
- if (item.kind === 'chart' && typeof item.payload?.['chartPart'] === 'string') {
156
- const checked = assertChartProperties(command.properties);
720
+ transform(node, command.transform, { archive, partUri: liveItem.partUri });
721
+ else if (command.type === 'xfrmSet') {
722
+ transform(node, {
723
+ ...(command.x !== undefined ? { x: command.x } : {}),
724
+ ...(command.y !== undefined ? { y: command.y } : {}),
725
+ ...(command.width !== undefined ? { width: command.width } : {}),
726
+ ...(command.height !== undefined ? { height: command.height } : {}),
727
+ ...(command.rotation !== undefined ? { rotation: command.rotation } : {}),
728
+ ...(command.flipX !== undefined ? { flipX: command.flipX } : {}),
729
+ ...(command.flipY !== undefined ? { flipY: command.flipY } : {}),
730
+ }, { archive, partUri: liveItem.partUri });
731
+ }
732
+ else if (command.type === 'set' || command.type === 'setProperties') {
733
+ if (command.type === 'set' && command.scope && command.scope !== 'local')
734
+ return err('UNSUPPORTED_CAPABILITY', `Write scope "${command.scope}" is not enabled in Phase 1a; use --scope local`, [], target !== undefined ? { target } : {});
735
+ const properties = command.type === 'set' ? mapDottedProperties(command.properties) : command.properties;
736
+ if (chartPart) {
737
+ const checked = assertChartProperties(properties);
157
738
  if (!checked.ok)
158
739
  return checked;
159
- const chartProps = { ...command.properties };
740
+ const chartProps = { ...properties };
160
741
  if (typeof chartProps['text'] === 'string' && chartProps['title'] === undefined)
161
742
  chartProps['title'] = chartProps['text'];
162
- const result = updateChart(archive, item.payload['chartPart'], chartProps);
743
+ const result = updateChart(archive, chartPart, chartProps);
744
+ chartMutated = true;
163
745
  if (result.workbook)
164
746
  diagnostics.push({
165
747
  severity: 'warning',
@@ -167,16 +749,41 @@ export async function mutate(command, archive, index) {
167
749
  message: 'Chart cache changed; embedded workbook was not modified',
168
750
  });
169
751
  }
752
+ else if (liveItem.kind === 'table') {
753
+ const applied = applyTableProperties(node, properties, {
754
+ archive,
755
+ partUri: liveItem.partUri,
756
+ });
757
+ if (!applied.ok)
758
+ return applied;
759
+ if (applied.value.applied.length === 0)
760
+ return err('INVALID_COMMAND', 'set requires at least one supported property');
761
+ }
762
+ else if (liveItem.kind === 'tableCell') {
763
+ const applied = applyTableCellProperties(node, properties);
764
+ if (!applied.ok)
765
+ return applied;
766
+ if (applied.value.applied.length === 0)
767
+ return err('INVALID_COMMAND', 'set requires at least one supported property');
768
+ }
170
769
  else {
171
- const applied = applyShapeProperties(node, command.properties);
770
+ const applied = applyShapeProperties(node, properties, {
771
+ archive,
772
+ partUri: liveItem.partUri,
773
+ });
172
774
  if (!applied.ok)
173
775
  return applied;
174
776
  if (applied.value.applied.length === 0)
175
- return err('INVALID_COMMAND', 'setProperties requires at least one supported property');
777
+ return err('INVALID_COMMAND', 'set requires at least one supported property');
176
778
  }
177
779
  }
780
+ else if (command.type === 'zMove') {
781
+ const moved = applyZMove(doc, node, command, index);
782
+ if (!moved.ok)
783
+ return moved;
784
+ }
178
785
  else if (command.type === 'replacePicture') {
179
- if (item.kind !== 'picture')
786
+ if (liveItem.kind !== 'picture')
180
787
  return err('INVALID_COMMAND', 'replacePicture requires a picture element');
181
788
  const loaded = await loadPictureBytes({
182
789
  ...(command.path !== undefined ? { path: command.path } : {}),
@@ -184,23 +791,39 @@ export async function mutate(command, archive, index) {
184
791
  });
185
792
  if (!loaded.ok)
186
793
  return loaded;
187
- const replaced = replacePictureMedia(archive, item.partUri, node, loaded.value.data, loaded.value.ext);
794
+ const replaced = replacePictureMedia(archive, liveItem.partUri, node, loaded.value.data, loaded.value.ext);
188
795
  if (!replaced.ok)
189
796
  return replaced;
190
797
  }
191
798
  else if (command.type === 'remove') {
192
- if (item.kind === 'picture')
193
- detachPictureAndCleanup(archive, item.partUri, doc, node);
799
+ if (liveItem.kind === 'picture')
800
+ detachPictureAndCleanup(archive, liveItem.partUri, doc, node);
801
+ else if (liveItem.kind === 'video' || liveItem.kind === 'audio')
802
+ detachMediaAndCleanup(archive, liveItem.partUri, doc, node);
194
803
  else
195
804
  node.parentNode?.removeChild(node);
196
805
  }
197
806
  else if (command.type === 'duplicate')
198
807
  duplicateElement(doc, node);
808
+ else if (command.type === 'add') {
809
+ const parent = liveItem.kind === 'slide' ? (first(doc, 'spTree') ?? node) : node;
810
+ await addElement(archive, liveItem.partUri, doc, parent, command.element);
811
+ }
199
812
  else {
200
- const parent = item.kind === 'slide' ? (first(doc, 'spTree') ?? node) : node;
201
- await addElement(archive, item.partUri, doc, parent, command.element);
813
+ return err('INVALID_COMMAND', `Unhandled mutation type`);
202
814
  }
203
- archive.writeXml(item.partUri, doc, archive.getPart(item.partUri)?.mediaType);
204
- return ok({ changed: true, diagnostics }, diagnostics);
815
+ archive.writeXml(liveItem.partUri, doc);
816
+ const changedParts = [liveItem.partUri];
817
+ if (chartMutated && chartPart)
818
+ changedParts.push(chartPart);
819
+ if (liveItem.kind === 'notes' && typeof liveItem.location?.['slidePart'] === 'string')
820
+ changedParts.push(liveItem.location['slidePart']);
821
+ return ok({
822
+ changed: true,
823
+ slides: slidesForItem(index, liveItem),
824
+ diagnostics,
825
+ ...(target ? { changedTargets: [target] } : {}),
826
+ changedParts,
827
+ }, diagnostics);
205
828
  }
206
829
  //# sourceMappingURL=mutations.js.map