@pptx-glimpse/editor 0.1.0 → 0.2.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 CHANGED
@@ -1,66 +1,144 @@
1
1
  # @pptx-glimpse/editor
2
2
 
3
- UI framework に依存しない `PptxSourceModel` の編集層です。`@pptx-glimpse/document`
4
- で PPTX を読み書きし、この package で command の検証・適用、selection、undo / redo、
5
- warning の取得を行います。
3
+ [![npm version](https://img.shields.io/npm/v/%40pptx-glimpse%2Feditor)](https://www.npmjs.com/package/@pptx-glimpse/editor)
4
+ [![CI](https://github.com/hirokisakabe/pptx-glimpse/actions/workflows/ci.yml/badge.svg)](https://github.com/hirokisakabe/pptx-glimpse/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/hirokisakabe/pptx-glimpse/blob/main/LICENSE)
6
6
 
7
- `@pptx-glimpse/editor` 0.x です。API は独立して利用できますが、command と結果型は
8
- 1.0 までに変更される可能性があります。
7
+ The public, headless editing layer for `PptxSourceModel`. It applies validated commands and manages
8
+ selection and undo/redo history without depending on a DOM, UI framework, or renderer.
9
9
 
10
- ## インストール
10
+ This package is published on npm. Install it together with its document-layer dependency so both
11
+ packages are direct dependencies of your application:
11
12
 
12
- ```sh
13
+ ```bash
13
14
  npm install @pptx-glimpse/document @pptx-glimpse/editor
14
15
  ```
15
16
 
16
- ## 最小例
17
+ ## Quick start
17
18
 
18
19
  ```ts
20
+ import { readFile, writeFile } from "node:fs/promises";
19
21
  import { readPptx, writePptx } from "@pptx-glimpse/document";
20
22
  import { createEditorSession } from "@pptx-glimpse/editor";
21
23
 
22
- const source = readPptx(input);
23
- const run = source.slides[0]?.shapes
24
- .find((shape) => shape.kind === "shape")
25
- ?.textBody?.paragraphs[0]?.runs[0];
24
+ const source = readPptx(await readFile("input.pptx"));
25
+ const run = source.slides[0]?.shapes.find((shape) => shape.kind === "shape")?.textBody
26
+ ?.paragraphs[0]?.runs[0];
26
27
 
27
- if (run?.handle === undefined) {
28
- throw new Error("editable text run not found");
28
+ if (run === undefined) {
29
+ throw new Error("No editable text run found");
29
30
  }
30
31
 
31
32
  const session = createEditorSession(source);
32
- const result = session.apply({
33
- kind: "replaceTextRunPlainText",
34
- handle: run.handle,
35
- text: "Edited",
36
- });
33
+ const result = session.replaceTextRunPlainText(run, "Edited");
34
+
35
+ if (!result.ok) {
36
+ throw new Error(`${result.code}: ${result.message}`, { cause: result.cause });
37
+ }
38
+
39
+ for (const warning of result.warnings ?? []) {
40
+ console.warn(warning.code, warning.message);
41
+ }
42
+
43
+ await writeFile("edited.pptx", writePptx(session.document));
44
+ ```
45
+
46
+ ## Source-node methods, commands, history, and selection
47
+
48
+ For ordinary edits, pass source nodes directly to the corresponding `EditorSession` convenience
49
+ method. Text-run and paragraph methods replace plain text or set/clear properties. Shape methods
50
+ move, resize, transform, style, or delete a `SourceShapeNode`; `replaceImage()` accepts a
51
+ `SourceImage`; slide topology methods accept a `SourceSlide`. `addTextBox()` and `addConnector()`
52
+ also accept the target `SourceSlide`, so application code does not need to extract source handles.
53
+ Nodes captured before earlier edits remain usable because the session resolves their stable handle
54
+ against its current document.
55
+
56
+ Every convenience method creates and applies the matching command through the same validation,
57
+ warning, selection-reconciliation, and undo/redo-history path. A successful convenience-method
58
+ call that changes the document creates one undo-history entry.
59
+
60
+ Use `session.apply(command)` when commands need to be stored, logged, transported by UI code, or
61
+ constructed independently of a source-node object. Use `session.applyAll(commands)` to apply
62
+ multiple operations atomically as one undo-history entry; convenience methods are single-operation
63
+ calls and cannot be grouped into one history entry by calling them sequentially. Failed validation
64
+ returns an `invalid-command` result without changing the document.
65
+
66
+ The released command set includes:
67
+
68
+ - Text: `replaceTextRunPlainText`, `replaceParagraphPlainText`, `setTextRunProperties`,
69
+ `clearTextRunProperties`, `setParagraphProperties`, and `clearParagraphProperties`.
70
+ - Shapes: `moveShape`, `resizeShape`, `setShapeTransform`, `setShapeFill`, `setShapeOutline`,
71
+ `addTextBox`, `addConnector`, and `deleteShape`.
72
+ - Media: `replaceImage`.
73
+ - Slides: `addEmptySlideFromLayout`, `duplicateSlide`, `moveSlide`, and `deleteSlide`.
74
+
75
+ Use `undo()`, `redo()`, `canUndo`, `canRedo`, `undoDepth`, and `redoDepth` to integrate history.
76
+ `selectShape(handle)` and `deselectShape()` manage a single shape selection. Selection is reconciled
77
+ after commands and history changes, and is cleared if its shape no longer exists.
78
+
79
+ ## Operation failures and warnings
80
+
81
+ Expected failures do not throw. `apply()` / `applyAll()`, `selectShape()`, `undo()`, and `redo()`
82
+ return a discriminated result with the shared `EditorOperationFailure` shape:
83
+
84
+ ```ts
85
+ const result = session.undo();
37
86
  if (!result.ok) {
38
- throw new Error(result.message);
87
+ switch (result.code) {
88
+ case "empty-undo-stack":
89
+ // Disable or ignore the undo action.
90
+ break;
91
+ default:
92
+ console.error(result.message, result.cause);
93
+ }
39
94
  }
95
+ ```
40
96
 
41
- const output = writePptx(session.document);
97
+ The operation codes are `invalid-command`, `invalid-selection`, `empty-undo-stack`, and
98
+ `empty-redo-stack`. Unexpected programmer errors and invariant violations still throw.
99
+
100
+ Warnings describe successful operations and remain on the success result:
101
+
102
+ ```ts
103
+ const result = session.apply(replaceImageCommand);
104
+ if (result.ok) {
105
+ for (const warning of result.warnings ?? []) {
106
+ if (warning.code === "shared-media-part") {
107
+ console.warn(warning.message);
108
+ }
109
+ }
110
+ }
42
111
  ```
43
112
 
44
- 複数 command は `session.applyAll(commands)` で一つの undo 履歴として適用できます。
45
- `selectShape()` / `deselectShape()`、`undo()` / `redo()`、`canUndo` / `canRedo` も
46
- session から利用できます。
47
-
48
- ## 対応 command
49
-
50
- - text: `replaceTextRunPlainText`, `replaceParagraphPlainText`,
51
- `setTextRunProperties`, `clearTextRunProperties`, `setParagraphProperties`,
52
- `clearParagraphProperties`
53
- - shape: `moveShape`, `resizeShape`, `setShapeTransform`, `setShapeFill`,
54
- `setShapeOutline`, `addTextBox`, `addConnector`, `deleteShape`
55
- - media: `replaceImage`
56
- - slide: `addEmptySlideFromLayout`, `duplicateSlide`, `moveSlide`, `deleteSlide`
57
-
58
- ## 既知の制約
59
-
60
- - source handle がない要素や、document 層が安全に保持できない編集は
61
- `invalid-command` として拒否されます。
62
- - table、chart、SmartArt の新規編集 command はありません。
63
- - `replaceImage` が共有 media part を更新する場合、参照する複数画像が同時に変わるため
64
- `shared-media-part` warning を返します。
65
- - DOM、React、ProseMirror、renderer は含みません。描画を伴う browser editor facade
66
- `pptx-glimpse` の browser entry を利用してください。
113
+ ## Package boundary
114
+
115
+ Use [`pptx-glimpse`](https://github.com/hirokisakabe/pptx-glimpse/blob/main/packages/core/README.md)
116
+ when you want high-level rendering plus an edit/rerender/save session. Use
117
+ [`@pptx-glimpse/document`](https://github.com/hirokisakabe/pptx-glimpse/blob/main/packages/document/README.md)
118
+ directly for source/computed document semantics, authoring, immutable editing operations, and
119
+ writing without session state.
120
+
121
+ `@pptx-glimpse/editor` adds command validation, selection, warnings, and history to the document
122
+ model. It does not include React, ProseMirror, a DOM UI, SVG/PNG rendering, or file I/O. Import
123
+ supported APIs from the package root; deep source and `dist` paths are internal.
124
+
125
+ ## Runtime support, stability, and limitations
126
+
127
+ - Node.js 22 or later is supported.
128
+ - Browser bundlers can use the same byte- and model-oriented APIs; the package has no DOM or
129
+ Node.js filesystem dependency.
130
+ - `@pptx-glimpse/editor` is a `0.x` package. Commands and result types may change before `1.0.0`.
131
+ - Elements without source handles, and edits the document layer cannot safely preserve, are
132
+ rejected as `invalid-command`.
133
+ - New table, chart, and SmartArt editing commands are not available.
134
+ - Replacing a shared media part can change multiple image references and returns a
135
+ `shared-media-part` warning.
136
+ - Expected operation rejection uses the exported `EditorOperationErrorCode` and
137
+ `EditorOperationFailure` contract.
138
+
139
+ See the [project README](https://github.com/hirokisakabe/pptx-glimpse#readme) for package selection
140
+ and the demo.
141
+
142
+ ## License
143
+
144
+ MIT
package/dist/index.cjs CHANGED
@@ -66,50 +66,198 @@ var EditorSession = class {
66
66
  deselectShape() {
67
67
  this.#selection = void 0;
68
68
  }
69
+ replaceTextRunPlainText(run, text) {
70
+ return this.applyToSourceNode(
71
+ "replaceTextRunPlainText",
72
+ run,
73
+ isSourceTextRun,
74
+ import_document.findTextRunBySourceHandle,
75
+ (handle) => ({ kind: "replaceTextRunPlainText", handle, text })
76
+ );
77
+ }
78
+ replaceParagraphPlainText(paragraph, text) {
79
+ return this.applyToSourceNode(
80
+ "replaceParagraphPlainText",
81
+ paragraph,
82
+ isSourceParagraph,
83
+ import_document.findParagraphBySourceHandle,
84
+ (handle) => ({ kind: "replaceParagraphPlainText", handle, text })
85
+ );
86
+ }
87
+ setTextRunProperties(run, properties) {
88
+ return this.applyToSourceNode(
89
+ "setTextRunProperties",
90
+ run,
91
+ isSourceTextRun,
92
+ import_document.findTextRunBySourceHandle,
93
+ (handle) => ({ kind: "setTextRunProperties", handle, properties })
94
+ );
95
+ }
96
+ clearTextRunProperties(run, properties) {
97
+ return this.applyToSourceNode(
98
+ "clearTextRunProperties",
99
+ run,
100
+ isSourceTextRun,
101
+ import_document.findTextRunBySourceHandle,
102
+ (handle) => ({ kind: "clearTextRunProperties", handle, properties })
103
+ );
104
+ }
105
+ setParagraphProperties(paragraph, properties) {
106
+ return this.applyToSourceNode(
107
+ "setParagraphProperties",
108
+ paragraph,
109
+ isSourceParagraph,
110
+ import_document.findParagraphBySourceHandle,
111
+ (handle) => ({ kind: "setParagraphProperties", handle, properties })
112
+ );
113
+ }
114
+ clearParagraphProperties(paragraph, properties) {
115
+ return this.applyToSourceNode(
116
+ "clearParagraphProperties",
117
+ paragraph,
118
+ isSourceParagraph,
119
+ import_document.findParagraphBySourceHandle,
120
+ (handle) => ({ kind: "clearParagraphProperties", handle, properties })
121
+ );
122
+ }
123
+ moveShape(shape, offsetX, offsetY) {
124
+ return this.applyToShapeNode("moveShape", shape, (handle) => ({
125
+ kind: "moveShape",
126
+ handle,
127
+ offsetX,
128
+ offsetY
129
+ }));
130
+ }
131
+ resizeShape(shape, width, height) {
132
+ return this.applyToShapeNode("resizeShape", shape, (handle) => ({
133
+ kind: "resizeShape",
134
+ handle,
135
+ width,
136
+ height
137
+ }));
138
+ }
139
+ setShapeTransform(shape, transform) {
140
+ return this.applyToShapeNode("setShapeTransform", shape, (handle) => ({
141
+ kind: "setShapeTransform",
142
+ handle,
143
+ ...transform
144
+ }));
145
+ }
146
+ setShapeFill(shape, fill) {
147
+ return this.applyToShapeNode("setShapeFill", shape, (handle) => ({
148
+ kind: "setShapeFill",
149
+ handle,
150
+ fill
151
+ }));
152
+ }
153
+ setShapeOutline(shape, outline) {
154
+ return this.applyToShapeNode("setShapeOutline", shape, (handle) => ({
155
+ kind: "setShapeOutline",
156
+ handle,
157
+ outline
158
+ }));
159
+ }
160
+ addTextBox(slide, input) {
161
+ return this.applyToSlide("addTextBox", slide, (slideHandle) => ({
162
+ kind: "addTextBox",
163
+ slideHandle,
164
+ ...input
165
+ }));
166
+ }
167
+ addConnector(slide, input) {
168
+ return this.applyToSlide("addConnector", slide, (slideHandle) => ({
169
+ kind: "addConnector",
170
+ slideHandle,
171
+ ...input
172
+ }));
173
+ }
174
+ deleteShape(shape) {
175
+ return this.applyToShapeNode("deleteShape", shape, (handle) => ({
176
+ kind: "deleteShape",
177
+ handle
178
+ }));
179
+ }
180
+ replaceImage(image, bytes) {
181
+ return this.applyToSourceNode(
182
+ "replaceImage",
183
+ image,
184
+ isSourceImage,
185
+ (document, handle) => {
186
+ const shape = (0, import_document.findShapeNodeBySourceHandle)(document, handle);
187
+ return shape?.kind === "image" ? shape : void 0;
188
+ },
189
+ (handle) => ({ kind: "replaceImage", handle, bytes })
190
+ );
191
+ }
192
+ addEmptySlideFromLayout(input) {
193
+ return this.apply({ kind: "addEmptySlideFromLayout", ...input });
194
+ }
195
+ duplicateSlide(slide) {
196
+ return this.applyToSlide("duplicateSlide", slide, (handle) => ({
197
+ kind: "duplicateSlide",
198
+ handle
199
+ }));
200
+ }
201
+ moveSlide(slide, input) {
202
+ return this.applyToSlide("moveSlide", slide, (handle) => ({
203
+ kind: "moveSlide",
204
+ handle,
205
+ ...input
206
+ }));
207
+ }
208
+ deleteSlide(slide) {
209
+ return this.applyToSlide("deleteSlide", slide, (handle) => ({
210
+ kind: "deleteSlide",
211
+ handle
212
+ }));
213
+ }
69
214
  apply(command) {
70
215
  return this.applyAll([command]);
71
216
  }
72
217
  applyAll(commands) {
73
218
  const before = this.#document;
74
219
  if (commands.length === 0) return { ok: true, document: before };
220
+ let after = before;
221
+ const warnings = [];
222
+ for (const command of commands) {
223
+ const result = applyCommandToDocument(after, command);
224
+ if (!result.ok) return result;
225
+ after = result.document;
226
+ warnings.push(...collectCommandWarnings(after, [command]));
227
+ }
75
228
  try {
76
- let after = before;
77
- const warnings = [];
78
- for (const command of commands) {
79
- after = applyCommandToDocument(after, command);
80
- warnings.push(...collectCommandWarnings(after, [command]));
81
- }
82
229
  assertNoConflictingParagraphAndRunEdits(after.edits);
83
- after = normalizeEditorEdits(after);
84
- const dedupedWarnings = dedupeCommandWarnings(warnings);
85
- if (after === before) {
86
- return {
87
- ok: true,
88
- document: before,
89
- ...dedupedWarnings.length > 0 ? { warnings: dedupedWarnings } : {}
90
- };
91
- }
92
- this.#document = after;
93
- this.reconcileSelectionAfterDocumentChange();
94
- this.#undoStack.push({ before, after });
95
- this.#redoStack.length = 0;
230
+ } catch (cause) {
231
+ return invalidCommandFailure(cause);
232
+ }
233
+ after = normalizeEditorEdits(after);
234
+ const dedupedWarnings = dedupeCommandWarnings(warnings);
235
+ if (after === before) {
96
236
  return {
97
237
  ok: true,
98
- document: after,
238
+ document: before,
99
239
  ...dedupedWarnings.length > 0 ? { warnings: dedupedWarnings } : {}
100
240
  };
101
- } catch (error) {
102
- return {
103
- ok: false,
104
- code: "invalid-command",
105
- message: error instanceof Error ? error.message : "Editor command was rejected.",
106
- cause: error
107
- };
108
241
  }
242
+ this.#document = after;
243
+ this.reconcileSelectionAfterDocumentChange();
244
+ this.#undoStack.push({ before, after });
245
+ this.#redoStack.length = 0;
246
+ return {
247
+ ok: true,
248
+ document: after,
249
+ ...dedupedWarnings.length > 0 ? { warnings: dedupedWarnings } : {}
250
+ };
109
251
  }
110
252
  undo() {
111
253
  const entry = this.#undoStack.pop();
112
- if (entry === void 0) return { ok: false, reason: "empty-undo-stack" };
254
+ if (entry === void 0) {
255
+ return {
256
+ ok: false,
257
+ code: "empty-undo-stack",
258
+ message: "undo: undo history is empty"
259
+ };
260
+ }
113
261
  this.#document = entry.before;
114
262
  this.reconcileSelectionAfterDocumentChange();
115
263
  this.#redoStack.push(entry);
@@ -117,7 +265,13 @@ var EditorSession = class {
117
265
  }
118
266
  redo() {
119
267
  const entry = this.#redoStack.pop();
120
- if (entry === void 0) return { ok: false, reason: "empty-redo-stack" };
268
+ if (entry === void 0) {
269
+ return {
270
+ ok: false,
271
+ code: "empty-redo-stack",
272
+ message: "redo: redo history is empty"
273
+ };
274
+ }
121
275
  this.#document = entry.after;
122
276
  this.reconcileSelectionAfterDocumentChange();
123
277
  this.#undoStack.push(entry);
@@ -129,11 +283,172 @@ var EditorSession = class {
129
283
  this.#selection = void 0;
130
284
  }
131
285
  }
286
+ applyToShapeNode(operation, shape, createCommand) {
287
+ return this.applyToSourceNode(
288
+ operation,
289
+ shape,
290
+ isSourceShapeNode,
291
+ import_document.findShapeNodeBySourceHandle,
292
+ createCommand
293
+ );
294
+ }
295
+ applyToSlide(operation, slide, createCommand) {
296
+ return this.applyToSourceNode(
297
+ operation,
298
+ slide,
299
+ isSourceSlide,
300
+ (document, handle) => document.slides.find((candidate) => sourceHandlesEqual(candidate.handle, handle)),
301
+ createCommand
302
+ );
303
+ }
304
+ applyToSourceNode(operation, node, isExpectedNode, findCurrentNode, createCommand) {
305
+ if (!isExpectedNode(node)) {
306
+ return invalidSourceNodeFailure(operation, "source node has the wrong target type");
307
+ }
308
+ if (node.handle === void 0) {
309
+ return invalidSourceNodeFailure(operation, "source node does not have a handle");
310
+ }
311
+ if (findCurrentNode(this.#document, node.handle) === void 0) {
312
+ return invalidSourceNodeFailure(
313
+ operation,
314
+ "source node handle was not found in the current EditorSession document"
315
+ );
316
+ }
317
+ return this.apply(createCommand(node.handle));
318
+ }
132
319
  };
133
320
  function createEditorSession(document) {
134
321
  return new EditorSession(document);
135
322
  }
323
+ var EDITOR_COMMAND_KINDS = /* @__PURE__ */ new Set([
324
+ "replaceTextRunPlainText",
325
+ "replaceParagraphPlainText",
326
+ "setTextRunProperties",
327
+ "clearTextRunProperties",
328
+ "setParagraphProperties",
329
+ "clearParagraphProperties",
330
+ "moveShape",
331
+ "resizeShape",
332
+ "setShapeTransform",
333
+ "setShapeFill",
334
+ "setShapeOutline",
335
+ "addTextBox",
336
+ "addConnector",
337
+ "deleteShape",
338
+ "replaceImage",
339
+ "addEmptySlideFromLayout",
340
+ "duplicateSlide",
341
+ "moveSlide",
342
+ "deleteSlide"
343
+ ]);
344
+ var EXPECTED_COMMAND_REJECTION_PREFIXES = [
345
+ "replaceTextRunPlainText:",
346
+ "replaceParagraphPlainText:",
347
+ "setTextRunProperties:",
348
+ "clearTextRunProperties:",
349
+ "setParagraphProperties:",
350
+ "clearParagraphProperties:",
351
+ "moveShape:",
352
+ "resizeShape:",
353
+ "setShapeTransform:",
354
+ "setShapeFill:",
355
+ "setShapeOutline:",
356
+ "addTextBox:",
357
+ "addConnector:",
358
+ "deleteShape:",
359
+ "replaceImageBytes:",
360
+ "addEmptySlideFromLayout:",
361
+ "duplicateSlide:",
362
+ "moveSlide:",
363
+ "deleteSlide:",
364
+ "updateTextRunProperties:",
365
+ "updateParagraphProperties:",
366
+ "updateShapeTransform:"
367
+ ];
136
368
  function applyCommandToDocument(document, command) {
369
+ if (!EDITOR_COMMAND_KINDS.has(command.kind)) {
370
+ throw new TypeError(`EditorSession: unsupported command kind '${String(command.kind)}'`);
371
+ }
372
+ switch (command.kind) {
373
+ case "replaceTextRunPlainText":
374
+ case "replaceParagraphPlainText":
375
+ case "setTextRunProperties":
376
+ case "clearTextRunProperties":
377
+ case "setParagraphProperties":
378
+ case "clearParagraphProperties":
379
+ case "moveShape":
380
+ case "resizeShape":
381
+ case "setShapeTransform":
382
+ case "setShapeFill":
383
+ case "setShapeOutline":
384
+ case "addTextBox":
385
+ case "addConnector":
386
+ case "deleteShape":
387
+ case "replaceImage":
388
+ case "addEmptySlideFromLayout":
389
+ case "duplicateSlide":
390
+ case "moveSlide":
391
+ case "deleteSlide":
392
+ return attemptCommand(() => executeCommand(document, command));
393
+ }
394
+ }
395
+ function attemptCommand(operation) {
396
+ try {
397
+ return { ok: true, document: operation() };
398
+ } catch (cause) {
399
+ return invalidCommandFailure(cause);
400
+ }
401
+ }
402
+ function invalidCommandFailure(cause) {
403
+ if (!(cause instanceof Error) || !EXPECTED_COMMAND_REJECTION_PREFIXES.some((prefix) => cause.message.startsWith(prefix))) {
404
+ throw cause;
405
+ }
406
+ return {
407
+ ok: false,
408
+ code: "invalid-command",
409
+ message: cause.message,
410
+ cause
411
+ };
412
+ }
413
+ function invalidSourceNodeFailure(operation, reason) {
414
+ return {
415
+ ok: false,
416
+ code: "invalid-command",
417
+ message: `${operation}: ${reason}`
418
+ };
419
+ }
420
+ function isSourceTextRun(value) {
421
+ return isObject(value) && value.kind === "textRun";
422
+ }
423
+ function isSourceParagraph(value) {
424
+ return isObject(value) && Array.isArray(value.runs);
425
+ }
426
+ var SOURCE_SHAPE_NODE_KINDS = /* @__PURE__ */ new Set([
427
+ "shape",
428
+ "connector",
429
+ "group",
430
+ "image",
431
+ "table",
432
+ "chart",
433
+ "smartArt",
434
+ "raw"
435
+ ]);
436
+ function isSourceShapeNode(value) {
437
+ return isObject(value) && typeof value.kind === "string" && SOURCE_SHAPE_NODE_KINDS.has(value.kind);
438
+ }
439
+ function isSourceImage(value) {
440
+ return isObject(value) && value.kind === "image";
441
+ }
442
+ function isSourceSlide(value) {
443
+ return isObject(value) && typeof value.partPath === "string" && typeof value.layoutPartPath === "string" && Array.isArray(value.shapes);
444
+ }
445
+ function isObject(value) {
446
+ return typeof value === "object" && value !== null;
447
+ }
448
+ function sourceHandlesEqual(left, right) {
449
+ return left !== void 0 && left.partPath === right.partPath && left.nodeId === right.nodeId && left.relationshipId === right.relationshipId && left.orderingSlot === right.orderingSlot;
450
+ }
451
+ function executeCommand(document, command) {
137
452
  switch (command.kind) {
138
453
  case "replaceTextRunPlainText":
139
454
  return replaceTextRunPlainTextCommand(document, command);
package/dist/index.d.cts CHANGED
@@ -1,4 +1,13 @@
1
- import { AddConnectorInput, SourceHandle, AddEmptySlideFromLayoutInput, AddTextBoxInput, EditableParagraphProperty, EditableTextRunProperty, PptxSourceModel, EditableTextRunProperties, EditableParagraphProperties, Emu, EditableShapeFill, EditableShapeOutline, MoveSlideInput } from '@pptx-glimpse/document';
1
+ import { AddConnectorInput, SourceHandle, AddEmptySlideFromLayoutInput, AddTextBoxInput, EditableParagraphProperty, EditableTextRunProperty, PptxSourceModel, EditableTextRunProperties, EditableParagraphProperties, Emu, EditableShapeFill, EditableShapeOutline, MoveSlideInput, SourceTextRun, SourceParagraph, SourceShapeNode, SourceTransform, SourceSlide, SourceImage } from '@pptx-glimpse/document';
2
+
3
+ /**
4
+ * Headless editor commands, selection, warnings, and history.
5
+ *
6
+ * Expected operation rejections use the shared discriminated failure contract exported
7
+ * by this module. Unexpected programmer errors and invariant violations must propagate.
8
+ * The repository-wide ownership, conversion, and catch rules are documented in
9
+ * [the editor error contract](../../../docs/editor-error-contract.md).
10
+ */
2
11
 
3
12
  interface ReplaceTextRunPlainTextCommand {
4
13
  readonly kind: "replaceTextRunPlainText";
@@ -93,16 +102,18 @@ interface DeleteSlideCommand {
93
102
  readonly handle: SourceHandle;
94
103
  }
95
104
  type EditorCommand = ReplaceTextRunPlainTextCommand | ReplaceParagraphPlainTextCommand | SetTextRunPropertiesCommand | ClearTextRunPropertiesCommand | SetParagraphPropertiesCommand | ClearParagraphPropertiesCommand | MoveShapeCommand | ResizeShapeCommand | SetShapeTransformCommand | SetShapeFillCommand | SetShapeOutlineCommand | AddTextBoxCommand | AddConnectorCommand | DeleteShapeCommand | ReplaceImageCommand | AddEmptySlideFromLayoutCommand | DuplicateSlideCommand | MoveSlideCommand | DeleteSlideCommand;
105
+ type EditorOperationErrorCode = "invalid-command" | "invalid-selection" | "empty-undo-stack" | "empty-redo-stack";
106
+ interface EditorOperationFailure<Code extends EditorOperationErrorCode = EditorOperationErrorCode> {
107
+ readonly ok: false;
108
+ readonly code: Code;
109
+ readonly message: string;
110
+ readonly cause?: unknown;
111
+ }
96
112
  type EditorApplyCommandResult = {
97
113
  readonly ok: true;
98
114
  readonly document: PptxSourceModel;
99
115
  readonly warnings?: readonly EditorCommandWarning[];
100
- } | {
101
- readonly ok: false;
102
- readonly code: "invalid-command";
103
- readonly message: string;
104
- readonly cause?: unknown;
105
- };
116
+ } | EditorOperationFailure<"invalid-command">;
106
117
  interface EditorCommandWarning {
107
118
  readonly code: "shared-media-part";
108
119
  readonly message: string;
@@ -112,21 +123,14 @@ interface EditorCommandWarning {
112
123
  type EditorHistoryResult = {
113
124
  readonly ok: true;
114
125
  readonly document: PptxSourceModel;
115
- } | {
116
- readonly ok: false;
117
- readonly reason: "empty-undo-stack" | "empty-redo-stack";
118
- };
126
+ } | EditorOperationFailure<"empty-undo-stack" | "empty-redo-stack">;
119
127
  interface EditorSelection {
120
128
  readonly shapeHandle: SourceHandle;
121
129
  }
122
130
  type EditorSelectShapeResult = {
123
131
  readonly ok: true;
124
132
  readonly selection: EditorSelection;
125
- } | {
126
- readonly ok: false;
127
- readonly code: "invalid-selection";
128
- readonly message: string;
129
- };
133
+ } | EditorOperationFailure<"invalid-selection">;
130
134
  declare class EditorSession {
131
135
  #private;
132
136
  constructor(document: PptxSourceModel);
@@ -138,12 +142,34 @@ declare class EditorSession {
138
142
  get redoDepth(): number;
139
143
  selectShape(handle: SourceHandle): EditorSelectShapeResult;
140
144
  deselectShape(): void;
145
+ replaceTextRunPlainText(run: SourceTextRun, text: string): EditorApplyCommandResult;
146
+ replaceParagraphPlainText(paragraph: SourceParagraph, text: string): EditorApplyCommandResult;
147
+ setTextRunProperties(run: SourceTextRun, properties: EditableTextRunProperties): EditorApplyCommandResult;
148
+ clearTextRunProperties(run: SourceTextRun, properties: readonly EditableTextRunProperty[]): EditorApplyCommandResult;
149
+ setParagraphProperties(paragraph: SourceParagraph, properties: EditableParagraphProperties): EditorApplyCommandResult;
150
+ clearParagraphProperties(paragraph: SourceParagraph, properties: readonly EditableParagraphProperty[]): EditorApplyCommandResult;
151
+ moveShape(shape: SourceShapeNode, offsetX: Emu, offsetY: Emu): EditorApplyCommandResult;
152
+ resizeShape(shape: SourceShapeNode, width: Emu, height: Emu): EditorApplyCommandResult;
153
+ setShapeTransform(shape: SourceShapeNode, transform: Pick<SourceTransform, "offsetX" | "offsetY" | "width" | "height">): EditorApplyCommandResult;
154
+ setShapeFill(shape: SourceShapeNode, fill: EditableShapeFill): EditorApplyCommandResult;
155
+ setShapeOutline(shape: SourceShapeNode, outline: EditableShapeOutline): EditorApplyCommandResult;
156
+ addTextBox(slide: SourceSlide, input: AddTextBoxInput): EditorApplyCommandResult;
157
+ addConnector(slide: SourceSlide, input: AddConnectorInput): EditorApplyCommandResult;
158
+ deleteShape(shape: SourceShapeNode): EditorApplyCommandResult;
159
+ replaceImage(image: SourceImage, bytes: Uint8Array): EditorApplyCommandResult;
160
+ addEmptySlideFromLayout(input: AddEmptySlideFromLayoutInput): EditorApplyCommandResult;
161
+ duplicateSlide(slide: SourceSlide): EditorApplyCommandResult;
162
+ moveSlide(slide: SourceSlide, input: MoveSlideInput): EditorApplyCommandResult;
163
+ deleteSlide(slide: SourceSlide): EditorApplyCommandResult;
141
164
  apply(command: EditorCommand): EditorApplyCommandResult;
142
165
  applyAll(commands: readonly EditorCommand[]): EditorApplyCommandResult;
143
166
  undo(): EditorHistoryResult;
144
167
  redo(): EditorHistoryResult;
145
168
  private reconcileSelectionAfterDocumentChange;
169
+ private applyToShapeNode;
170
+ private applyToSlide;
171
+ private applyToSourceNode;
146
172
  }
147
173
  declare function createEditorSession(document: PptxSourceModel): EditorSession;
148
174
 
149
- export { type AddConnectorCommand, type AddEmptySlideFromLayoutCommand, type AddTextBoxCommand, type ClearParagraphPropertiesCommand, type ClearTextRunPropertiesCommand, type DeleteShapeCommand, type DeleteSlideCommand, type DuplicateSlideCommand, type EditorApplyCommandResult, type EditorCommand, type EditorCommandWarning, type EditorHistoryResult, type EditorSelectShapeResult, type EditorSelection, EditorSession, type MoveShapeCommand, type MoveSlideCommand, type ReplaceImageCommand, type ReplaceParagraphPlainTextCommand, type ReplaceTextRunPlainTextCommand, type ResizeShapeCommand, type SetParagraphPropertiesCommand, type SetShapeFillCommand, type SetShapeOutlineCommand, type SetShapeTransformCommand, type SetTextRunPropertiesCommand, createEditorSession };
175
+ export { type AddConnectorCommand, type AddEmptySlideFromLayoutCommand, type AddTextBoxCommand, type ClearParagraphPropertiesCommand, type ClearTextRunPropertiesCommand, type DeleteShapeCommand, type DeleteSlideCommand, type DuplicateSlideCommand, type EditorApplyCommandResult, type EditorCommand, type EditorCommandWarning, type EditorHistoryResult, type EditorOperationErrorCode, type EditorOperationFailure, type EditorSelectShapeResult, type EditorSelection, EditorSession, type MoveShapeCommand, type MoveSlideCommand, type ReplaceImageCommand, type ReplaceParagraphPlainTextCommand, type ReplaceTextRunPlainTextCommand, type ResizeShapeCommand, type SetParagraphPropertiesCommand, type SetShapeFillCommand, type SetShapeOutlineCommand, type SetShapeTransformCommand, type SetTextRunPropertiesCommand, createEditorSession };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,13 @@
1
- import { AddConnectorInput, SourceHandle, AddEmptySlideFromLayoutInput, AddTextBoxInput, EditableParagraphProperty, EditableTextRunProperty, PptxSourceModel, EditableTextRunProperties, EditableParagraphProperties, Emu, EditableShapeFill, EditableShapeOutline, MoveSlideInput } from '@pptx-glimpse/document';
1
+ import { AddConnectorInput, SourceHandle, AddEmptySlideFromLayoutInput, AddTextBoxInput, EditableParagraphProperty, EditableTextRunProperty, PptxSourceModel, EditableTextRunProperties, EditableParagraphProperties, Emu, EditableShapeFill, EditableShapeOutline, MoveSlideInput, SourceTextRun, SourceParagraph, SourceShapeNode, SourceTransform, SourceSlide, SourceImage } from '@pptx-glimpse/document';
2
+
3
+ /**
4
+ * Headless editor commands, selection, warnings, and history.
5
+ *
6
+ * Expected operation rejections use the shared discriminated failure contract exported
7
+ * by this module. Unexpected programmer errors and invariant violations must propagate.
8
+ * The repository-wide ownership, conversion, and catch rules are documented in
9
+ * [the editor error contract](../../../docs/editor-error-contract.md).
10
+ */
2
11
 
3
12
  interface ReplaceTextRunPlainTextCommand {
4
13
  readonly kind: "replaceTextRunPlainText";
@@ -93,16 +102,18 @@ interface DeleteSlideCommand {
93
102
  readonly handle: SourceHandle;
94
103
  }
95
104
  type EditorCommand = ReplaceTextRunPlainTextCommand | ReplaceParagraphPlainTextCommand | SetTextRunPropertiesCommand | ClearTextRunPropertiesCommand | SetParagraphPropertiesCommand | ClearParagraphPropertiesCommand | MoveShapeCommand | ResizeShapeCommand | SetShapeTransformCommand | SetShapeFillCommand | SetShapeOutlineCommand | AddTextBoxCommand | AddConnectorCommand | DeleteShapeCommand | ReplaceImageCommand | AddEmptySlideFromLayoutCommand | DuplicateSlideCommand | MoveSlideCommand | DeleteSlideCommand;
105
+ type EditorOperationErrorCode = "invalid-command" | "invalid-selection" | "empty-undo-stack" | "empty-redo-stack";
106
+ interface EditorOperationFailure<Code extends EditorOperationErrorCode = EditorOperationErrorCode> {
107
+ readonly ok: false;
108
+ readonly code: Code;
109
+ readonly message: string;
110
+ readonly cause?: unknown;
111
+ }
96
112
  type EditorApplyCommandResult = {
97
113
  readonly ok: true;
98
114
  readonly document: PptxSourceModel;
99
115
  readonly warnings?: readonly EditorCommandWarning[];
100
- } | {
101
- readonly ok: false;
102
- readonly code: "invalid-command";
103
- readonly message: string;
104
- readonly cause?: unknown;
105
- };
116
+ } | EditorOperationFailure<"invalid-command">;
106
117
  interface EditorCommandWarning {
107
118
  readonly code: "shared-media-part";
108
119
  readonly message: string;
@@ -112,21 +123,14 @@ interface EditorCommandWarning {
112
123
  type EditorHistoryResult = {
113
124
  readonly ok: true;
114
125
  readonly document: PptxSourceModel;
115
- } | {
116
- readonly ok: false;
117
- readonly reason: "empty-undo-stack" | "empty-redo-stack";
118
- };
126
+ } | EditorOperationFailure<"empty-undo-stack" | "empty-redo-stack">;
119
127
  interface EditorSelection {
120
128
  readonly shapeHandle: SourceHandle;
121
129
  }
122
130
  type EditorSelectShapeResult = {
123
131
  readonly ok: true;
124
132
  readonly selection: EditorSelection;
125
- } | {
126
- readonly ok: false;
127
- readonly code: "invalid-selection";
128
- readonly message: string;
129
- };
133
+ } | EditorOperationFailure<"invalid-selection">;
130
134
  declare class EditorSession {
131
135
  #private;
132
136
  constructor(document: PptxSourceModel);
@@ -138,12 +142,34 @@ declare class EditorSession {
138
142
  get redoDepth(): number;
139
143
  selectShape(handle: SourceHandle): EditorSelectShapeResult;
140
144
  deselectShape(): void;
145
+ replaceTextRunPlainText(run: SourceTextRun, text: string): EditorApplyCommandResult;
146
+ replaceParagraphPlainText(paragraph: SourceParagraph, text: string): EditorApplyCommandResult;
147
+ setTextRunProperties(run: SourceTextRun, properties: EditableTextRunProperties): EditorApplyCommandResult;
148
+ clearTextRunProperties(run: SourceTextRun, properties: readonly EditableTextRunProperty[]): EditorApplyCommandResult;
149
+ setParagraphProperties(paragraph: SourceParagraph, properties: EditableParagraphProperties): EditorApplyCommandResult;
150
+ clearParagraphProperties(paragraph: SourceParagraph, properties: readonly EditableParagraphProperty[]): EditorApplyCommandResult;
151
+ moveShape(shape: SourceShapeNode, offsetX: Emu, offsetY: Emu): EditorApplyCommandResult;
152
+ resizeShape(shape: SourceShapeNode, width: Emu, height: Emu): EditorApplyCommandResult;
153
+ setShapeTransform(shape: SourceShapeNode, transform: Pick<SourceTransform, "offsetX" | "offsetY" | "width" | "height">): EditorApplyCommandResult;
154
+ setShapeFill(shape: SourceShapeNode, fill: EditableShapeFill): EditorApplyCommandResult;
155
+ setShapeOutline(shape: SourceShapeNode, outline: EditableShapeOutline): EditorApplyCommandResult;
156
+ addTextBox(slide: SourceSlide, input: AddTextBoxInput): EditorApplyCommandResult;
157
+ addConnector(slide: SourceSlide, input: AddConnectorInput): EditorApplyCommandResult;
158
+ deleteShape(shape: SourceShapeNode): EditorApplyCommandResult;
159
+ replaceImage(image: SourceImage, bytes: Uint8Array): EditorApplyCommandResult;
160
+ addEmptySlideFromLayout(input: AddEmptySlideFromLayoutInput): EditorApplyCommandResult;
161
+ duplicateSlide(slide: SourceSlide): EditorApplyCommandResult;
162
+ moveSlide(slide: SourceSlide, input: MoveSlideInput): EditorApplyCommandResult;
163
+ deleteSlide(slide: SourceSlide): EditorApplyCommandResult;
141
164
  apply(command: EditorCommand): EditorApplyCommandResult;
142
165
  applyAll(commands: readonly EditorCommand[]): EditorApplyCommandResult;
143
166
  undo(): EditorHistoryResult;
144
167
  redo(): EditorHistoryResult;
145
168
  private reconcileSelectionAfterDocumentChange;
169
+ private applyToShapeNode;
170
+ private applyToSlide;
171
+ private applyToSourceNode;
146
172
  }
147
173
  declare function createEditorSession(document: PptxSourceModel): EditorSession;
148
174
 
149
- export { type AddConnectorCommand, type AddEmptySlideFromLayoutCommand, type AddTextBoxCommand, type ClearParagraphPropertiesCommand, type ClearTextRunPropertiesCommand, type DeleteShapeCommand, type DeleteSlideCommand, type DuplicateSlideCommand, type EditorApplyCommandResult, type EditorCommand, type EditorCommandWarning, type EditorHistoryResult, type EditorSelectShapeResult, type EditorSelection, EditorSession, type MoveShapeCommand, type MoveSlideCommand, type ReplaceImageCommand, type ReplaceParagraphPlainTextCommand, type ReplaceTextRunPlainTextCommand, type ResizeShapeCommand, type SetParagraphPropertiesCommand, type SetShapeFillCommand, type SetShapeOutlineCommand, type SetShapeTransformCommand, type SetTextRunPropertiesCommand, createEditorSession };
175
+ export { type AddConnectorCommand, type AddEmptySlideFromLayoutCommand, type AddTextBoxCommand, type ClearParagraphPropertiesCommand, type ClearTextRunPropertiesCommand, type DeleteShapeCommand, type DeleteSlideCommand, type DuplicateSlideCommand, type EditorApplyCommandResult, type EditorCommand, type EditorCommandWarning, type EditorHistoryResult, type EditorOperationErrorCode, type EditorOperationFailure, type EditorSelectShapeResult, type EditorSelection, EditorSession, type MoveShapeCommand, type MoveSlideCommand, type ReplaceImageCommand, type ReplaceParagraphPlainTextCommand, type ReplaceTextRunPlainTextCommand, type ResizeShapeCommand, type SetParagraphPropertiesCommand, type SetShapeFillCommand, type SetShapeOutlineCommand, type SetShapeTransformCommand, type SetTextRunPropertiesCommand, createEditorSession };
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  deleteShape,
9
9
  deleteSlide,
10
10
  duplicateSlide,
11
+ findParagraphBySourceHandle,
11
12
  findShapeNodeBySourceHandle,
13
+ findTextRunBySourceHandle,
12
14
  moveSlide,
13
15
  replaceImageBytes,
14
16
  replaceParagraphPlainText,
@@ -60,50 +62,198 @@ var EditorSession = class {
60
62
  deselectShape() {
61
63
  this.#selection = void 0;
62
64
  }
65
+ replaceTextRunPlainText(run, text) {
66
+ return this.applyToSourceNode(
67
+ "replaceTextRunPlainText",
68
+ run,
69
+ isSourceTextRun,
70
+ findTextRunBySourceHandle,
71
+ (handle) => ({ kind: "replaceTextRunPlainText", handle, text })
72
+ );
73
+ }
74
+ replaceParagraphPlainText(paragraph, text) {
75
+ return this.applyToSourceNode(
76
+ "replaceParagraphPlainText",
77
+ paragraph,
78
+ isSourceParagraph,
79
+ findParagraphBySourceHandle,
80
+ (handle) => ({ kind: "replaceParagraphPlainText", handle, text })
81
+ );
82
+ }
83
+ setTextRunProperties(run, properties) {
84
+ return this.applyToSourceNode(
85
+ "setTextRunProperties",
86
+ run,
87
+ isSourceTextRun,
88
+ findTextRunBySourceHandle,
89
+ (handle) => ({ kind: "setTextRunProperties", handle, properties })
90
+ );
91
+ }
92
+ clearTextRunProperties(run, properties) {
93
+ return this.applyToSourceNode(
94
+ "clearTextRunProperties",
95
+ run,
96
+ isSourceTextRun,
97
+ findTextRunBySourceHandle,
98
+ (handle) => ({ kind: "clearTextRunProperties", handle, properties })
99
+ );
100
+ }
101
+ setParagraphProperties(paragraph, properties) {
102
+ return this.applyToSourceNode(
103
+ "setParagraphProperties",
104
+ paragraph,
105
+ isSourceParagraph,
106
+ findParagraphBySourceHandle,
107
+ (handle) => ({ kind: "setParagraphProperties", handle, properties })
108
+ );
109
+ }
110
+ clearParagraphProperties(paragraph, properties) {
111
+ return this.applyToSourceNode(
112
+ "clearParagraphProperties",
113
+ paragraph,
114
+ isSourceParagraph,
115
+ findParagraphBySourceHandle,
116
+ (handle) => ({ kind: "clearParagraphProperties", handle, properties })
117
+ );
118
+ }
119
+ moveShape(shape, offsetX, offsetY) {
120
+ return this.applyToShapeNode("moveShape", shape, (handle) => ({
121
+ kind: "moveShape",
122
+ handle,
123
+ offsetX,
124
+ offsetY
125
+ }));
126
+ }
127
+ resizeShape(shape, width, height) {
128
+ return this.applyToShapeNode("resizeShape", shape, (handle) => ({
129
+ kind: "resizeShape",
130
+ handle,
131
+ width,
132
+ height
133
+ }));
134
+ }
135
+ setShapeTransform(shape, transform) {
136
+ return this.applyToShapeNode("setShapeTransform", shape, (handle) => ({
137
+ kind: "setShapeTransform",
138
+ handle,
139
+ ...transform
140
+ }));
141
+ }
142
+ setShapeFill(shape, fill) {
143
+ return this.applyToShapeNode("setShapeFill", shape, (handle) => ({
144
+ kind: "setShapeFill",
145
+ handle,
146
+ fill
147
+ }));
148
+ }
149
+ setShapeOutline(shape, outline) {
150
+ return this.applyToShapeNode("setShapeOutline", shape, (handle) => ({
151
+ kind: "setShapeOutline",
152
+ handle,
153
+ outline
154
+ }));
155
+ }
156
+ addTextBox(slide, input) {
157
+ return this.applyToSlide("addTextBox", slide, (slideHandle) => ({
158
+ kind: "addTextBox",
159
+ slideHandle,
160
+ ...input
161
+ }));
162
+ }
163
+ addConnector(slide, input) {
164
+ return this.applyToSlide("addConnector", slide, (slideHandle) => ({
165
+ kind: "addConnector",
166
+ slideHandle,
167
+ ...input
168
+ }));
169
+ }
170
+ deleteShape(shape) {
171
+ return this.applyToShapeNode("deleteShape", shape, (handle) => ({
172
+ kind: "deleteShape",
173
+ handle
174
+ }));
175
+ }
176
+ replaceImage(image, bytes) {
177
+ return this.applyToSourceNode(
178
+ "replaceImage",
179
+ image,
180
+ isSourceImage,
181
+ (document, handle) => {
182
+ const shape = findShapeNodeBySourceHandle(document, handle);
183
+ return shape?.kind === "image" ? shape : void 0;
184
+ },
185
+ (handle) => ({ kind: "replaceImage", handle, bytes })
186
+ );
187
+ }
188
+ addEmptySlideFromLayout(input) {
189
+ return this.apply({ kind: "addEmptySlideFromLayout", ...input });
190
+ }
191
+ duplicateSlide(slide) {
192
+ return this.applyToSlide("duplicateSlide", slide, (handle) => ({
193
+ kind: "duplicateSlide",
194
+ handle
195
+ }));
196
+ }
197
+ moveSlide(slide, input) {
198
+ return this.applyToSlide("moveSlide", slide, (handle) => ({
199
+ kind: "moveSlide",
200
+ handle,
201
+ ...input
202
+ }));
203
+ }
204
+ deleteSlide(slide) {
205
+ return this.applyToSlide("deleteSlide", slide, (handle) => ({
206
+ kind: "deleteSlide",
207
+ handle
208
+ }));
209
+ }
63
210
  apply(command) {
64
211
  return this.applyAll([command]);
65
212
  }
66
213
  applyAll(commands) {
67
214
  const before = this.#document;
68
215
  if (commands.length === 0) return { ok: true, document: before };
216
+ let after = before;
217
+ const warnings = [];
218
+ for (const command of commands) {
219
+ const result = applyCommandToDocument(after, command);
220
+ if (!result.ok) return result;
221
+ after = result.document;
222
+ warnings.push(...collectCommandWarnings(after, [command]));
223
+ }
69
224
  try {
70
- let after = before;
71
- const warnings = [];
72
- for (const command of commands) {
73
- after = applyCommandToDocument(after, command);
74
- warnings.push(...collectCommandWarnings(after, [command]));
75
- }
76
225
  assertNoConflictingParagraphAndRunEdits(after.edits);
77
- after = normalizeEditorEdits(after);
78
- const dedupedWarnings = dedupeCommandWarnings(warnings);
79
- if (after === before) {
80
- return {
81
- ok: true,
82
- document: before,
83
- ...dedupedWarnings.length > 0 ? { warnings: dedupedWarnings } : {}
84
- };
85
- }
86
- this.#document = after;
87
- this.reconcileSelectionAfterDocumentChange();
88
- this.#undoStack.push({ before, after });
89
- this.#redoStack.length = 0;
226
+ } catch (cause) {
227
+ return invalidCommandFailure(cause);
228
+ }
229
+ after = normalizeEditorEdits(after);
230
+ const dedupedWarnings = dedupeCommandWarnings(warnings);
231
+ if (after === before) {
90
232
  return {
91
233
  ok: true,
92
- document: after,
234
+ document: before,
93
235
  ...dedupedWarnings.length > 0 ? { warnings: dedupedWarnings } : {}
94
236
  };
95
- } catch (error) {
96
- return {
97
- ok: false,
98
- code: "invalid-command",
99
- message: error instanceof Error ? error.message : "Editor command was rejected.",
100
- cause: error
101
- };
102
237
  }
238
+ this.#document = after;
239
+ this.reconcileSelectionAfterDocumentChange();
240
+ this.#undoStack.push({ before, after });
241
+ this.#redoStack.length = 0;
242
+ return {
243
+ ok: true,
244
+ document: after,
245
+ ...dedupedWarnings.length > 0 ? { warnings: dedupedWarnings } : {}
246
+ };
103
247
  }
104
248
  undo() {
105
249
  const entry = this.#undoStack.pop();
106
- if (entry === void 0) return { ok: false, reason: "empty-undo-stack" };
250
+ if (entry === void 0) {
251
+ return {
252
+ ok: false,
253
+ code: "empty-undo-stack",
254
+ message: "undo: undo history is empty"
255
+ };
256
+ }
107
257
  this.#document = entry.before;
108
258
  this.reconcileSelectionAfterDocumentChange();
109
259
  this.#redoStack.push(entry);
@@ -111,7 +261,13 @@ var EditorSession = class {
111
261
  }
112
262
  redo() {
113
263
  const entry = this.#redoStack.pop();
114
- if (entry === void 0) return { ok: false, reason: "empty-redo-stack" };
264
+ if (entry === void 0) {
265
+ return {
266
+ ok: false,
267
+ code: "empty-redo-stack",
268
+ message: "redo: redo history is empty"
269
+ };
270
+ }
115
271
  this.#document = entry.after;
116
272
  this.reconcileSelectionAfterDocumentChange();
117
273
  this.#undoStack.push(entry);
@@ -123,11 +279,172 @@ var EditorSession = class {
123
279
  this.#selection = void 0;
124
280
  }
125
281
  }
282
+ applyToShapeNode(operation, shape, createCommand) {
283
+ return this.applyToSourceNode(
284
+ operation,
285
+ shape,
286
+ isSourceShapeNode,
287
+ findShapeNodeBySourceHandle,
288
+ createCommand
289
+ );
290
+ }
291
+ applyToSlide(operation, slide, createCommand) {
292
+ return this.applyToSourceNode(
293
+ operation,
294
+ slide,
295
+ isSourceSlide,
296
+ (document, handle) => document.slides.find((candidate) => sourceHandlesEqual(candidate.handle, handle)),
297
+ createCommand
298
+ );
299
+ }
300
+ applyToSourceNode(operation, node, isExpectedNode, findCurrentNode, createCommand) {
301
+ if (!isExpectedNode(node)) {
302
+ return invalidSourceNodeFailure(operation, "source node has the wrong target type");
303
+ }
304
+ if (node.handle === void 0) {
305
+ return invalidSourceNodeFailure(operation, "source node does not have a handle");
306
+ }
307
+ if (findCurrentNode(this.#document, node.handle) === void 0) {
308
+ return invalidSourceNodeFailure(
309
+ operation,
310
+ "source node handle was not found in the current EditorSession document"
311
+ );
312
+ }
313
+ return this.apply(createCommand(node.handle));
314
+ }
126
315
  };
127
316
  function createEditorSession(document) {
128
317
  return new EditorSession(document);
129
318
  }
319
+ var EDITOR_COMMAND_KINDS = /* @__PURE__ */ new Set([
320
+ "replaceTextRunPlainText",
321
+ "replaceParagraphPlainText",
322
+ "setTextRunProperties",
323
+ "clearTextRunProperties",
324
+ "setParagraphProperties",
325
+ "clearParagraphProperties",
326
+ "moveShape",
327
+ "resizeShape",
328
+ "setShapeTransform",
329
+ "setShapeFill",
330
+ "setShapeOutline",
331
+ "addTextBox",
332
+ "addConnector",
333
+ "deleteShape",
334
+ "replaceImage",
335
+ "addEmptySlideFromLayout",
336
+ "duplicateSlide",
337
+ "moveSlide",
338
+ "deleteSlide"
339
+ ]);
340
+ var EXPECTED_COMMAND_REJECTION_PREFIXES = [
341
+ "replaceTextRunPlainText:",
342
+ "replaceParagraphPlainText:",
343
+ "setTextRunProperties:",
344
+ "clearTextRunProperties:",
345
+ "setParagraphProperties:",
346
+ "clearParagraphProperties:",
347
+ "moveShape:",
348
+ "resizeShape:",
349
+ "setShapeTransform:",
350
+ "setShapeFill:",
351
+ "setShapeOutline:",
352
+ "addTextBox:",
353
+ "addConnector:",
354
+ "deleteShape:",
355
+ "replaceImageBytes:",
356
+ "addEmptySlideFromLayout:",
357
+ "duplicateSlide:",
358
+ "moveSlide:",
359
+ "deleteSlide:",
360
+ "updateTextRunProperties:",
361
+ "updateParagraphProperties:",
362
+ "updateShapeTransform:"
363
+ ];
130
364
  function applyCommandToDocument(document, command) {
365
+ if (!EDITOR_COMMAND_KINDS.has(command.kind)) {
366
+ throw new TypeError(`EditorSession: unsupported command kind '${String(command.kind)}'`);
367
+ }
368
+ switch (command.kind) {
369
+ case "replaceTextRunPlainText":
370
+ case "replaceParagraphPlainText":
371
+ case "setTextRunProperties":
372
+ case "clearTextRunProperties":
373
+ case "setParagraphProperties":
374
+ case "clearParagraphProperties":
375
+ case "moveShape":
376
+ case "resizeShape":
377
+ case "setShapeTransform":
378
+ case "setShapeFill":
379
+ case "setShapeOutline":
380
+ case "addTextBox":
381
+ case "addConnector":
382
+ case "deleteShape":
383
+ case "replaceImage":
384
+ case "addEmptySlideFromLayout":
385
+ case "duplicateSlide":
386
+ case "moveSlide":
387
+ case "deleteSlide":
388
+ return attemptCommand(() => executeCommand(document, command));
389
+ }
390
+ }
391
+ function attemptCommand(operation) {
392
+ try {
393
+ return { ok: true, document: operation() };
394
+ } catch (cause) {
395
+ return invalidCommandFailure(cause);
396
+ }
397
+ }
398
+ function invalidCommandFailure(cause) {
399
+ if (!(cause instanceof Error) || !EXPECTED_COMMAND_REJECTION_PREFIXES.some((prefix) => cause.message.startsWith(prefix))) {
400
+ throw cause;
401
+ }
402
+ return {
403
+ ok: false,
404
+ code: "invalid-command",
405
+ message: cause.message,
406
+ cause
407
+ };
408
+ }
409
+ function invalidSourceNodeFailure(operation, reason) {
410
+ return {
411
+ ok: false,
412
+ code: "invalid-command",
413
+ message: `${operation}: ${reason}`
414
+ };
415
+ }
416
+ function isSourceTextRun(value) {
417
+ return isObject(value) && value.kind === "textRun";
418
+ }
419
+ function isSourceParagraph(value) {
420
+ return isObject(value) && Array.isArray(value.runs);
421
+ }
422
+ var SOURCE_SHAPE_NODE_KINDS = /* @__PURE__ */ new Set([
423
+ "shape",
424
+ "connector",
425
+ "group",
426
+ "image",
427
+ "table",
428
+ "chart",
429
+ "smartArt",
430
+ "raw"
431
+ ]);
432
+ function isSourceShapeNode(value) {
433
+ return isObject(value) && typeof value.kind === "string" && SOURCE_SHAPE_NODE_KINDS.has(value.kind);
434
+ }
435
+ function isSourceImage(value) {
436
+ return isObject(value) && value.kind === "image";
437
+ }
438
+ function isSourceSlide(value) {
439
+ return isObject(value) && typeof value.partPath === "string" && typeof value.layoutPartPath === "string" && Array.isArray(value.shapes);
440
+ }
441
+ function isObject(value) {
442
+ return typeof value === "object" && value !== null;
443
+ }
444
+ function sourceHandlesEqual(left, right) {
445
+ return left !== void 0 && left.partPath === right.partPath && left.nodeId === right.nodeId && left.relationshipId === right.relationshipId && left.orderingSlot === right.orderingSlot;
446
+ }
447
+ function executeCommand(document, command) {
131
448
  switch (command.kind) {
132
449
  case "replaceTextRunPlainText":
133
450
  return replaceTextRunPlainTextCommand(document, command);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pptx-glimpse/editor",
3
- "version": "0.1.0",
4
- "description": "UI-independent commands, selection, history, and validation for PptxSourceModel.",
3
+ "version": "0.2.0",
4
+ "description": "Headless PPTX editing commands, validation, selection, and undo/redo history for PptxSourceModel.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -19,7 +19,7 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@pptx-glimpse/document": "^0.12.0"
22
+ "@pptx-glimpse/document": "^0.12.1"
23
23
  },
24
24
  "files": [
25
25
  "dist",
@@ -32,7 +32,11 @@
32
32
  "pptx",
33
33
  "powerpoint",
34
34
  "editor",
35
- "headless"
35
+ "headless",
36
+ "commands",
37
+ "undo",
38
+ "redo",
39
+ "selection"
36
40
  ],
37
41
  "repository": {
38
42
  "type": "git",