pptx-viewer-mcp 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/README.md +146 -0
- package/dist/codec/index.d.mts +26 -0
- package/dist/codec/index.d.ts +26 -0
- package/dist/codec/index.js +241 -0
- package/dist/codec/index.mjs +238 -0
- package/dist/index.d.mts +480 -0
- package/dist/index.d.ts +480 -0
- package/dist/index.js +1581 -0
- package/dist/index.mjs +1547 -0
- package/dist/mcp/index.d.mts +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +2339 -0
- package/dist/mcp/index.mjs +2337 -0
- package/dist/schemas/index.d.mts +728 -0
- package/dist/schemas/index.d.ts +728 -0
- package/dist/schemas/index.js +285 -0
- package/dist/schemas/index.mjs +259 -0
- package/package.json +87 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1547 @@
|
|
|
1
|
+
import { PptxHandler, hasTextProperties, PptxMarkdownConverter } from 'pptx-viewer-core';
|
|
2
|
+
|
|
3
|
+
// src/execution.ts
|
|
4
|
+
var PPTX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
5
|
+
async function loadPresentation(filePath, execCtx) {
|
|
6
|
+
const rawBytes = await execCtx.filesystem.readFile(filePath);
|
|
7
|
+
if (execCtx.collaboration) {
|
|
8
|
+
const room = execCtx.collaboration.getRoom(filePath);
|
|
9
|
+
if (room) {
|
|
10
|
+
const codec = execCtx.collaboration.getCodec(filePath);
|
|
11
|
+
if (codec) {
|
|
12
|
+
const currentBytes = await codec.dehydrate(room.ydoc);
|
|
13
|
+
const handler2 = new PptxHandler();
|
|
14
|
+
const pptxData2 = await handler2.load(currentBytes.buffer);
|
|
15
|
+
return { pptxData: pptxData2, rawBytes };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const handler = new PptxHandler();
|
|
20
|
+
const pptxData = await handler.load(rawBytes.buffer);
|
|
21
|
+
return { pptxData, rawBytes };
|
|
22
|
+
}
|
|
23
|
+
async function savePresentation(filePath, pptxData, rawBytes, execCtx) {
|
|
24
|
+
const handler = new PptxHandler();
|
|
25
|
+
await handler.load(rawBytes.buffer);
|
|
26
|
+
const outputBytes = await handler.save(pptxData.slides, {
|
|
27
|
+
headerFooter: pptxData.headerFooter
|
|
28
|
+
});
|
|
29
|
+
if (execCtx.collaboration) {
|
|
30
|
+
const room = execCtx.collaboration.getRoom(filePath);
|
|
31
|
+
if (room) {
|
|
32
|
+
const codec = execCtx.collaboration.getCodec(filePath);
|
|
33
|
+
if (codec) {
|
|
34
|
+
const origin = execCtx.collaboration.agentOrigin(execCtx.agentName ?? "pptx-tool");
|
|
35
|
+
await codec.hydrate(room.ydoc, outputBytes, origin);
|
|
36
|
+
if (execCtx.viewer) {
|
|
37
|
+
execCtx.viewer.replaceContent(filePath, outputBytes, {
|
|
38
|
+
markDirty: false,
|
|
39
|
+
mimeType: PPTX_MIME_TYPE
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return { savedToDisk: false, routedThroughCollaboration: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await execCtx.filesystem.writeFile(filePath, outputBytes);
|
|
47
|
+
if (execCtx.viewer) {
|
|
48
|
+
execCtx.viewer.replaceContent(filePath, outputBytes, {
|
|
49
|
+
markDirty: false,
|
|
50
|
+
mimeType: PPTX_MIME_TYPE
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { savedToDisk: true, routedThroughCollaboration: false };
|
|
54
|
+
}
|
|
55
|
+
async function executeToolWithContext(filePath, execCtx, toolFn) {
|
|
56
|
+
const { pptxData, rawBytes } = await loadPresentation(filePath, execCtx);
|
|
57
|
+
const toolResult = await toolFn({ pptxData });
|
|
58
|
+
if (toolResult.dirty) {
|
|
59
|
+
const saveResult = await savePresentation(filePath, toolResult.pptxData, rawBytes, execCtx);
|
|
60
|
+
return { ...toolResult.result, ...saveResult };
|
|
61
|
+
}
|
|
62
|
+
return toolResult.result;
|
|
63
|
+
}
|
|
64
|
+
function generateElementId() {
|
|
65
|
+
return `el-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
66
|
+
}
|
|
67
|
+
function generateSlideId() {
|
|
68
|
+
return `slide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
69
|
+
}
|
|
70
|
+
function describeElement(el) {
|
|
71
|
+
const base = {
|
|
72
|
+
id: el.id,
|
|
73
|
+
type: el.type,
|
|
74
|
+
x: Math.round(el.x),
|
|
75
|
+
y: Math.round(el.y),
|
|
76
|
+
width: Math.round(el.width),
|
|
77
|
+
height: Math.round(el.height)
|
|
78
|
+
};
|
|
79
|
+
if (el.rotation) {
|
|
80
|
+
base.rotation = el.rotation;
|
|
81
|
+
}
|
|
82
|
+
if (el.hidden) {
|
|
83
|
+
base.hidden = true;
|
|
84
|
+
}
|
|
85
|
+
if (el.opacity !== void 0 && el.opacity !== 1) {
|
|
86
|
+
base.opacity = el.opacity;
|
|
87
|
+
}
|
|
88
|
+
if (hasTextProperties(el)) {
|
|
89
|
+
base.text = el.text ?? "";
|
|
90
|
+
if (el.textStyle) {
|
|
91
|
+
const ts = {};
|
|
92
|
+
if (el.textStyle.fontSize) {
|
|
93
|
+
ts.fontSize = el.textStyle.fontSize;
|
|
94
|
+
}
|
|
95
|
+
if (el.textStyle.fontFamily) {
|
|
96
|
+
ts.fontFamily = el.textStyle.fontFamily;
|
|
97
|
+
}
|
|
98
|
+
if (el.textStyle.bold) {
|
|
99
|
+
ts.bold = true;
|
|
100
|
+
}
|
|
101
|
+
if (el.textStyle.italic) {
|
|
102
|
+
ts.italic = true;
|
|
103
|
+
}
|
|
104
|
+
if (el.textStyle.underline) {
|
|
105
|
+
ts.underline = el.textStyle.underline;
|
|
106
|
+
}
|
|
107
|
+
if (el.textStyle.align) {
|
|
108
|
+
ts.align = el.textStyle.align;
|
|
109
|
+
}
|
|
110
|
+
if (el.textStyle.color) {
|
|
111
|
+
ts.color = el.textStyle.color;
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(ts).length > 0) {
|
|
114
|
+
base.textStyle = ts;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if ("shapeType" in el && el.shapeType) {
|
|
119
|
+
base.shapeType = el.shapeType;
|
|
120
|
+
}
|
|
121
|
+
if ("shapeStyle" in el && el.shapeStyle) {
|
|
122
|
+
const ss = {};
|
|
123
|
+
const style = el.shapeStyle;
|
|
124
|
+
if (style.fillColor) {
|
|
125
|
+
ss.fillColor = style.fillColor;
|
|
126
|
+
}
|
|
127
|
+
if (style.strokeColor) {
|
|
128
|
+
ss.strokeColor = style.strokeColor;
|
|
129
|
+
}
|
|
130
|
+
if (style.strokeWidth) {
|
|
131
|
+
ss.strokeWidth = style.strokeWidth;
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(ss).length > 0) {
|
|
134
|
+
base.shapeStyle = ss;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (el.type === "image" || el.type === "picture") {
|
|
138
|
+
const img = el;
|
|
139
|
+
if (img.altText) {
|
|
140
|
+
base.altText = img.altText;
|
|
141
|
+
}
|
|
142
|
+
if (img.imagePath) {
|
|
143
|
+
base.imagePath = img.imagePath;
|
|
144
|
+
}
|
|
145
|
+
base.hasImageData = Boolean(img.imageData);
|
|
146
|
+
}
|
|
147
|
+
if (el.type === "table") {
|
|
148
|
+
const tbl = el;
|
|
149
|
+
if (tbl.tableData) {
|
|
150
|
+
base.rowCount = tbl.tableData.rows.length;
|
|
151
|
+
base.columnCount = tbl.tableData.columnWidths.length;
|
|
152
|
+
base.cells = tbl.tableData.rows.map((row) => row.cells.map((cell) => cell.text ?? ""));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (el.type === "chart") {
|
|
156
|
+
base.chartType = el.chartData?.type;
|
|
157
|
+
}
|
|
158
|
+
if (el.type === "group") {
|
|
159
|
+
const grp = el;
|
|
160
|
+
base.childCount = grp.children?.length ?? 0;
|
|
161
|
+
}
|
|
162
|
+
return base;
|
|
163
|
+
}
|
|
164
|
+
function extractSlideText(slide) {
|
|
165
|
+
const texts = [];
|
|
166
|
+
for (const el of slide.elements) {
|
|
167
|
+
if (hasTextProperties(el) && el.text) {
|
|
168
|
+
texts.push(el.text);
|
|
169
|
+
}
|
|
170
|
+
if (el.type === "table") {
|
|
171
|
+
const tbl = el;
|
|
172
|
+
if (tbl.tableData) {
|
|
173
|
+
for (const row of tbl.tableData.rows) {
|
|
174
|
+
for (const cell of row.cells) {
|
|
175
|
+
if (cell.text) {
|
|
176
|
+
texts.push(cell.text);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (slide.notes) {
|
|
184
|
+
texts.push(slide.notes);
|
|
185
|
+
}
|
|
186
|
+
return texts.join(" ");
|
|
187
|
+
}
|
|
188
|
+
function validateSlideIndex(slideIndex, slideCount) {
|
|
189
|
+
if (slideIndex < 0 || slideIndex >= slideCount) {
|
|
190
|
+
return `Slide index ${slideIndex} out of range (0\u2013${slideCount - 1}).`;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/tools/slide-tools.ts
|
|
196
|
+
function getSlide(ctx, params) {
|
|
197
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
198
|
+
if (err) {
|
|
199
|
+
throw new Error(err);
|
|
200
|
+
}
|
|
201
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
202
|
+
return {
|
|
203
|
+
pptxData: ctx.pptxData,
|
|
204
|
+
dirty: false,
|
|
205
|
+
result: {
|
|
206
|
+
slideIndex: params.slideIndex,
|
|
207
|
+
slideNumber: slide.slideNumber,
|
|
208
|
+
hidden: slide.hidden ?? false,
|
|
209
|
+
layoutName: slide.layoutName,
|
|
210
|
+
backgroundColor: slide.backgroundColor,
|
|
211
|
+
notes: slide.notes ?? "",
|
|
212
|
+
commentCount: slide.comments?.length ?? 0,
|
|
213
|
+
elements: slide.elements.map(describeElement)
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function addSlide(ctx, params) {
|
|
218
|
+
const { slides } = ctx.pptxData;
|
|
219
|
+
const insertAt = params.insertAfterIndex !== void 0 && params.insertAfterIndex >= 0 ? Math.min(params.insertAfterIndex + 1, slides.length) : slides.length;
|
|
220
|
+
const newSlide = {
|
|
221
|
+
id: generateSlideId(),
|
|
222
|
+
rId: "",
|
|
223
|
+
slideNumber: insertAt + 1,
|
|
224
|
+
elements: [],
|
|
225
|
+
backgroundColor: params.backgroundColor
|
|
226
|
+
};
|
|
227
|
+
slides.splice(insertAt, 0, newSlide);
|
|
228
|
+
slides.forEach((s, i) => {
|
|
229
|
+
s.slideNumber = i + 1;
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
pptxData: ctx.pptxData,
|
|
233
|
+
dirty: true,
|
|
234
|
+
result: { newSlideIndex: insertAt, slideCount: slides.length }
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function deleteSlides(ctx, params) {
|
|
238
|
+
const { slides } = ctx.pptxData;
|
|
239
|
+
if (!params.slideIndexes || params.slideIndexes.length === 0) {
|
|
240
|
+
throw new Error("slideIndexes is required for the delete action.");
|
|
241
|
+
}
|
|
242
|
+
const indexSet = new Set(params.slideIndexes);
|
|
243
|
+
const invalid = params.slideIndexes.filter((i) => i < 0 || i >= slides.length);
|
|
244
|
+
if (invalid.length > 0) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Invalid slide indexes: ${invalid.join(", ")}. Valid range: 0\u2013${slides.length - 1}.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (indexSet.size >= slides.length) {
|
|
250
|
+
throw new Error("Cannot delete all slides. At least one slide must remain.");
|
|
251
|
+
}
|
|
252
|
+
ctx.pptxData.slides = slides.filter((_, i) => !indexSet.has(i));
|
|
253
|
+
ctx.pptxData.slides.forEach((s, i) => {
|
|
254
|
+
s.slideNumber = i + 1;
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
pptxData: ctx.pptxData,
|
|
258
|
+
dirty: true,
|
|
259
|
+
result: { deletedCount: indexSet.size, slideCount: ctx.pptxData.slides.length }
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function reorderSlides(ctx, params) {
|
|
263
|
+
const { slides } = ctx.pptxData;
|
|
264
|
+
if (!params.newOrder || params.newOrder.length !== slides.length) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`newOrder must have exactly ${slides.length} indexes. Got ${params.newOrder?.length ?? 0}.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const sorted = [...params.newOrder].sort((a, b) => a - b);
|
|
270
|
+
const expected = slides.map((_, i) => i);
|
|
271
|
+
if (JSON.stringify(sorted) !== JSON.stringify(expected)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`newOrder must contain each index from 0 to ${slides.length - 1} exactly once.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
ctx.pptxData.slides = params.newOrder.map((idx) => slides[idx]);
|
|
277
|
+
ctx.pptxData.slides.forEach((s, i) => {
|
|
278
|
+
s.slideNumber = i + 1;
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
pptxData: ctx.pptxData,
|
|
282
|
+
dirty: true,
|
|
283
|
+
result: { slideCount: ctx.pptxData.slides.length }
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function duplicateSlide(ctx, params) {
|
|
287
|
+
const { slides } = ctx.pptxData;
|
|
288
|
+
const err = validateSlideIndex(params.slideIndex, slides.length);
|
|
289
|
+
if (err) {
|
|
290
|
+
throw new Error(err);
|
|
291
|
+
}
|
|
292
|
+
const original = slides[params.slideIndex];
|
|
293
|
+
const clone = structuredClone(original);
|
|
294
|
+
clone.id = generateSlideId();
|
|
295
|
+
clone.rId = "";
|
|
296
|
+
for (const el of clone.elements) {
|
|
297
|
+
el.id = generateElementId();
|
|
298
|
+
}
|
|
299
|
+
const insertIndex = params.targetIndex ?? params.slideIndex + 1;
|
|
300
|
+
slides.splice(insertIndex, 0, clone);
|
|
301
|
+
slides.forEach((s, i) => {
|
|
302
|
+
s.slideNumber = i + 1;
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
pptxData: ctx.pptxData,
|
|
306
|
+
dirty: true,
|
|
307
|
+
result: { newSlideIndex: insertIndex, slideCount: slides.length }
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function updateSlideProperties(ctx, params) {
|
|
311
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
312
|
+
if (err) {
|
|
313
|
+
throw new Error(err);
|
|
314
|
+
}
|
|
315
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
316
|
+
if (params.backgroundColor !== void 0) {
|
|
317
|
+
slide.backgroundColor = params.backgroundColor;
|
|
318
|
+
}
|
|
319
|
+
if (params.backgroundGradient !== void 0) {
|
|
320
|
+
slide.backgroundGradient = params.backgroundGradient;
|
|
321
|
+
}
|
|
322
|
+
if (params.backgroundImage !== void 0) {
|
|
323
|
+
slide.backgroundImage = params.backgroundImage;
|
|
324
|
+
}
|
|
325
|
+
if (params.notes !== void 0) {
|
|
326
|
+
slide.notes = params.notes;
|
|
327
|
+
}
|
|
328
|
+
if (params.hidden !== void 0) {
|
|
329
|
+
slide.hidden = params.hidden;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
pptxData: ctx.pptxData,
|
|
333
|
+
dirty: true,
|
|
334
|
+
result: { slideIndex: params.slideIndex }
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function setSlideTransition(ctx, params) {
|
|
338
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
339
|
+
if (err) {
|
|
340
|
+
throw new Error(err);
|
|
341
|
+
}
|
|
342
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
343
|
+
if (params.type === "none") {
|
|
344
|
+
slide.transition = void 0;
|
|
345
|
+
} else {
|
|
346
|
+
const transition = {
|
|
347
|
+
type: params.type
|
|
348
|
+
};
|
|
349
|
+
if (params.durationMs !== void 0) {
|
|
350
|
+
transition.durationMs = params.durationMs;
|
|
351
|
+
}
|
|
352
|
+
if (params.direction !== void 0) {
|
|
353
|
+
transition.direction = params.direction;
|
|
354
|
+
}
|
|
355
|
+
if (params.advanceOnClick !== void 0) {
|
|
356
|
+
transition.advanceOnClick = params.advanceOnClick;
|
|
357
|
+
}
|
|
358
|
+
if (params.advanceAfterMs !== void 0) {
|
|
359
|
+
transition.advanceAfterMs = params.advanceAfterMs;
|
|
360
|
+
}
|
|
361
|
+
slide.transition = transition;
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
pptxData: ctx.pptxData,
|
|
365
|
+
dirty: true,
|
|
366
|
+
result: { slideIndex: params.slideIndex, transitionType: params.type }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function setCanvasSize(ctx, params) {
|
|
370
|
+
ctx.pptxData.width = params.width;
|
|
371
|
+
ctx.pptxData.height = params.height;
|
|
372
|
+
if (ctx.pptxData.widthEmu !== void 0) {
|
|
373
|
+
ctx.pptxData.widthEmu = Math.round(params.width * 12700);
|
|
374
|
+
}
|
|
375
|
+
if (ctx.pptxData.heightEmu !== void 0) {
|
|
376
|
+
ctx.pptxData.heightEmu = Math.round(params.height * 12700);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
pptxData: ctx.pptxData,
|
|
380
|
+
dirty: true,
|
|
381
|
+
result: { canvasWidth: params.width, canvasHeight: params.height }
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function addElement(ctx, params) {
|
|
385
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
386
|
+
if (err) {
|
|
387
|
+
throw new Error(err);
|
|
388
|
+
}
|
|
389
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
390
|
+
const id = generateElementId();
|
|
391
|
+
const x = params.x ?? 100;
|
|
392
|
+
const y = params.y ?? 100;
|
|
393
|
+
const width = params.width ?? 300;
|
|
394
|
+
const height = params.height ?? 60;
|
|
395
|
+
switch (params.type) {
|
|
396
|
+
case "text": {
|
|
397
|
+
const el = {
|
|
398
|
+
id,
|
|
399
|
+
type: "text",
|
|
400
|
+
x,
|
|
401
|
+
y,
|
|
402
|
+
width,
|
|
403
|
+
height,
|
|
404
|
+
text: params.text ?? "",
|
|
405
|
+
textStyle: {
|
|
406
|
+
fontSize: params.fontSize,
|
|
407
|
+
fontFamily: params.fontFamily,
|
|
408
|
+
color: params.fontColor,
|
|
409
|
+
bold: params.bold,
|
|
410
|
+
italic: params.italic,
|
|
411
|
+
underline: params.underline,
|
|
412
|
+
align: params.alignment
|
|
413
|
+
},
|
|
414
|
+
textSegments: [
|
|
415
|
+
{
|
|
416
|
+
text: params.text ?? "",
|
|
417
|
+
style: {
|
|
418
|
+
fontSize: params.fontSize,
|
|
419
|
+
fontFamily: params.fontFamily,
|
|
420
|
+
color: params.fontColor,
|
|
421
|
+
bold: params.bold,
|
|
422
|
+
italic: params.italic,
|
|
423
|
+
underline: params.underline
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
slide.elements.push(el);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "shape": {
|
|
432
|
+
const el = {
|
|
433
|
+
id,
|
|
434
|
+
type: "shape",
|
|
435
|
+
x,
|
|
436
|
+
y,
|
|
437
|
+
width,
|
|
438
|
+
height,
|
|
439
|
+
shapeType: params.shapeType ?? "rect",
|
|
440
|
+
text: params.text,
|
|
441
|
+
textStyle: params.text ? {
|
|
442
|
+
fontSize: params.fontSize,
|
|
443
|
+
fontFamily: params.fontFamily,
|
|
444
|
+
color: params.fontColor,
|
|
445
|
+
bold: params.bold,
|
|
446
|
+
italic: params.italic,
|
|
447
|
+
align: params.alignment
|
|
448
|
+
} : void 0,
|
|
449
|
+
shapeStyle: {
|
|
450
|
+
fillColor: params.fillColor,
|
|
451
|
+
strokeColor: params.strokeColor,
|
|
452
|
+
strokeWidth: params.strokeWidth
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
slide.elements.push(el);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case "image": {
|
|
459
|
+
const el = {
|
|
460
|
+
id,
|
|
461
|
+
type: "image",
|
|
462
|
+
x,
|
|
463
|
+
y,
|
|
464
|
+
width,
|
|
465
|
+
height,
|
|
466
|
+
imageData: params.imageData,
|
|
467
|
+
altText: params.altText
|
|
468
|
+
};
|
|
469
|
+
slide.elements.push(el);
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case "table": {
|
|
473
|
+
const rowCount = params.rows ?? 2;
|
|
474
|
+
const colCount = params.columns ?? 2;
|
|
475
|
+
const colWidth = 1 / colCount;
|
|
476
|
+
const tableRows = Array.from({ length: rowCount }, (_, r) => ({
|
|
477
|
+
height: 40,
|
|
478
|
+
cells: Array.from({ length: colCount }, (__, c) => ({
|
|
479
|
+
text: params.cellData?.[r]?.[c] ?? ""
|
|
480
|
+
}))
|
|
481
|
+
}));
|
|
482
|
+
const tableEl = {
|
|
483
|
+
id,
|
|
484
|
+
type: "table",
|
|
485
|
+
x,
|
|
486
|
+
y,
|
|
487
|
+
width,
|
|
488
|
+
height: params.height ?? rowCount * 40,
|
|
489
|
+
tableData: {
|
|
490
|
+
rows: tableRows,
|
|
491
|
+
columnWidths: Array.from({ length: colCount }, () => colWidth),
|
|
492
|
+
firstRowHeader: params.headerRow ?? false
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
slide.elements.push(tableEl);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case "connector": {
|
|
499
|
+
const el = {
|
|
500
|
+
id,
|
|
501
|
+
type: "connector",
|
|
502
|
+
x,
|
|
503
|
+
y,
|
|
504
|
+
width,
|
|
505
|
+
height,
|
|
506
|
+
shapeStyle: {
|
|
507
|
+
strokeColor: params.strokeColor ?? "#000000",
|
|
508
|
+
strokeWidth: params.strokeWidth ?? 1,
|
|
509
|
+
connectorStartArrow: params.startArrow,
|
|
510
|
+
connectorEndArrow: params.endArrow
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
slide.elements.push(el);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
default: {
|
|
517
|
+
throw new Error(`Unknown element type: ${String(params.type)}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
pptxData: ctx.pptxData,
|
|
522
|
+
dirty: true,
|
|
523
|
+
result: { elementId: id, slideIndex: params.slideIndex }
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function updateElement(ctx, params) {
|
|
527
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
528
|
+
if (err) {
|
|
529
|
+
throw new Error(err);
|
|
530
|
+
}
|
|
531
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
532
|
+
const el = slide.elements.find((e) => e.id === params.elementId);
|
|
533
|
+
if (!el) {
|
|
534
|
+
throw new Error(`Element '${params.elementId}' not found on slide ${params.slideIndex}.`);
|
|
535
|
+
}
|
|
536
|
+
if (params.x !== void 0) {
|
|
537
|
+
el.x = params.x;
|
|
538
|
+
}
|
|
539
|
+
if (params.y !== void 0) {
|
|
540
|
+
el.y = params.y;
|
|
541
|
+
}
|
|
542
|
+
if (params.width !== void 0) {
|
|
543
|
+
el.width = params.width;
|
|
544
|
+
}
|
|
545
|
+
if (params.height !== void 0) {
|
|
546
|
+
el.height = params.height;
|
|
547
|
+
}
|
|
548
|
+
if (params.rotation !== void 0) {
|
|
549
|
+
el.rotation = params.rotation;
|
|
550
|
+
}
|
|
551
|
+
if (params.opacity !== void 0) {
|
|
552
|
+
el.opacity = params.opacity;
|
|
553
|
+
}
|
|
554
|
+
if (params.hidden !== void 0) {
|
|
555
|
+
el.hidden = params.hidden;
|
|
556
|
+
}
|
|
557
|
+
if (params.flipH !== void 0) {
|
|
558
|
+
el.flipHorizontal = params.flipH;
|
|
559
|
+
}
|
|
560
|
+
if (params.flipV !== void 0) {
|
|
561
|
+
el.flipVertical = params.flipV;
|
|
562
|
+
}
|
|
563
|
+
if (hasTextProperties(el)) {
|
|
564
|
+
const textEl = el;
|
|
565
|
+
if (params.text !== void 0) {
|
|
566
|
+
textEl.text = params.text;
|
|
567
|
+
if (textEl.textSegments && textEl.textSegments.length > 0) {
|
|
568
|
+
textEl.textSegments[0].text = params.text;
|
|
569
|
+
} else {
|
|
570
|
+
textEl.textSegments = [{ text: params.text, style: {} }];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (!textEl.textStyle) {
|
|
574
|
+
textEl.textStyle = {};
|
|
575
|
+
}
|
|
576
|
+
if (params.fontSize !== void 0) {
|
|
577
|
+
textEl.textStyle.fontSize = params.fontSize;
|
|
578
|
+
}
|
|
579
|
+
if (params.fontFamily !== void 0) {
|
|
580
|
+
textEl.textStyle.fontFamily = params.fontFamily;
|
|
581
|
+
}
|
|
582
|
+
if (params.fontColor !== void 0) {
|
|
583
|
+
textEl.textStyle.color = params.fontColor;
|
|
584
|
+
}
|
|
585
|
+
if (params.bold !== void 0) {
|
|
586
|
+
textEl.textStyle.bold = params.bold;
|
|
587
|
+
}
|
|
588
|
+
if (params.italic !== void 0) {
|
|
589
|
+
textEl.textStyle.italic = params.italic;
|
|
590
|
+
}
|
|
591
|
+
if (params.underline !== void 0) {
|
|
592
|
+
textEl.textStyle.underline = params.underline;
|
|
593
|
+
}
|
|
594
|
+
if (params.alignment !== void 0) {
|
|
595
|
+
textEl.textStyle.align = params.alignment;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if ("shapeStyle" in el && el.shapeStyle) {
|
|
599
|
+
const ss = el.shapeStyle;
|
|
600
|
+
if (params.fillColor !== void 0) {
|
|
601
|
+
ss.fillColor = params.fillColor;
|
|
602
|
+
}
|
|
603
|
+
if (params.strokeColor !== void 0) {
|
|
604
|
+
ss.strokeColor = params.strokeColor;
|
|
605
|
+
}
|
|
606
|
+
if (params.strokeWidth !== void 0) {
|
|
607
|
+
ss.strokeWidth = params.strokeWidth;
|
|
608
|
+
}
|
|
609
|
+
} else if (params.fillColor !== void 0 || params.strokeColor !== void 0 || params.strokeWidth !== void 0) {
|
|
610
|
+
if ("shapeStyle" in el) {
|
|
611
|
+
el.shapeStyle = {
|
|
612
|
+
fillColor: params.fillColor,
|
|
613
|
+
strokeColor: params.strokeColor,
|
|
614
|
+
strokeWidth: params.strokeWidth
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
pptxData: ctx.pptxData,
|
|
620
|
+
dirty: true,
|
|
621
|
+
result: { elementId: params.elementId }
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function deleteElements(ctx, params) {
|
|
625
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
626
|
+
if (err) {
|
|
627
|
+
throw new Error(err);
|
|
628
|
+
}
|
|
629
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
630
|
+
const idSet = new Set(params.elementIds);
|
|
631
|
+
const notFound = params.elementIds.filter((id) => !slide.elements.some((e) => e.id === id));
|
|
632
|
+
if (notFound.length > 0) {
|
|
633
|
+
throw new Error(`Elements not found: ${notFound.join(", ")}`);
|
|
634
|
+
}
|
|
635
|
+
const before = slide.elements.length;
|
|
636
|
+
slide.elements = slide.elements.filter((e) => !idSet.has(e.id));
|
|
637
|
+
return {
|
|
638
|
+
pptxData: ctx.pptxData,
|
|
639
|
+
dirty: true,
|
|
640
|
+
result: { deletedCount: before - slide.elements.length }
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function arrangeElements(ctx, params) {
|
|
644
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
645
|
+
if (err) {
|
|
646
|
+
throw new Error(err);
|
|
647
|
+
}
|
|
648
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
649
|
+
if (params.action === "align") {
|
|
650
|
+
if (!params.elementIds || params.elementIds.length === 0) {
|
|
651
|
+
throw new Error("elementIds is required for align action.");
|
|
652
|
+
}
|
|
653
|
+
const targets = slide.elements.filter((e) => params.elementIds.includes(e.id));
|
|
654
|
+
if (targets.length === 0) {
|
|
655
|
+
throw new Error("No matching elements found for align.");
|
|
656
|
+
}
|
|
657
|
+
switch (params.alignment) {
|
|
658
|
+
case "left": {
|
|
659
|
+
const minX = Math.min(...targets.map((e) => e.x));
|
|
660
|
+
for (const t of targets) {
|
|
661
|
+
t.x = minX;
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "right": {
|
|
666
|
+
const maxRight = Math.max(...targets.map((e) => e.x + e.width));
|
|
667
|
+
for (const t of targets) {
|
|
668
|
+
t.x = maxRight - t.width;
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
case "top": {
|
|
673
|
+
const minY = Math.min(...targets.map((e) => e.y));
|
|
674
|
+
for (const t of targets) {
|
|
675
|
+
t.y = minY;
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case "bottom": {
|
|
680
|
+
const maxBottom = Math.max(...targets.map((e) => e.y + e.height));
|
|
681
|
+
for (const t of targets) {
|
|
682
|
+
t.y = maxBottom - t.height;
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "centerH": {
|
|
687
|
+
const avgCX = targets.reduce((s, e) => s + e.x + e.width / 2, 0) / targets.length;
|
|
688
|
+
for (const t of targets) {
|
|
689
|
+
t.x = avgCX - t.width / 2;
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
case "centerV": {
|
|
694
|
+
const avgCY = targets.reduce((s, e) => s + e.y + e.height / 2, 0) / targets.length;
|
|
695
|
+
for (const t of targets) {
|
|
696
|
+
t.y = avgCY - t.height / 2;
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
default: {
|
|
701
|
+
throw new Error(`Unknown alignment: ${String(params.alignment)}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} else if (params.action === "reorderLayer") {
|
|
705
|
+
if (!params.elementId) {
|
|
706
|
+
throw new Error("elementId is required for reorderLayer action.");
|
|
707
|
+
}
|
|
708
|
+
const idx = slide.elements.findIndex((e) => e.id === params.elementId);
|
|
709
|
+
if (idx < 0) {
|
|
710
|
+
throw new Error(`Element '${params.elementId}' not found.`);
|
|
711
|
+
}
|
|
712
|
+
const [el] = slide.elements.splice(idx, 1);
|
|
713
|
+
switch (params.layerAction) {
|
|
714
|
+
case "bringToFront":
|
|
715
|
+
slide.elements.push(el);
|
|
716
|
+
break;
|
|
717
|
+
case "sendToBack":
|
|
718
|
+
slide.elements.unshift(el);
|
|
719
|
+
break;
|
|
720
|
+
case "bringForward": {
|
|
721
|
+
const newIdx = Math.min(idx + 1, slide.elements.length);
|
|
722
|
+
slide.elements.splice(newIdx, 0, el);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "sendBackward": {
|
|
726
|
+
const newIdx = Math.max(idx - 1, 0);
|
|
727
|
+
slide.elements.splice(newIdx, 0, el);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
default: {
|
|
731
|
+
slide.elements.splice(idx, 0, el);
|
|
732
|
+
throw new Error(`Unknown layerAction: ${String(params.layerAction)}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
throw new Error(`Unknown action: ${String(params.action)}`);
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
pptxData: ctx.pptxData,
|
|
740
|
+
dirty: true,
|
|
741
|
+
result: { slideIndex: params.slideIndex }
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function cloneElement(ctx, params) {
|
|
745
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
746
|
+
if (err) {
|
|
747
|
+
throw new Error(err);
|
|
748
|
+
}
|
|
749
|
+
const srcSlide = ctx.pptxData.slides[params.slideIndex];
|
|
750
|
+
const original = srcSlide.elements.find((e) => e.id === params.elementId);
|
|
751
|
+
if (!original) {
|
|
752
|
+
throw new Error(`Element '${params.elementId}' not found on slide ${params.slideIndex}.`);
|
|
753
|
+
}
|
|
754
|
+
const targets = params.targetSlideIndexes ?? [params.slideIndex];
|
|
755
|
+
const clonedIds = [];
|
|
756
|
+
const offsetX = params.offsetX ?? 20;
|
|
757
|
+
const offsetY = params.offsetY ?? 20;
|
|
758
|
+
for (const targetIdx of targets) {
|
|
759
|
+
const terr = validateSlideIndex(targetIdx, ctx.pptxData.slides.length);
|
|
760
|
+
if (terr) {
|
|
761
|
+
throw new Error(terr);
|
|
762
|
+
}
|
|
763
|
+
const clone = structuredClone(original);
|
|
764
|
+
clone.id = generateElementId();
|
|
765
|
+
clone.x += offsetX;
|
|
766
|
+
clone.y += offsetY;
|
|
767
|
+
ctx.pptxData.slides[targetIdx].elements.push(clone);
|
|
768
|
+
clonedIds.push(clone.id);
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
pptxData: ctx.pptxData,
|
|
772
|
+
dirty: true,
|
|
773
|
+
result: { clonedIds }
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function setElementAnimation(ctx, params) {
|
|
777
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
778
|
+
if (err) {
|
|
779
|
+
throw new Error(err);
|
|
780
|
+
}
|
|
781
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
782
|
+
const elExists = slide.elements.some((e) => e.id === params.elementId);
|
|
783
|
+
if (!elExists) {
|
|
784
|
+
throw new Error(`Element '${params.elementId}' not found on slide ${params.slideIndex}.`);
|
|
785
|
+
}
|
|
786
|
+
if (!slide.animations) {
|
|
787
|
+
slide.animations = [];
|
|
788
|
+
}
|
|
789
|
+
const existing = slide.animations.find((a) => a.elementId === params.elementId);
|
|
790
|
+
if (existing) {
|
|
791
|
+
if (params.entrance !== void 0) {
|
|
792
|
+
existing.entrance = params.entrance;
|
|
793
|
+
}
|
|
794
|
+
if (params.exit !== void 0) {
|
|
795
|
+
existing.exit = params.exit;
|
|
796
|
+
}
|
|
797
|
+
if (params.durationMs !== void 0) {
|
|
798
|
+
existing.durationMs = params.durationMs;
|
|
799
|
+
}
|
|
800
|
+
if (params.delayMs !== void 0) {
|
|
801
|
+
existing.delayMs = params.delayMs;
|
|
802
|
+
}
|
|
803
|
+
if (params.order !== void 0) {
|
|
804
|
+
existing.order = params.order;
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
const anim = {
|
|
808
|
+
elementId: params.elementId,
|
|
809
|
+
entrance: params.entrance,
|
|
810
|
+
exit: params.exit,
|
|
811
|
+
durationMs: params.durationMs,
|
|
812
|
+
delayMs: params.delayMs,
|
|
813
|
+
order: params.order
|
|
814
|
+
};
|
|
815
|
+
slide.animations.push(anim);
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
pptxData: ctx.pptxData,
|
|
819
|
+
dirty: true,
|
|
820
|
+
result: { elementId: params.elementId }
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function groupElements(ctx, params) {
|
|
824
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
825
|
+
if (err) {
|
|
826
|
+
throw new Error(err);
|
|
827
|
+
}
|
|
828
|
+
if (params.elementIds.length < 2) {
|
|
829
|
+
throw new Error("At least 2 elements are required to form a group.");
|
|
830
|
+
}
|
|
831
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
832
|
+
const idSet = new Set(params.elementIds);
|
|
833
|
+
const children = slide.elements.filter((e) => idSet.has(e.id));
|
|
834
|
+
if (children.length !== params.elementIds.length) {
|
|
835
|
+
const missing = params.elementIds.filter((id) => !slide.elements.some((e) => e.id === id));
|
|
836
|
+
throw new Error(`Elements not found: ${missing.join(", ")}`);
|
|
837
|
+
}
|
|
838
|
+
const minX = Math.min(...children.map((e) => e.x));
|
|
839
|
+
const minY = Math.min(...children.map((e) => e.y));
|
|
840
|
+
const maxX = Math.max(...children.map((e) => e.x + e.width));
|
|
841
|
+
const maxY = Math.max(...children.map((e) => e.y + e.height));
|
|
842
|
+
const localChildren = children.map((e) => {
|
|
843
|
+
const c = structuredClone(e);
|
|
844
|
+
c.x -= minX;
|
|
845
|
+
c.y -= minY;
|
|
846
|
+
return c;
|
|
847
|
+
});
|
|
848
|
+
slide.elements = slide.elements.filter((e) => !idSet.has(e.id));
|
|
849
|
+
const groupId = generateElementId();
|
|
850
|
+
const group = {
|
|
851
|
+
id: groupId,
|
|
852
|
+
type: "group",
|
|
853
|
+
x: minX,
|
|
854
|
+
y: minY,
|
|
855
|
+
width: maxX - minX,
|
|
856
|
+
height: maxY - minY,
|
|
857
|
+
children: localChildren
|
|
858
|
+
};
|
|
859
|
+
slide.elements.push(group);
|
|
860
|
+
return {
|
|
861
|
+
pptxData: ctx.pptxData,
|
|
862
|
+
dirty: true,
|
|
863
|
+
result: { groupId }
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function ungroupElements(ctx, params) {
|
|
867
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
868
|
+
if (err) {
|
|
869
|
+
throw new Error(err);
|
|
870
|
+
}
|
|
871
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
872
|
+
const groupIdx = slide.elements.findIndex((e) => e.id === params.groupElementId);
|
|
873
|
+
if (groupIdx < 0) {
|
|
874
|
+
throw new Error(`Group element '${params.groupElementId}' not found.`);
|
|
875
|
+
}
|
|
876
|
+
const group = slide.elements[groupIdx];
|
|
877
|
+
if (group.type !== "group") {
|
|
878
|
+
throw new Error(`Element '${params.groupElementId}' is not a group.`);
|
|
879
|
+
}
|
|
880
|
+
const grp = group;
|
|
881
|
+
const restored = grp.children.map((child) => {
|
|
882
|
+
const c = structuredClone(child);
|
|
883
|
+
c.x += grp.x;
|
|
884
|
+
c.y += grp.y;
|
|
885
|
+
return c;
|
|
886
|
+
});
|
|
887
|
+
slide.elements.splice(groupIdx, 1, ...restored);
|
|
888
|
+
return {
|
|
889
|
+
pptxData: ctx.pptxData,
|
|
890
|
+
dirty: true,
|
|
891
|
+
result: { restoredIds: restored.map((c) => c.id) }
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function batchUpdateElements(ctx, params) {
|
|
895
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
896
|
+
if (err) {
|
|
897
|
+
throw new Error(err);
|
|
898
|
+
}
|
|
899
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
900
|
+
const idSet = new Set(params.elementIds);
|
|
901
|
+
let count = 0;
|
|
902
|
+
for (const el of slide.elements) {
|
|
903
|
+
if (!idSet.has(el.id)) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (params.x !== void 0) {
|
|
907
|
+
el.x = params.x;
|
|
908
|
+
}
|
|
909
|
+
if (params.y !== void 0) {
|
|
910
|
+
el.y = params.y;
|
|
911
|
+
}
|
|
912
|
+
if (params.width !== void 0) {
|
|
913
|
+
el.width = params.width;
|
|
914
|
+
}
|
|
915
|
+
if (params.height !== void 0) {
|
|
916
|
+
el.height = params.height;
|
|
917
|
+
}
|
|
918
|
+
if (params.rotation !== void 0) {
|
|
919
|
+
el.rotation = params.rotation;
|
|
920
|
+
}
|
|
921
|
+
if (params.opacity !== void 0) {
|
|
922
|
+
el.opacity = params.opacity;
|
|
923
|
+
}
|
|
924
|
+
if (params.hidden !== void 0) {
|
|
925
|
+
el.hidden = params.hidden;
|
|
926
|
+
}
|
|
927
|
+
count++;
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
pptxData: ctx.pptxData,
|
|
931
|
+
dirty: true,
|
|
932
|
+
result: { updatedCount: count }
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/tools/table-tools.ts
|
|
937
|
+
function updateTableCells(ctx, params) {
|
|
938
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
939
|
+
if (err) {
|
|
940
|
+
throw new Error(err);
|
|
941
|
+
}
|
|
942
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
943
|
+
const el = slide.elements.find((e) => e.id === params.elementId);
|
|
944
|
+
if (!el) {
|
|
945
|
+
throw new Error(`Element '${params.elementId}' not found on slide ${params.slideIndex}.`);
|
|
946
|
+
}
|
|
947
|
+
if (el.type !== "table") {
|
|
948
|
+
throw new Error(`Element '${params.elementId}' is not a table.`);
|
|
949
|
+
}
|
|
950
|
+
const tbl = el;
|
|
951
|
+
if (!tbl.tableData) {
|
|
952
|
+
throw new Error(`Table '${params.elementId}' has no tableData.`);
|
|
953
|
+
}
|
|
954
|
+
let count = 0;
|
|
955
|
+
for (const cellUpdate of params.cells) {
|
|
956
|
+
const row = tbl.tableData.rows[cellUpdate.row];
|
|
957
|
+
if (!row) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Row ${cellUpdate.row} out of range (table has ${tbl.tableData.rows.length} rows).`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const cell = row.cells[cellUpdate.col];
|
|
963
|
+
if (!cell) {
|
|
964
|
+
throw new Error(`Column ${cellUpdate.col} out of range (row has ${row.cells.length} cells).`);
|
|
965
|
+
}
|
|
966
|
+
cell.text = cellUpdate.text;
|
|
967
|
+
count++;
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
pptxData: ctx.pptxData,
|
|
971
|
+
dirty: true,
|
|
972
|
+
result: { updatedCount: count }
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function manageTableStructure(ctx, params) {
|
|
976
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
977
|
+
if (err) {
|
|
978
|
+
throw new Error(err);
|
|
979
|
+
}
|
|
980
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
981
|
+
const el = slide.elements.find((e) => e.id === params.elementId);
|
|
982
|
+
if (!el) {
|
|
983
|
+
throw new Error(`Element '${params.elementId}' not found on slide ${params.slideIndex}.`);
|
|
984
|
+
}
|
|
985
|
+
if (el.type !== "table") {
|
|
986
|
+
throw new Error(`Element '${params.elementId}' is not a table.`);
|
|
987
|
+
}
|
|
988
|
+
const tbl = el;
|
|
989
|
+
if (!tbl.tableData) {
|
|
990
|
+
throw new Error(`Table '${params.elementId}' has no tableData.`);
|
|
991
|
+
}
|
|
992
|
+
const { rows, columnWidths } = tbl.tableData;
|
|
993
|
+
const colCount = columnWidths.length;
|
|
994
|
+
const rowCount = rows.length;
|
|
995
|
+
switch (params.action) {
|
|
996
|
+
case "insertRow": {
|
|
997
|
+
const pos = params.position !== void 0 ? Math.min(Math.max(params.position, 0), rowCount) : rowCount;
|
|
998
|
+
const newRow = {
|
|
999
|
+
height: 40,
|
|
1000
|
+
cells: Array.from({ length: colCount }, (_, c) => ({
|
|
1001
|
+
text: params.cellTexts?.[c] ?? ""
|
|
1002
|
+
}))
|
|
1003
|
+
};
|
|
1004
|
+
rows.splice(pos, 0, newRow);
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
case "deleteRow": {
|
|
1008
|
+
if (rowCount <= 1) {
|
|
1009
|
+
throw new Error("Cannot delete the last row of a table.");
|
|
1010
|
+
}
|
|
1011
|
+
const ref = params.referenceIndex ?? rowCount - 1;
|
|
1012
|
+
if (ref < 0 || ref >= rowCount) {
|
|
1013
|
+
throw new Error(`Row index ${ref} out of range (0\u2013${rowCount - 1}).`);
|
|
1014
|
+
}
|
|
1015
|
+
rows.splice(ref, 1);
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
case "insertColumn": {
|
|
1019
|
+
const pos = params.position !== void 0 ? Math.min(Math.max(params.position, 0), colCount) : colCount;
|
|
1020
|
+
const newWidth = 1 / (colCount + 1);
|
|
1021
|
+
const scaleFactor = colCount / (colCount + 1);
|
|
1022
|
+
for (let i = 0; i < columnWidths.length; i++) {
|
|
1023
|
+
columnWidths[i] *= scaleFactor;
|
|
1024
|
+
}
|
|
1025
|
+
columnWidths.splice(pos, 0, newWidth);
|
|
1026
|
+
for (let r = 0; r < rows.length; r++) {
|
|
1027
|
+
rows[r].cells.splice(pos, 0, {
|
|
1028
|
+
text: params.cellTexts?.[r] ?? ""
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
case "deleteColumn": {
|
|
1034
|
+
if (colCount <= 1) {
|
|
1035
|
+
throw new Error("Cannot delete the last column of a table.");
|
|
1036
|
+
}
|
|
1037
|
+
const ref = params.referenceIndex ?? colCount - 1;
|
|
1038
|
+
if (ref < 0 || ref >= colCount) {
|
|
1039
|
+
throw new Error(`Column index ${ref} out of range (0\u2013${colCount - 1}).`);
|
|
1040
|
+
}
|
|
1041
|
+
const removedWidth = columnWidths[ref];
|
|
1042
|
+
columnWidths.splice(ref, 1);
|
|
1043
|
+
const remaining = columnWidths.length;
|
|
1044
|
+
if (remaining > 0) {
|
|
1045
|
+
const extra = removedWidth / remaining;
|
|
1046
|
+
for (let i = 0; i < columnWidths.length; i++) {
|
|
1047
|
+
columnWidths[i] += extra;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const row of rows) {
|
|
1051
|
+
row.cells.splice(ref, 1);
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
default: {
|
|
1056
|
+
throw new Error(`Unknown action: ${String(params.action)}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
pptxData: ctx.pptxData,
|
|
1061
|
+
dirty: true,
|
|
1062
|
+
result: {
|
|
1063
|
+
rowCount: tbl.tableData.rows.length,
|
|
1064
|
+
columnCount: tbl.tableData.columnWidths.length
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function updateElementStyle(ctx, params) {
|
|
1069
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
1070
|
+
if (err) {
|
|
1071
|
+
throw new Error(err);
|
|
1072
|
+
}
|
|
1073
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
1074
|
+
const el = slide.elements.find((e) => e.id === params.elementId);
|
|
1075
|
+
if (!el) {
|
|
1076
|
+
throw new Error(`Element '${params.elementId}' not found on slide ${params.slideIndex}.`);
|
|
1077
|
+
}
|
|
1078
|
+
if ("shapeStyle" in el) {
|
|
1079
|
+
if (!el.shapeStyle) {
|
|
1080
|
+
el.shapeStyle = {};
|
|
1081
|
+
}
|
|
1082
|
+
const ss = el.shapeStyle;
|
|
1083
|
+
if (params.fillColor !== void 0) {
|
|
1084
|
+
ss.fillColor = params.fillColor;
|
|
1085
|
+
}
|
|
1086
|
+
if (params.fillMode !== void 0) {
|
|
1087
|
+
ss.fillMode = params.fillMode;
|
|
1088
|
+
}
|
|
1089
|
+
if (params.fillGradientStops !== void 0) {
|
|
1090
|
+
ss.fillGradientStops = params.fillGradientStops;
|
|
1091
|
+
}
|
|
1092
|
+
if (params.fillGradientAngle !== void 0) {
|
|
1093
|
+
ss.fillGradientAngle = params.fillGradientAngle;
|
|
1094
|
+
}
|
|
1095
|
+
if (params.fillGradientType !== void 0) {
|
|
1096
|
+
ss.fillGradientType = params.fillGradientType;
|
|
1097
|
+
}
|
|
1098
|
+
if (params.fillOpacity !== void 0) {
|
|
1099
|
+
ss.fillOpacity = params.fillOpacity;
|
|
1100
|
+
}
|
|
1101
|
+
if (params.strokeColor !== void 0) {
|
|
1102
|
+
ss.strokeColor = params.strokeColor;
|
|
1103
|
+
}
|
|
1104
|
+
if (params.strokeWidth !== void 0) {
|
|
1105
|
+
ss.strokeWidth = params.strokeWidth;
|
|
1106
|
+
}
|
|
1107
|
+
if (params.strokeDash !== void 0) {
|
|
1108
|
+
ss.strokeDash = params.strokeDash;
|
|
1109
|
+
}
|
|
1110
|
+
if (params.strokeOpacity !== void 0) {
|
|
1111
|
+
ss.strokeOpacity = params.strokeOpacity;
|
|
1112
|
+
}
|
|
1113
|
+
if (params.shadowColor !== void 0) {
|
|
1114
|
+
ss.shadowColor = params.shadowColor;
|
|
1115
|
+
}
|
|
1116
|
+
if (params.shadowBlur !== void 0) {
|
|
1117
|
+
ss.shadowBlur = params.shadowBlur;
|
|
1118
|
+
}
|
|
1119
|
+
if (params.shadowOffsetX !== void 0) {
|
|
1120
|
+
ss.shadowOffsetX = params.shadowOffsetX;
|
|
1121
|
+
}
|
|
1122
|
+
if (params.shadowOffsetY !== void 0) {
|
|
1123
|
+
ss.shadowOffsetY = params.shadowOffsetY;
|
|
1124
|
+
}
|
|
1125
|
+
if (params.shadowOpacity !== void 0) {
|
|
1126
|
+
ss.shadowOpacity = params.shadowOpacity;
|
|
1127
|
+
}
|
|
1128
|
+
if (params.glowColor !== void 0) {
|
|
1129
|
+
ss.glowColor = params.glowColor;
|
|
1130
|
+
}
|
|
1131
|
+
if (params.glowRadius !== void 0) {
|
|
1132
|
+
ss.glowRadius = params.glowRadius;
|
|
1133
|
+
}
|
|
1134
|
+
if (params.glowOpacity !== void 0) {
|
|
1135
|
+
ss.glowOpacity = params.glowOpacity;
|
|
1136
|
+
}
|
|
1137
|
+
if (params.softEdgeRadius !== void 0) {
|
|
1138
|
+
ss.softEdgeRadius = params.softEdgeRadius;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (el.type === "image" || el.type === "picture") {
|
|
1142
|
+
const img = el;
|
|
1143
|
+
if (params.altText !== void 0) {
|
|
1144
|
+
img.altText = params.altText;
|
|
1145
|
+
}
|
|
1146
|
+
if (params.cropLeft !== void 0) {
|
|
1147
|
+
img.cropLeft = params.cropLeft;
|
|
1148
|
+
}
|
|
1149
|
+
if (params.cropTop !== void 0) {
|
|
1150
|
+
img.cropTop = params.cropTop;
|
|
1151
|
+
}
|
|
1152
|
+
if (params.cropRight !== void 0) {
|
|
1153
|
+
img.cropRight = params.cropRight;
|
|
1154
|
+
}
|
|
1155
|
+
if (params.cropBottom !== void 0) {
|
|
1156
|
+
img.cropBottom = params.cropBottom;
|
|
1157
|
+
}
|
|
1158
|
+
if (params.brightness !== void 0 || params.contrast !== void 0 || params.grayscale !== void 0) {
|
|
1159
|
+
if (!img.imageEffects) {
|
|
1160
|
+
img.imageEffects = {};
|
|
1161
|
+
}
|
|
1162
|
+
if (params.brightness !== void 0) {
|
|
1163
|
+
img.imageEffects.brightness = params.brightness;
|
|
1164
|
+
}
|
|
1165
|
+
if (params.contrast !== void 0) {
|
|
1166
|
+
img.imageEffects.contrast = params.contrast;
|
|
1167
|
+
}
|
|
1168
|
+
if (params.grayscale !== void 0) {
|
|
1169
|
+
img.imageEffects.grayscale = params.grayscale;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
pptxData: ctx.pptxData,
|
|
1175
|
+
dirty: true,
|
|
1176
|
+
result: { elementId: params.elementId }
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function runAccessibilityCheck(ctx) {
|
|
1180
|
+
const issues = [];
|
|
1181
|
+
for (let si = 0; si < ctx.pptxData.slides.length; si++) {
|
|
1182
|
+
const slide = ctx.pptxData.slides[si];
|
|
1183
|
+
let slideHasText = false;
|
|
1184
|
+
for (const el of slide.elements) {
|
|
1185
|
+
if (el.type === "image" || el.type === "picture") {
|
|
1186
|
+
const img = el;
|
|
1187
|
+
if (!img.altText) {
|
|
1188
|
+
issues.push({
|
|
1189
|
+
slideIndex: si,
|
|
1190
|
+
elementId: el.id,
|
|
1191
|
+
severity: "error",
|
|
1192
|
+
message: `Image element '${el.id}' has no alt text.`
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (hasTextProperties(el)) {
|
|
1197
|
+
const textEl = el;
|
|
1198
|
+
slideHasText = true;
|
|
1199
|
+
if (!textEl.text || textEl.text.trim() === "") {
|
|
1200
|
+
issues.push({
|
|
1201
|
+
slideIndex: si,
|
|
1202
|
+
elementId: el.id,
|
|
1203
|
+
severity: "info",
|
|
1204
|
+
message: `Text element '${el.id}' is empty.`
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
const fs = textEl.textStyle?.fontSize;
|
|
1208
|
+
if (fs !== void 0 && fs < 10) {
|
|
1209
|
+
issues.push({
|
|
1210
|
+
slideIndex: si,
|
|
1211
|
+
elementId: el.id,
|
|
1212
|
+
severity: "warning",
|
|
1213
|
+
message: `Text element '${el.id}' has very small font size (${fs}pt < 10pt).`
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
const textColor = textEl.textStyle?.color?.toLowerCase();
|
|
1217
|
+
const bgColor = slide.backgroundColor?.toLowerCase();
|
|
1218
|
+
if (textColor && bgColor && textColor === bgColor) {
|
|
1219
|
+
issues.push({
|
|
1220
|
+
slideIndex: si,
|
|
1221
|
+
elementId: el.id,
|
|
1222
|
+
severity: "error",
|
|
1223
|
+
message: `Text element '${el.id}' has same color as slide background (${textColor}).`
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!slideHasText) {
|
|
1229
|
+
issues.push({
|
|
1230
|
+
slideIndex: si,
|
|
1231
|
+
severity: "warning",
|
|
1232
|
+
message: `Slide ${si + 1} contains no text elements.`
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
1237
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
1238
|
+
const infoCount = issues.filter((i) => i.severity === "info").length;
|
|
1239
|
+
return {
|
|
1240
|
+
pptxData: ctx.pptxData,
|
|
1241
|
+
dirty: false,
|
|
1242
|
+
result: {
|
|
1243
|
+
slideCount: ctx.pptxData.slides.length,
|
|
1244
|
+
totalIssues: issues.length,
|
|
1245
|
+
errorCount,
|
|
1246
|
+
warningCount,
|
|
1247
|
+
infoCount,
|
|
1248
|
+
issues
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function buildSearchRegex(query, useRegex, caseSensitive) {
|
|
1253
|
+
const pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1254
|
+
const flags = caseSensitive ? "g" : "gi";
|
|
1255
|
+
return new RegExp(pattern, flags);
|
|
1256
|
+
}
|
|
1257
|
+
function findText(ctx, params) {
|
|
1258
|
+
const regex = buildSearchRegex(
|
|
1259
|
+
params.query,
|
|
1260
|
+
params.useRegex === true,
|
|
1261
|
+
params.caseSensitive === true
|
|
1262
|
+
);
|
|
1263
|
+
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
1264
|
+
const matches = [];
|
|
1265
|
+
for (const si of targetIndexes) {
|
|
1266
|
+
const err = validateSlideIndex(si, ctx.pptxData.slides.length);
|
|
1267
|
+
if (err) {
|
|
1268
|
+
throw new Error(err);
|
|
1269
|
+
}
|
|
1270
|
+
const slide = ctx.pptxData.slides[si];
|
|
1271
|
+
for (const el of slide.elements) {
|
|
1272
|
+
if (hasTextProperties(el)) {
|
|
1273
|
+
const textEl = el;
|
|
1274
|
+
if (textEl.text) {
|
|
1275
|
+
const localMatches = Array.from(textEl.text.matchAll(regex));
|
|
1276
|
+
for (const m of localMatches) {
|
|
1277
|
+
matches.push({
|
|
1278
|
+
slideIndex: si,
|
|
1279
|
+
elementId: el.id,
|
|
1280
|
+
elementType: el.type,
|
|
1281
|
+
matchedText: m[0],
|
|
1282
|
+
context: textEl.text.slice(
|
|
1283
|
+
Math.max(0, (m.index ?? 0) - 20),
|
|
1284
|
+
(m.index ?? 0) + m[0].length + 20
|
|
1285
|
+
)
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (el.type === "table") {
|
|
1291
|
+
const tbl = el;
|
|
1292
|
+
if (tbl.tableData) {
|
|
1293
|
+
for (const row of tbl.tableData.rows) {
|
|
1294
|
+
for (const cell of row.cells) {
|
|
1295
|
+
if (cell.text) {
|
|
1296
|
+
const localMatches = Array.from(cell.text.matchAll(regex));
|
|
1297
|
+
for (const m of localMatches) {
|
|
1298
|
+
matches.push({
|
|
1299
|
+
slideIndex: si,
|
|
1300
|
+
elementId: el.id,
|
|
1301
|
+
elementType: "table-cell",
|
|
1302
|
+
matchedText: m[0],
|
|
1303
|
+
context: cell.text.slice(
|
|
1304
|
+
Math.max(0, (m.index ?? 0) - 20),
|
|
1305
|
+
(m.index ?? 0) + m[0].length + 20
|
|
1306
|
+
)
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (slide.notes) {
|
|
1316
|
+
const noteMatches = Array.from(slide.notes.matchAll(regex));
|
|
1317
|
+
for (const m of noteMatches) {
|
|
1318
|
+
matches.push({
|
|
1319
|
+
slideIndex: si,
|
|
1320
|
+
elementId: "notes",
|
|
1321
|
+
elementType: "notes",
|
|
1322
|
+
matchedText: m[0],
|
|
1323
|
+
context: slide.notes.slice(
|
|
1324
|
+
Math.max(0, (m.index ?? 0) - 20),
|
|
1325
|
+
(m.index ?? 0) + m[0].length + 20
|
|
1326
|
+
)
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
pptxData: ctx.pptxData,
|
|
1333
|
+
dirty: false,
|
|
1334
|
+
result: {
|
|
1335
|
+
query: params.query,
|
|
1336
|
+
matchCount: matches.length,
|
|
1337
|
+
matches
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function replaceText(ctx, params) {
|
|
1342
|
+
const regex = buildSearchRegex(
|
|
1343
|
+
params.query,
|
|
1344
|
+
params.useRegex === true,
|
|
1345
|
+
params.caseSensitive === true
|
|
1346
|
+
);
|
|
1347
|
+
const targetIndexes = params.slideIndexes ? params.slideIndexes : ctx.pptxData.slides.map((_, i) => i);
|
|
1348
|
+
let count = 0;
|
|
1349
|
+
for (const si of targetIndexes) {
|
|
1350
|
+
const err = validateSlideIndex(si, ctx.pptxData.slides.length);
|
|
1351
|
+
if (err) {
|
|
1352
|
+
throw new Error(err);
|
|
1353
|
+
}
|
|
1354
|
+
const slide = ctx.pptxData.slides[si];
|
|
1355
|
+
for (const el of slide.elements) {
|
|
1356
|
+
if (hasTextProperties(el)) {
|
|
1357
|
+
const textEl = el;
|
|
1358
|
+
if (textEl.text) {
|
|
1359
|
+
const before = textEl.text;
|
|
1360
|
+
const after = before.replace(regex, params.replacement);
|
|
1361
|
+
if (after !== before) {
|
|
1362
|
+
const matchCount = Array.from(before.matchAll(regex)).length;
|
|
1363
|
+
count += matchCount;
|
|
1364
|
+
textEl.text = after;
|
|
1365
|
+
if (textEl.textSegments && textEl.textSegments.length > 0) {
|
|
1366
|
+
const firstSeg = textEl.textSegments[0];
|
|
1367
|
+
const updatedFirst = firstSeg.text.replace(regex, params.replacement);
|
|
1368
|
+
if (updatedFirst !== firstSeg.text) {
|
|
1369
|
+
firstSeg.text = updatedFirst;
|
|
1370
|
+
} else {
|
|
1371
|
+
textEl.textSegments = [{ text: after, style: firstSeg.style }];
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (el.type === "table") {
|
|
1378
|
+
const tbl = el;
|
|
1379
|
+
if (tbl.tableData) {
|
|
1380
|
+
for (const row of tbl.tableData.rows) {
|
|
1381
|
+
for (const cell of row.cells) {
|
|
1382
|
+
if (cell.text) {
|
|
1383
|
+
const before = cell.text;
|
|
1384
|
+
const after = before.replace(regex, params.replacement);
|
|
1385
|
+
if (after !== before) {
|
|
1386
|
+
count += Array.from(before.matchAll(regex)).length;
|
|
1387
|
+
cell.text = after;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (slide.notes) {
|
|
1396
|
+
const before = slide.notes;
|
|
1397
|
+
const after = before.replace(regex, params.replacement);
|
|
1398
|
+
if (after !== before) {
|
|
1399
|
+
count += Array.from(before.matchAll(regex)).length;
|
|
1400
|
+
slide.notes = after;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
pptxData: ctx.pptxData,
|
|
1406
|
+
dirty: count > 0,
|
|
1407
|
+
result: {
|
|
1408
|
+
query: params.query,
|
|
1409
|
+
replacement: params.replacement,
|
|
1410
|
+
replacementCount: count
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function manageComments(ctx, params) {
|
|
1415
|
+
switch (params.action) {
|
|
1416
|
+
case "list": {
|
|
1417
|
+
const all = [];
|
|
1418
|
+
for (let si = 0; si < ctx.pptxData.slides.length; si++) {
|
|
1419
|
+
if (params.slideIndex !== void 0 && si !== params.slideIndex) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const slide = ctx.pptxData.slides[si];
|
|
1423
|
+
for (const c of slide.comments ?? []) {
|
|
1424
|
+
if (!params.includeResolved && c.resolved) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
all.push({
|
|
1428
|
+
id: c.id,
|
|
1429
|
+
slideIndex: si,
|
|
1430
|
+
text: c.text,
|
|
1431
|
+
author: c.author,
|
|
1432
|
+
createdAt: c.createdAt,
|
|
1433
|
+
resolved: c.resolved
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
pptxData: ctx.pptxData,
|
|
1439
|
+
dirty: false,
|
|
1440
|
+
result: { action: "list", comments: all, count: all.length }
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
case "add": {
|
|
1444
|
+
if (params.slideIndex === void 0) {
|
|
1445
|
+
throw new Error("slideIndex is required for add action.");
|
|
1446
|
+
}
|
|
1447
|
+
const err = validateSlideIndex(params.slideIndex, ctx.pptxData.slides.length);
|
|
1448
|
+
if (err) {
|
|
1449
|
+
throw new Error(err);
|
|
1450
|
+
}
|
|
1451
|
+
if (!params.text) {
|
|
1452
|
+
throw new Error("text is required for add action.");
|
|
1453
|
+
}
|
|
1454
|
+
const slide = ctx.pptxData.slides[params.slideIndex];
|
|
1455
|
+
if (!slide.comments) {
|
|
1456
|
+
slide.comments = [];
|
|
1457
|
+
}
|
|
1458
|
+
const newId = `comment-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1459
|
+
slide.comments.push({
|
|
1460
|
+
id: newId,
|
|
1461
|
+
text: params.text,
|
|
1462
|
+
author: params.author,
|
|
1463
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1464
|
+
resolved: false
|
|
1465
|
+
});
|
|
1466
|
+
return {
|
|
1467
|
+
pptxData: ctx.pptxData,
|
|
1468
|
+
dirty: true,
|
|
1469
|
+
result: { action: "add", commentId: newId }
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
case "delete": {
|
|
1473
|
+
if (!params.commentId) {
|
|
1474
|
+
throw new Error("commentId is required for delete action.");
|
|
1475
|
+
}
|
|
1476
|
+
let deleted = false;
|
|
1477
|
+
for (const slide of ctx.pptxData.slides) {
|
|
1478
|
+
if (!slide.comments) {
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
const before = slide.comments.length;
|
|
1482
|
+
slide.comments = slide.comments.filter((c) => c.id !== params.commentId);
|
|
1483
|
+
if (slide.comments.length < before) {
|
|
1484
|
+
deleted = true;
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (!deleted) {
|
|
1489
|
+
throw new Error(`Comment '${params.commentId}' not found.`);
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
pptxData: ctx.pptxData,
|
|
1493
|
+
dirty: true,
|
|
1494
|
+
result: { action: "delete", commentId: params.commentId }
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
case "resolve": {
|
|
1498
|
+
if (!params.commentId) {
|
|
1499
|
+
throw new Error("commentId is required for resolve action.");
|
|
1500
|
+
}
|
|
1501
|
+
let found = false;
|
|
1502
|
+
for (const slide of ctx.pptxData.slides) {
|
|
1503
|
+
const comment = slide.comments?.find((c) => c.id === params.commentId);
|
|
1504
|
+
if (comment) {
|
|
1505
|
+
comment.resolved = params.resolved !== false;
|
|
1506
|
+
found = true;
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (!found) {
|
|
1511
|
+
throw new Error(`Comment '${params.commentId}' not found.`);
|
|
1512
|
+
}
|
|
1513
|
+
return {
|
|
1514
|
+
pptxData: ctx.pptxData,
|
|
1515
|
+
dirty: true,
|
|
1516
|
+
result: { action: "resolve", commentId: params.commentId }
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
default: {
|
|
1520
|
+
throw new Error(`Unknown action: ${String(params.action)}`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async function convertToMarkdown(ctx, params) {
|
|
1525
|
+
const options = {
|
|
1526
|
+
mediaFolderName: params.mediaFolderName ?? "media",
|
|
1527
|
+
includeMetadata: params.includeMetadata !== false,
|
|
1528
|
+
slideRange: params.slideRange,
|
|
1529
|
+
includeSpeakerNotes: params.includeSpeakerNotes !== false,
|
|
1530
|
+
semanticMode: params.semanticMode === true,
|
|
1531
|
+
sourceName: params.sourceName ?? "unknown"
|
|
1532
|
+
};
|
|
1533
|
+
const converter = new PptxMarkdownConverter(params.outputDir ?? ".", options);
|
|
1534
|
+
const markdown = await converter.convert(ctx.pptxData);
|
|
1535
|
+
return {
|
|
1536
|
+
pptxData: ctx.pptxData,
|
|
1537
|
+
dirty: false,
|
|
1538
|
+
result: {
|
|
1539
|
+
markdown,
|
|
1540
|
+
slidesConverted: converter.slidesConverted,
|
|
1541
|
+
totalSlides: converter.presentationSlides,
|
|
1542
|
+
imagesExtracted: converter.imagesExtracted
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
export { addElement, addSlide, arrangeElements, batchUpdateElements, cloneElement, convertToMarkdown, deleteElements, deleteSlides, describeElement, duplicateSlide, executeToolWithContext, extractSlideText, findText, generateElementId, generateSlideId, getSlide, groupElements, loadPresentation, manageComments, manageTableStructure, reorderSlides, replaceText, runAccessibilityCheck, savePresentation, setCanvasSize, setElementAnimation, setSlideTransition, ungroupElements, updateElement, updateElementStyle, updateSlideProperties, updateTableCells, validateSlideIndex };
|