superdoc-macros 0.7.0 → 0.7.1

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
@@ -87,7 +87,9 @@ If you must waive isolation (e.g. a CSP that blocks `srcdoc`), switch to the dir
87
87
  ## 2. Macro recorder
88
88
 
89
89
  ```ts
90
- kit.startRecording();
90
+ if (!kit.startRecording()) {
91
+ throw new Error('A macro is running or an unsaved recording is pending');
92
+ }
91
93
  // the user works normally: typing, formatting, lists...
92
94
  const recording = kit.stopRecording('Standard intro', 'Ctrl+Alt+1');
93
95
 
@@ -97,6 +99,8 @@ await kit.replayRecording(recording.id);
97
99
 
98
100
  The recorder captures **commands and typing**, not caret positions — exactly like Word's recorder: replay applies wherever the caret stands. Consecutive keystrokes coalesce into one step, `undo`/`redo` are not recorded (configurable via `RecorderOptions`), and recordings persist as clean JSON that can be exported and shared. `updateRecording({id, name?, shortcut?})` renames a recording or edits its shortcut.
99
101
 
102
+ Finalization is loss-aware. A command payload that cannot be stored faithfully is reported instead of silently omitted; `stopRecording()` keeps the stopped capture pending until it is saved or explicitly cancelled. A mixed capture requires `{ allowIncomplete: true }` after the host has obtained user consent, while a capture containing no replayable step is rejected as `recording-uncapturable`. Storage, capacity, and validation failures are retryable: fix the problem and call `stopRecording()` again. Starting a new recording while one is pending is rejected, preventing accidental loss.
103
+
100
104
  ## 3. Snippets and auto-text
101
105
 
102
106
  ```ts
package/dist/manager.d.ts CHANGED
@@ -108,13 +108,19 @@ export declare class MacroKit {
108
108
  runSource(source: string): Promise<MacroRunResult>;
109
109
  get isRecording(): boolean;
110
110
  get recordedStepCount(): number;
111
- startRecording(): void;
111
+ /**
112
+ * Starts a fresh recording. Returns false instead of overwriting an active
113
+ * or stopped-but-unsaved capture, or while another macro is running.
114
+ */
115
+ startRecording(): boolean;
112
116
  /**
113
117
  * Stops and saves. `null` when no step was recorded — there is nothing to
114
118
  * save.
115
119
  *
116
120
  * Throws — with the stopped recording **retained for retry** (call again;
117
121
  * `cancelRecording` is the explicit way to drop it) — when:
122
+ * - `recording-uncapturable`: every action was uncapturable, so even an
123
+ * explicitly incomplete save would create an empty macro.
118
124
  * - `recording-incomplete`: some actions could not be captured (e.g. an
119
125
  * inserted image, whose payload is the whole file). Saving that as-is
120
126
  * would present a macro that replays less than what the user did, so it
package/dist/manager.js CHANGED
@@ -223,10 +223,15 @@ export class MacroKit {
223
223
  get recordedStepCount() {
224
224
  return this.recorder.stepCount;
225
225
  }
226
+ /**
227
+ * Starts a fresh recording. Returns false instead of overwriting an active
228
+ * or stopped-but-unsaved capture, or while another macro is running.
229
+ */
226
230
  startRecording() {
227
- if (this.running)
228
- return;
231
+ if (this.running || this.recorder.recording || this.recorder.hasPending)
232
+ return false;
229
233
  this.recorder.start();
234
+ return this.recorder.recording;
230
235
  }
231
236
  /**
232
237
  * Stops and saves. `null` when no step was recorded — there is nothing to
@@ -234,6 +239,8 @@ export class MacroKit {
234
239
  *
235
240
  * Throws — with the stopped recording **retained for retry** (call again;
236
241
  * `cancelRecording` is the explicit way to drop it) — when:
242
+ * - `recording-uncapturable`: every action was uncapturable, so even an
243
+ * explicitly incomplete save would create an empty macro.
237
244
  * - `recording-incomplete`: some actions could not be captured (e.g. an
238
245
  * inserted image, whose payload is the whole file). Saving that as-is
239
246
  * would present a macro that replays less than what the user did, so it
@@ -243,19 +250,27 @@ export class MacroKit {
243
250
  * - a persistence failure (quota, oversized state).
244
251
  */
245
252
  stopRecording(name, shortcut, options = {}) {
246
- this.requireValidShortcut(shortcut);
247
- this.requireItemLimits({ name, shortcut });
248
- this.requireRoom(this.state.recordings);
253
+ // Stop first. Every failure below must leave the recorder inactive and
254
+ // its snapshot retryable. Checking capacity before stop() used to leave a
255
+ // manual recording active behind a stopped UI, and could lose an
256
+ // auto-stopped recording on the next start.
249
257
  const pending = this.recorder.stop();
250
258
  const { steps, truncated } = splitOversizedSteps(pending.steps);
251
259
  if (truncated)
252
260
  throw new MacroError(macroMessages().recordingTooLarge, 'recording-too-large');
261
+ const commandIds = [...new Set(pending.warnings.map((warning) => warning.commandId))].join(', ');
262
+ if (pending.warnings.length > 0 && steps.length === 0) {
263
+ const messages = macroMessages();
264
+ throw new MacroError(messages.recordingUncapturable?.(commandIds) ?? messages.recordingIncomplete(commandIds), 'recording-uncapturable');
265
+ }
253
266
  if (steps.length === 0) {
254
267
  this.recorder.discard();
255
268
  return null;
256
269
  }
270
+ this.requireValidShortcut(shortcut);
271
+ this.requireItemLimits({ name, shortcut });
272
+ this.requireRoom(this.state.recordings);
257
273
  if (pending.warnings.length > 0 && !options.allowIncomplete) {
258
- const commandIds = [...new Set(pending.warnings.map((warning) => warning.commandId))].join(', ');
259
274
  throw new MacroError(macroMessages().recordingIncomplete(commandIds), 'recording-incomplete');
260
275
  }
261
276
  const recording = {
@@ -43,6 +43,12 @@ export interface MacroMessages {
43
43
  saveFailed: string;
44
44
  recordingTooLarge: string;
45
45
  recordingIncomplete: (commandIds: string) => string;
46
+ /**
47
+ * No usable step was captured; saving with allowIncomplete would create an
48
+ * empty macro. Optional for source compatibility with existing full locale
49
+ * objects compiled against 0.7.0.
50
+ */
51
+ recordingUncapturable?: (commandIds: string) => string;
46
52
  shortcutInvalid: string;
47
53
  shortcutNeedsModifier: string;
48
54
  shortcutReserved: string;
package/dist/messages.js CHANGED
@@ -43,6 +43,7 @@ export const ENGLISH_MESSAGES = {
43
43
  saveFailed: 'Saving failed — the change was not applied',
44
44
  recordingTooLarge: 'The recording is too large to save',
45
45
  recordingIncomplete: (commandIds) => `The recording is missing actions that cannot be recorded (${commandIds})`,
46
+ recordingUncapturable: (commandIds) => `The recording contains only actions that cannot be recorded (${commandIds})`,
46
47
  shortcutInvalid: 'Invalid shortcut — use a form like Ctrl+Alt+M',
47
48
  shortcutNeedsModifier: 'A shortcut must include Ctrl, Alt or Meta',
48
49
  shortcutReserved: 'This shortcut is reserved by the editor',
@@ -83,6 +84,7 @@ export const HEBREW_MESSAGES = {
83
84
  saveFailed: 'השמירה נכשלה — השינוי לא הוחל',
84
85
  recordingTooLarge: 'ההקלטה גדולה מכדי להישמר',
85
86
  recordingIncomplete: (commandIds) => `בהקלטה חסרות פעולות שאינן ניתנות להקלטה (${commandIds})`,
87
+ recordingUncapturable: (commandIds) => `ההקלטה מכילה רק פעולות שאינן ניתנות להקלטה (${commandIds})`,
86
88
  shortcutInvalid: 'קיצור לא תקין — הצורה הנדרשת היא למשל Ctrl+Alt+M',
87
89
  shortcutNeedsModifier: 'קיצור חייב לכלול Ctrl,‏ Alt או Meta',
88
90
  shortcutReserved: 'הקיצור הזה שמור לעורך',
@@ -80,6 +80,13 @@ export declare class MacroRecorder {
80
80
  applyAutoTextExpansion(consumed: number, replacement: string): void;
81
81
  private teardown;
82
82
  private push;
83
+ private addWarning;
84
+ /**
85
+ * Warnings are part of the stopped snapshot just like steps. Counting both
86
+ * prevents a warning-only recording from growing without bound while never
87
+ * reaching the ordinary step cap.
88
+ */
89
+ private maybeAutoStop;
83
90
  /**
84
91
  * Records a programmatic insertion the host will not report as typing —
85
92
  * e.g. a snippet expanded from a button or shortcut, which writes through
@@ -146,12 +146,27 @@ export class MacroRecorder {
146
146
  }
147
147
  push(step) {
148
148
  this.steps.push(step);
149
- if (this.steps.length >= this.maxSteps) {
150
- // The cap stops the *listening*, not the data: the steps stay for
151
- // stop() to collect, and the owner is told the recording ended.
152
- this.teardown();
153
- this.onAutoStop?.();
154
- }
149
+ this.maybeAutoStop();
150
+ }
151
+ addWarning(warning) {
152
+ this.warnings.push(warning);
153
+ this.maybeAutoStop();
154
+ }
155
+ /**
156
+ * Warnings are part of the stopped snapshot just like steps. Counting both
157
+ * prevents a warning-only recording from growing without bound while never
158
+ * reaching the ordinary step cap.
159
+ */
160
+ maybeAutoStop() {
161
+ if (!this.active)
162
+ return;
163
+ if (this.steps.length + this.warnings.length < this.maxSteps)
164
+ return;
165
+ this.teardown();
166
+ this.pending = { steps: this.steps, warnings: this.warnings };
167
+ this.steps = [];
168
+ this.warnings = [];
169
+ this.onAutoStop?.();
155
170
  }
156
171
  /**
157
172
  * Records a programmatic insertion the host will not report as typing —
@@ -184,15 +199,15 @@ export class MacroRecorder {
184
199
  json = JSON.stringify(payload);
185
200
  }
186
201
  catch {
187
- this.warnings.push({ commandId: id, reason: 'payload-not-serializable' });
202
+ this.addWarning({ commandId: id, reason: 'payload-not-serializable' });
188
203
  return;
189
204
  }
190
205
  if (typeof json !== 'string') {
191
- this.warnings.push({ commandId: id, reason: 'payload-not-serializable' });
206
+ this.addWarning({ commandId: id, reason: 'payload-not-serializable' });
192
207
  return;
193
208
  }
194
209
  if (json.length > IMPORT_LIMITS.maxPayloadLength) {
195
- this.warnings.push({ commandId: id, reason: 'payload-too-large' });
210
+ this.addWarning({ commandId: id, reason: 'payload-too-large' });
196
211
  return;
197
212
  }
198
213
  this.push({ type: 'command', id, payload: JSON.parse(json) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superdoc-macros",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Macro toolkit for SuperDoc-based editors: sandboxed scripted macros, a Word-style macro recorder, and snippets with auto-text expansion.",
5
5
  "license": "MIT",
6
6
  "type": "module",