@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.
- package/LICENSE +662 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapter.d.ts +70 -3
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +519 -86
- package/dist/adapter.js.map +1 -1
- package/dist/addressing.d.ts +45 -0
- package/dist/addressing.d.ts.map +1 -0
- package/dist/addressing.js +401 -0
- package/dist/addressing.js.map +1 -0
- package/dist/chart-classify.d.ts +10 -0
- package/dist/chart-classify.d.ts.map +1 -0
- package/dist/chart-classify.js +54 -0
- package/dist/chart-classify.js.map +1 -0
- package/dist/chart.d.ts +33 -0
- package/dist/chart.d.ts.map +1 -0
- package/dist/chart.js +492 -0
- package/dist/chart.js.map +1 -0
- package/dist/edition.d.ts +20 -0
- package/dist/edition.d.ts.map +1 -0
- package/dist/edition.js +46 -0
- package/dist/edition.js.map +1 -0
- package/dist/elements.d.ts.map +1 -1
- package/dist/elements.js +61 -46
- package/dist/elements.js.map +1 -1
- package/dist/hyperlink.d.ts +7 -0
- package/dist/hyperlink.d.ts.map +1 -0
- package/dist/hyperlink.js +83 -0
- package/dist/hyperlink.js.map +1 -0
- package/dist/index-sync.d.ts +7 -0
- package/dist/index-sync.d.ts.map +1 -0
- package/dist/index-sync.js +24 -0
- package/dist/index-sync.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts +4 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +105 -27
- package/dist/indexer.js.map +1 -1
- package/dist/media.d.ts +15 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +141 -0
- package/dist/media.js.map +1 -0
- package/dist/mutations.d.ts +8 -2
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +695 -72
- package/dist/mutations.js.map +1 -1
- package/dist/picture.d.ts.map +1 -1
- package/dist/picture.js +1 -3
- package/dist/picture.js.map +1 -1
- package/dist/properties.d.ts +6 -1
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +159 -10
- package/dist/properties.js.map +1 -1
- package/dist/resolve-properties.d.ts +17 -0
- package/dist/resolve-properties.d.ts.map +1 -0
- package/dist/resolve-properties.js +545 -0
- package/dist/resolve-properties.js.map +1 -0
- package/dist/slides.d.ts +3 -1
- package/dist/slides.d.ts.map +1 -1
- package/dist/slides.js +77 -4
- package/dist/slides.js.map +1 -1
- package/dist/table.d.ts +15 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +456 -0
- package/dist/table.js.map +1 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace.d.ts +9 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +67 -47
- package/dist/workspace.js.map +1 -1
- package/dist/xml.d.ts +7 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +86 -13
- package/dist/xml.js.map +1 -1
- 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
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
|
|
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 : ''
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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: ${
|
|
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(
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
370
|
+
const resolveCommandRef = (command, index) => {
|
|
104
371
|
if (command.type === 'replaceText')
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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({
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
141
|
-
|
|
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,
|
|
717
|
+
setNodeText(node, text);
|
|
151
718
|
}
|
|
152
719
|
else if (command.type === 'setTransform')
|
|
153
|
-
transform(node, command.transform);
|
|
154
|
-
else if (command.type === '
|
|
155
|
-
|
|
156
|
-
|
|
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 = { ...
|
|
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,
|
|
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,
|
|
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', '
|
|
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 (
|
|
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,
|
|
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 (
|
|
193
|
-
detachPictureAndCleanup(archive,
|
|
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
|
-
|
|
201
|
-
await addElement(archive, item.partUri, doc, parent, command.element);
|
|
813
|
+
return err('INVALID_COMMAND', `Unhandled mutation type`);
|
|
202
814
|
}
|
|
203
|
-
archive.writeXml(
|
|
204
|
-
|
|
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
|