superdoc-macros 0.2.0 → 0.4.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/dist/manager.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * `MacroKit` — הפאסדה שמארח מתקין פעם אחת ומקבל את שלוש היכולות מחווטות:
3
- * סקריפטים (עם ארגז חול), מקליט, וקטעי טקסט עם השלמה אוטומטית — פלוס שמירה,
4
- * ייבוא/ייצוא וקיצורי מקלדת.
2
+ * `MacroKit` — the facade a host installs once to get all three capabilities
3
+ * wired together: scripts (sandboxed), the recorder, and snippets with
4
+ * auto-text plus persistence, import/export and keyboard shortcuts.
5
5
  *
6
- * כלל בטיחות אחד נאכף כאן: אין ריצה בזמן הקלטה ואין שתי ריצות במקביל.
7
- * ניגון או סקריפט שרצים תוך כדי הקלטה היו מוקלטים בעצמם ומכפילים את עצמם
8
- * בניגון הבא.
6
+ * One safety rule is enforced here: no running while recording, and no two
7
+ * runs at once. A replay or script running during a recording would be
8
+ * recorded itself and duplicate itself on the next replay.
9
9
  */
10
10
  import { createMacroApi } from './scripting/macro-api.js';
11
11
  import { createEvalRunner } from './scripting/eval-runner.js';
@@ -13,8 +13,10 @@ import { createIframeRunner } from './scripting/iframe-runner.js';
13
13
  import { MacroRecorder, replayMacro } from './recorder/recorder.js';
14
14
  import { AutoText } from './snippets/autotext.js';
15
15
  import { expandSnippet } from './snippets/snippets.js';
16
- import { bindShortcuts } from './shortcuts.js';
16
+ import { bindShortcuts, hasBindingModifier, parseShortcut, shortcutSignatures, } from './shortcuts.js';
17
+ import { MacroError } from './scripting/macro-api.js';
17
18
  import { createLocalStorage, emptyState, parsePersistedState } from './storage.js';
19
+ import { macroMessages } from './messages.js';
18
20
  let idCounter = 0;
19
21
  function newId() {
20
22
  try {
@@ -34,6 +36,7 @@ export class MacroKit {
34
36
  state;
35
37
  recorder;
36
38
  autoText;
39
+ reservedSignatures;
37
40
  running = false;
38
41
  constructor(options) {
39
42
  this.host = options.host;
@@ -46,12 +49,71 @@ export class MacroKit {
46
49
  this.state = this.storage.load() ?? emptyState();
47
50
  this.recorder = new MacroRecorder(this.host);
48
51
  this.autoText = new AutoText(this.host, () => this.state.snippets, options.autoText);
52
+ const reserved = new Set();
53
+ for (const shortcut of options.reservedShortcuts ?? []) {
54
+ const parsed = parseShortcut(shortcut);
55
+ if (parsed)
56
+ for (const signature of shortcutSignatures(parsed))
57
+ reserved.add(signature);
58
+ }
59
+ this.reservedSignatures = reserved;
60
+ }
61
+ /* ---------- Shortcut validation ---------- */
62
+ /**
63
+ * Whether a shortcut is acceptable for a saved binding: parseable, carries
64
+ * a real modifier, not reserved by the host, and not already used by
65
+ * another saved item (`excludeId` skips the item being edited). Empty or
66
+ * undefined means "no shortcut" and is fine. The save paths enforce this;
67
+ * UIs call it directly to show the message before saving.
68
+ */
69
+ validateShortcut(shortcut, excludeId) {
70
+ const trimmed = shortcut?.trim();
71
+ if (!trimmed)
72
+ return { ok: true };
73
+ const parsed = parseShortcut(trimmed);
74
+ if (!parsed)
75
+ return { ok: false, message: macroMessages().shortcutInvalid };
76
+ if (!hasBindingModifier(parsed))
77
+ return { ok: false, message: macroMessages().shortcutNeedsModifier };
78
+ const signatures = shortcutSignatures(parsed);
79
+ if (signatures.some((signature) => this.reservedSignatures.has(signature))) {
80
+ return { ok: false, message: macroMessages().shortcutReserved };
81
+ }
82
+ const owner = this.findShortcutOwner(signatures, excludeId);
83
+ if (owner)
84
+ return { ok: false, message: macroMessages().shortcutTaken(owner) };
85
+ return { ok: true };
49
86
  }
50
- /* ---------- סקריפטים ---------- */
87
+ findShortcutOwner(signatures, excludeId) {
88
+ const items = [
89
+ ...this.state.scripts,
90
+ ...this.state.recordings,
91
+ ...this.state.snippets,
92
+ ];
93
+ for (const item of items) {
94
+ if (!item.shortcut || item.id === excludeId)
95
+ continue;
96
+ const parsed = parseShortcut(item.shortcut);
97
+ if (!parsed)
98
+ continue;
99
+ const existing = shortcutSignatures(parsed);
100
+ if (existing.some((signature) => signatures.includes(signature)))
101
+ return item.name;
102
+ }
103
+ return null;
104
+ }
105
+ /** Throws a MacroError when the shortcut is unacceptable. The save paths call this. */
106
+ requireValidShortcut(shortcut, excludeId) {
107
+ const validation = this.validateShortcut(shortcut, excludeId);
108
+ if (!validation.ok)
109
+ throw new MacroError(validation.message, 'invalid-shortcut');
110
+ }
111
+ /* ---------- Scripts ---------- */
51
112
  listScripts() {
52
113
  return this.state.scripts;
53
114
  }
54
115
  saveScript(input) {
116
+ this.requireValidShortcut(input.shortcut, input.id);
55
117
  const script = {
56
118
  id: input.id ?? newId(),
57
119
  name: input.name,
@@ -69,10 +131,10 @@ export class MacroKit {
69
131
  async runScript(id) {
70
132
  const script = this.state.scripts.find((entry) => entry.id === id);
71
133
  if (!script)
72
- return { ok: false, reason: 'error', message: 'המאקרו לא נמצא' };
134
+ return { ok: false, reason: 'error', message: macroMessages().scriptNotFound };
73
135
  return this.runSource(script.source);
74
136
  }
75
- /** מריצה סקריפט שלא נשמרלמשל מתוך עורך המאקרו לפני שמירה. */
137
+ /** Runs an unsaved scripte.g. from the macro editor before saving. */
76
138
  async runSource(source) {
77
139
  const guard = this.guardRun();
78
140
  if (guard)
@@ -86,7 +148,7 @@ export class MacroKit {
86
148
  this.running = false;
87
149
  }
88
150
  }
89
- /* ---------- מקליט ---------- */
151
+ /* ---------- Recorder ---------- */
90
152
  get isRecording() {
91
153
  return this.recorder.recording;
92
154
  }
@@ -98,8 +160,9 @@ export class MacroKit {
98
160
  return;
99
161
  this.recorder.start();
100
162
  }
101
- /** עוצרת ושומרת. `null` כשלא הוקלט אף צעדאין מה לשמור. */
163
+ /** Stops and saves. `null` when no step was recorded there is nothing to save. */
102
164
  stopRecording(name, shortcut) {
165
+ this.requireValidShortcut(shortcut);
103
166
  const steps = this.recorder.stop();
104
167
  if (steps.length === 0)
105
168
  return null;
@@ -125,11 +188,13 @@ export class MacroKit {
125
188
  this.state.recordings = this.state.recordings.filter((recording) => recording.id !== id);
126
189
  this.persist();
127
190
  }
128
- /** עדכון שם או קיצור של הקלטה קיימת. `null` כשההקלטה לא נמצאה. */
191
+ /** Renames a recording or edits its shortcut. `null` when the recording was not found. */
129
192
  updateRecording(input) {
130
193
  const recording = this.state.recordings.find((entry) => entry.id === input.id);
131
194
  if (!recording)
132
195
  return null;
196
+ if (input.shortcut !== undefined)
197
+ this.requireValidShortcut(input.shortcut, input.id);
133
198
  if (input.name !== undefined)
134
199
  recording.name = input.name;
135
200
  if (input.shortcut !== undefined) {
@@ -143,8 +208,13 @@ export class MacroKit {
143
208
  }
144
209
  async replayRecording(id, options) {
145
210
  const recording = this.state.recordings.find((entry) => entry.id === id);
146
- if (!recording)
147
- return { ok: false, completed: 0, failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: 'ההקלטה לא נמצאה' }] };
211
+ if (!recording) {
212
+ return {
213
+ ok: false,
214
+ completed: 0,
215
+ failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: macroMessages().recordingNotFound }],
216
+ };
217
+ }
148
218
  const guard = this.guardRun();
149
219
  if (guard) {
150
220
  return { ok: false, completed: 0, failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: guard.message }] };
@@ -157,11 +227,12 @@ export class MacroKit {
157
227
  this.running = false;
158
228
  }
159
229
  }
160
- /* ---------- קטעי טקסט ---------- */
230
+ /* ---------- Snippets ---------- */
161
231
  listSnippets() {
162
232
  return this.state.snippets;
163
233
  }
164
234
  saveSnippet(input) {
235
+ this.requireValidShortcut(input.shortcut, input.id);
165
236
  const snippet = {
166
237
  id: input.id ?? newId(),
167
238
  name: input.name,
@@ -180,22 +251,23 @@ export class MacroKit {
180
251
  async expandSnippet(id, options) {
181
252
  const snippet = this.state.snippets.find((entry) => entry.id === id);
182
253
  if (!snippet)
183
- return { ok: false, message: 'הקטע לא נמצא' };
254
+ return { ok: false, message: macroMessages().snippetNotFound };
184
255
  const outcome = await expandSnippet(this.host, snippet, options);
185
256
  return outcome.ok ? { ok: true } : { ok: false, message: outcome.message };
186
257
  }
187
- /** מפעילה השלמה אוטומטית (trigger + רווח). מחזירה פונקציית כיבוי. */
258
+ /** Enables auto-text (trigger + space). Returns a disable function. */
188
259
  enableAutoText() {
189
260
  return this.autoText.attach();
190
261
  }
191
262
  disableAutoText() {
192
263
  this.autoText.detach();
193
264
  }
194
- /* ---------- קיצורי מקלדת ---------- */
265
+ /* ---------- Keyboard shortcuts ---------- */
195
266
  /**
196
- * קושרת את הקיצורים של כל מה ששמור (סקריפטים, הקלטות, קטעים) ליעד — בדרך
197
- * כלל ה-container של העורך או `window`. הרשימה חיה: שמירה חדשה נקלטת בלי
198
- * לקשור מחדש. מחזירה פונקציית ניתוק.
267
+ * Binds the shortcuts of everything saved (scripts, recordings, snippets)
268
+ * to a target usually the editor container or `window`. The list is
269
+ * live: a new save is picked up without rebinding. Returns a dispose
270
+ * function.
199
271
  */
200
272
  attachShortcuts(target) {
201
273
  return bindShortcuts(target, () => this.currentBindings());
@@ -216,18 +288,19 @@ export class MacroKit {
216
288
  }
217
289
  return bindings;
218
290
  }
219
- /* ---------- ייבוא/ייצוא ---------- */
291
+ /* ---------- Import/export ---------- */
220
292
  exportState() {
221
293
  return JSON.stringify(this.state, null, 2);
222
294
  }
223
295
  /**
224
- * ייבוא מ-JSON שיוצא ב-`exportState`. במיזוג (`merge: true`) פריט מיובא עם
225
- * `id` קיים מחליף את הקיים; בלי מיזוג המצב כולו מוחלף.
296
+ * Imports JSON produced by `exportState`. With `merge: true` an imported
297
+ * item with an existing `id` replaces it; without merge the whole state is
298
+ * replaced.
226
299
  */
227
300
  importState(json, options = {}) {
228
301
  const imported = parsePersistedState(json);
229
302
  if (!imported)
230
- return { ok: false, message: 'הקובץ אינו ייצוא מאקרו תקין' };
303
+ return { ok: false, message: macroMessages().invalidImport };
231
304
  if (options.merge) {
232
305
  for (const script of imported.scripts)
233
306
  this.upsert(this.state.scripts, script);
@@ -242,13 +315,13 @@ export class MacroKit {
242
315
  this.persist();
243
316
  return { ok: true };
244
317
  }
245
- /* ---------- פנימי ---------- */
318
+ /* ---------- Internal ---------- */
246
319
  guardRun() {
247
320
  if (this.recorder.recording) {
248
- return { ok: false, reason: 'error', message: 'אי אפשר להריץ מאקרו בזמן הקלטה' };
321
+ return { ok: false, reason: 'error', message: macroMessages().cannotRunWhileRecording };
249
322
  }
250
323
  if (this.running) {
251
- return { ok: false, reason: 'error', message: 'מאקרו אחר עדיין רץ' };
324
+ return { ok: false, reason: 'error', message: macroMessages().anotherMacroRunning };
252
325
  }
253
326
  return null;
254
327
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * All user-facing runtime strings, in one place.
3
+ *
4
+ * The toolkit reports failures to end users (status bars, dialogs), so the
5
+ * strings are part of the product, not debug output. Defaults are English;
6
+ * a host with a localized UI swaps them once at startup:
7
+ *
8
+ * ```ts
9
+ * import { setMacroMessages, HEBREW_MESSAGES } from 'superdoc-macros';
10
+ * setMacroMessages(HEBREW_MESSAGES);
11
+ * ```
12
+ *
13
+ * A module-level locale rather than per-instance options, deliberately: the
14
+ * strings surface from many layers (API, runners, host adapter, manager),
15
+ * and threading an options object through all of them would make every
16
+ * factory signature about localization. One UI language per page is the
17
+ * reality these editors live in.
18
+ */
19
+ export interface MacroMessages {
20
+ unknownMethod: (method: string) => string;
21
+ mustBeString: (name: string) => string;
22
+ commandFailed: (id: string) => string;
23
+ insertTextFailed: string;
24
+ insertParagraphFailed: string;
25
+ deleteFailed: string;
26
+ replaceFailed: string;
27
+ syntaxError: (detail: string) => string;
28
+ timedOut: (seconds: number) => string;
29
+ callLimitExceeded: (limit: number) => string;
30
+ macroStopped: string;
31
+ scriptNotFound: string;
32
+ recordingNotFound: string;
33
+ snippetNotFound: string;
34
+ cannotRunWhileRecording: string;
35
+ anotherMacroRunning: string;
36
+ invalidImport: string;
37
+ shortcutInvalid: string;
38
+ shortcutNeedsModifier: string;
39
+ shortcutReserved: string;
40
+ shortcutTaken: (ownerName: string) => string;
41
+ noDocument: string;
42
+ selectionUnavailable: string;
43
+ unknownCommand: (id: string) => string;
44
+ actionFailed: string;
45
+ deletionUnavailable: string;
46
+ searchUnavailable: string;
47
+ searchUnavailableInDocument: string;
48
+ }
49
+ export declare const ENGLISH_MESSAGES: MacroMessages;
50
+ /** Hebrew locale — the strings the toolkit shipped with originally. */
51
+ export declare const HEBREW_MESSAGES: MacroMessages;
52
+ /** Replaces some or all runtime strings. Call once, before creating hosts/kits. */
53
+ export declare function setMacroMessages(messages: Partial<MacroMessages>): void;
54
+ /** The active locale. Internal — modules read strings through this. */
55
+ export declare function macroMessages(): MacroMessages;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * All user-facing runtime strings, in one place.
3
+ *
4
+ * The toolkit reports failures to end users (status bars, dialogs), so the
5
+ * strings are part of the product, not debug output. Defaults are English;
6
+ * a host with a localized UI swaps them once at startup:
7
+ *
8
+ * ```ts
9
+ * import { setMacroMessages, HEBREW_MESSAGES } from 'superdoc-macros';
10
+ * setMacroMessages(HEBREW_MESSAGES);
11
+ * ```
12
+ *
13
+ * A module-level locale rather than per-instance options, deliberately: the
14
+ * strings surface from many layers (API, runners, host adapter, manager),
15
+ * and threading an options object through all of them would make every
16
+ * factory signature about localization. One UI language per page is the
17
+ * reality these editors live in.
18
+ */
19
+ export const ENGLISH_MESSAGES = {
20
+ unknownMethod: (method) => `Unknown method: ${method}`,
21
+ mustBeString: (name) => `${name} must be a string`,
22
+ commandFailed: (id) => `Command ${id} failed`,
23
+ insertTextFailed: 'Failed to insert text',
24
+ insertParagraphFailed: 'Failed to insert paragraph',
25
+ deleteFailed: 'Delete failed',
26
+ replaceFailed: 'Replace failed',
27
+ syntaxError: (detail) => `Macro syntax error: ${detail}`,
28
+ timedOut: (seconds) => `The macro did not finish within ${seconds} seconds and was stopped`,
29
+ callLimitExceeded: (limit) => `The macro exceeded the API call limit (${limit}) and was stopped`,
30
+ macroStopped: 'The macro was stopped — the call was not executed',
31
+ scriptNotFound: 'Macro not found',
32
+ recordingNotFound: 'Recording not found',
33
+ snippetNotFound: 'Snippet not found',
34
+ cannotRunWhileRecording: 'Cannot run a macro while recording',
35
+ anotherMacroRunning: 'Another macro is still running',
36
+ invalidImport: 'The file is not a valid macro export',
37
+ shortcutInvalid: 'Invalid shortcut — use a form like Ctrl+Alt+M',
38
+ shortcutNeedsModifier: 'A shortcut must include Ctrl, Alt or Meta',
39
+ shortcutReserved: 'This shortcut is reserved by the editor',
40
+ shortcutTaken: (ownerName) => `This shortcut is already used by "${ownerName}"`,
41
+ noDocument: 'No document is open',
42
+ selectionUnavailable: 'The caret position could not be read — nothing was inserted',
43
+ unknownCommand: (id) => `The engine does not recognize the command ${id}`,
44
+ actionFailed: 'The operation failed',
45
+ deletionUnavailable: 'Deletion is not available in this document',
46
+ searchUnavailable: 'Search is not available',
47
+ searchUnavailableInDocument: 'Search is not available in this document',
48
+ };
49
+ /** Hebrew locale — the strings the toolkit shipped with originally. */
50
+ export const HEBREW_MESSAGES = {
51
+ unknownMethod: (method) => `מתודה לא מוכרת: ${method}`,
52
+ mustBeString: (name) => `${name} חייב להיות מחרוזת`,
53
+ commandFailed: (id) => `הפקודה ${id} נכשלה`,
54
+ insertTextFailed: 'הכנסת הטקסט נכשלה',
55
+ insertParagraphFailed: 'הכנסת הפסקה נכשלה',
56
+ deleteFailed: 'המחיקה נכשלה',
57
+ replaceFailed: 'ההחלפה נכשלה',
58
+ syntaxError: (detail) => `שגיאת תחביר במאקרו: ${detail}`,
59
+ timedOut: (seconds) => `המאקרו לא הסתיים תוך ${seconds} שניות ונעצר`,
60
+ callLimitExceeded: (limit) => `המאקרו חצה את תקרת הקריאות (${limit}) ונעצר`,
61
+ macroStopped: 'המאקרו נעצר — הקריאה לא בוצעה',
62
+ scriptNotFound: 'המאקרו לא נמצא',
63
+ recordingNotFound: 'ההקלטה לא נמצאה',
64
+ snippetNotFound: 'הקטע לא נמצא',
65
+ cannotRunWhileRecording: 'אי אפשר להריץ מאקרו בזמן הקלטה',
66
+ anotherMacroRunning: 'מאקרו אחר עדיין רץ',
67
+ invalidImport: 'הקובץ אינו ייצוא מאקרו תקין',
68
+ shortcutInvalid: 'קיצור לא תקין — הצורה הנדרשת היא למשל Ctrl+Alt+M',
69
+ shortcutNeedsModifier: 'קיצור חייב לכלול Ctrl,‏ Alt או Meta',
70
+ shortcutReserved: 'הקיצור הזה שמור לעורך',
71
+ shortcutTaken: (ownerName) => `הקיצור כבר בשימוש של "${ownerName}"`,
72
+ noDocument: 'אין מסמך פתוח',
73
+ selectionUnavailable: 'קריאת מיקום הסמן נכשלה — לא הוכנס דבר',
74
+ unknownCommand: (id) => `הפקודה ${id} אינה מוכרת למנוע`,
75
+ actionFailed: 'הפעולה נכשלה',
76
+ deletionUnavailable: 'מחיקה אינה זמינה במסמך הזה',
77
+ searchUnavailable: 'החיפוש אינו זמין',
78
+ searchUnavailableInDocument: 'החיפוש אינו זמין במסמך הזה',
79
+ };
80
+ let current = { ...ENGLISH_MESSAGES };
81
+ /** Replaces some or all runtime strings. Call once, before creating hosts/kits. */
82
+ export function setMacroMessages(messages) {
83
+ current = { ...current, ...messages };
84
+ }
85
+ /** The active locale. Internal — modules read strings through this. */
86
+ export function macroMessages() {
87
+ return current;
88
+ }
@@ -1,20 +1,22 @@
1
1
  /**
2
- * מקליט מאקרו בסגנון Word: מקליט **פקודות** והקלדה, לא מיקומי סמן.
2
+ * A Word-style macro recorder: records **commands** and typing, not caret
3
+ * positions.
3
4
  *
4
- * הבחירה הזאת מכוונת. הקלטת צעדי ProseMirror גולמיים (עם מיקומים אבסולוטיים)
5
- * נשברת ברגע שהמסמך שונה ממה שהיה בזמן ההקלטה; הקלטת פקודות ("bold",
6
- * "bullet-list", הקלדת "בס\"ד") מתנהגת כמו המקליט של Word — הפעולות חלות
7
- * במקום שבו הסמן נמצא בזמן הניגון. זה גם מה שהופך הקלטה לניתנת לשמירה
8
- * ולשיתוף: הצעדים JSON בלבד.
5
+ * That choice is deliberate. Recording raw ProseMirror steps (with absolute
6
+ * positions) breaks the moment the document differs from what it was at
7
+ * recording time; recording commands ("bold", "bullet-list", typing a
8
+ * greeting) behaves like Word's recorder the actions apply wherever the
9
+ * caret is at replay time. It is also what makes a recording saveable and
10
+ * shareable: the steps are plain JSON.
9
11
  *
10
- * מה לא נקלט: תנועת סמן ובחירה בעכבר. כמו ב-Word, מאקרו מוקלט פועל מהמקום
11
- * שבו הסמן עומד כשמריצים אותו.
12
+ * What is not recorded: caret movement and mouse selection. As in Word, a
13
+ * recorded macro acts from wherever the caret stands when it runs.
12
14
  */
13
15
  import type { MacroHost, MacroStep } from '../types.js';
14
16
  export interface RecorderOptions {
15
- /** סינון פקודות. ברירת המחדל מקליטה הכול חוץ מ-undo/redo. */
17
+ /** Command filter. The default records everything except undo/redo. */
16
18
  shouldRecordCommand?: (id: string) => boolean;
17
- /** תקרת צעדים להקלטה אחת, נגד הקלטה שנשכחה פתוחה. */
19
+ /** Step cap per recording, against a recording left running by mistake. */
18
20
  maxSteps?: number;
19
21
  }
20
22
  export declare class MacroRecorder {
@@ -28,9 +30,9 @@ export declare class MacroRecorder {
28
30
  get recording(): boolean;
29
31
  get stepCount(): number;
30
32
  start(): void;
31
- /** עוצרת ומחזירה את הצעדים. ריקה כשלא הוקלט דבר. */
33
+ /** Stops and returns the steps. Empty when nothing was recorded. */
32
34
  stop(): MacroStep[];
33
- /** עוצרת וזורקת את מה שהוקלט. */
35
+ /** Stops and discards whatever was recorded. */
34
36
  cancel(): void;
35
37
  private teardown;
36
38
  private push;
@@ -44,14 +46,14 @@ export interface ReplayFailure {
44
46
  }
45
47
  export interface ReplayResult {
46
48
  ok: boolean;
47
- /** כמה צעדים הושלמו בהצלחה. */
49
+ /** How many steps completed successfully. */
48
50
  completed: number;
49
51
  failures: ReplayFailure[];
50
52
  }
51
53
  export interface ReplayOptions {
52
- /** עצירה בכשל הראשון. ברירת מחדל: true — מאקרו שנכשל באמצע לא ממשיך לרוץ עיוור. */
54
+ /** Stop at the first failure. Default: true — a macro that failed midway must not keep running blind. */
53
55
  stopOnError?: boolean;
54
- /** נקראת לפני כל צעד; מאפשרת מד התקדמות. */
56
+ /** Called before each step; enables a progress indicator. */
55
57
  onStep?: (index: number, step: MacroStep) => void;
56
58
  }
57
59
  export declare function replayMacro(host: MacroHost, steps: readonly MacroStep[], options?: ReplayOptions): Promise<ReplayResult>;
@@ -1,5 +1,5 @@
1
1
  const DEFAULT_MAX_STEPS = 5_000;
2
- /** undo/redo בזמן הקלטה מתקנים את ההקלטה עצמהניגון שלהם היה משחזר גם את הטעות. */
2
+ /** Undo/redo during recording fix the recording itselfreplaying them would replay the mistake too. */
3
3
  function defaultShouldRecord(id) {
4
4
  return id !== 'undo' && id !== 'redo';
5
5
  }
@@ -31,7 +31,7 @@ export class MacroRecorder {
31
31
  this.host.onTextInput((event) => this.recordTextInput(event)),
32
32
  ];
33
33
  }
34
- /** עוצרת ומחזירה את הצעדים. ריקה כשלא הוקלט דבר. */
34
+ /** Stops and returns the steps. Empty when nothing was recorded. */
35
35
  stop() {
36
36
  if (!this.active)
37
37
  return [];
@@ -40,7 +40,7 @@ export class MacroRecorder {
40
40
  this.steps = [];
41
41
  return recorded;
42
42
  }
43
- /** עוצרת וזורקת את מה שהוקלט. */
43
+ /** Stops and discards whatever was recorded. */
44
44
  cancel() {
45
45
  if (!this.active)
46
46
  return;
@@ -68,7 +68,7 @@ export class MacroRecorder {
68
68
  const last = this.steps[this.steps.length - 1];
69
69
  switch (event.kind) {
70
70
  case 'insert-text': {
71
- // הקשות רצופות מתלכדות לצעד אחדגם קריא יותר וגם ניגון מהיר יותר.
71
+ // Consecutive keystrokes coalesce into one step more readable, faster to replay.
72
72
  if (last?.type === 'insert-text') {
73
73
  last.text += event.text;
74
74
  return;
@@ -109,8 +109,7 @@ async function runStep(host, step) {
109
109
  case 'delete-backward':
110
110
  return host.deleteBackward(step.count);
111
111
  case 'delete-forward':
112
- // המנוע אינו חושף מחיקה קדימה נפרדת; מדווח ככשל מפורש ולא מדלג בשקט.
113
- return { ok: false, message: 'מחיקה קדימה אינה נתמכת בניגון', reason: 'unsupported-step' };
112
+ return host.deleteForward(step.count);
114
113
  }
115
114
  }
116
115
  export async function replayMacro(host, steps, options = {}) {
@@ -1,16 +1,18 @@
1
- /**
2
- * מריץ סקריפטים ישיר — `AsyncFunction` באותו הקשר של הדף.
3
- *
4
- * **אינו ארגז חול.** סקריפט שרץ כאן מקבל גישה לכל מה שהדף מכיר. מיועד לשני
5
- * מצבים: בדיקות, וסביבה שבה כל המאקרו נכתבים בידי המשתמש עצמו והוחלט
6
- * במפורש לוותר על בידוד (למשל בגלל CSP שחוסם iframe). ברירת המחדל של
7
- * `MacroKit` היא מריץ ה-iframe.
8
- *
9
- * תקרת הזמן כאן היא race על ההבטחה בלבד: לולאה סינכרונית אינסופית תחסום את
10
- * ה-thread ולא תיעצר. תקרת הקריאות כן נאכפת, דרך ה-bridge.
11
- */
12
1
  import type { MacroBridge } from './macro-api.js';
13
2
  import { type MacroRunner } from './runner.js';
14
- /** עוטפת bridge בתקרת קריאות. חשופה כדי ששני המריצים ישתמשו באותה אכיפה. */
3
+ /** Wraps a bridge with a call cap. Exposed so both runners share the same enforcement. */
15
4
  export declare function limitCalls(bridge: MacroBridge, maxCalls: number): MacroBridge;
5
+ /**
6
+ * Wraps a bridge with a kill switch. After `revoke()` every new call is
7
+ * rejected — so a script that keeps running past its timeout (the eval
8
+ * runner cannot stop it) can no longer touch the document.
9
+ *
10
+ * What this cannot do: abort a host call that already reached the engine.
11
+ * The engine's public surfaces expose no cancellation, so an in-flight
12
+ * operation completes; what is guaranteed is that nothing *new* starts.
13
+ */
14
+ export declare function revocable(bridge: MacroBridge): {
15
+ bridge: MacroBridge;
16
+ revoke: () => void;
17
+ };
16
18
  export declare function createEvalRunner(): MacroRunner;
@@ -1,21 +1,61 @@
1
+ /**
2
+ * Direct script runner — `AsyncFunction` in the page's own context.
3
+ *
4
+ * **Not a sandbox.** A script running here can reach everything the page
5
+ * can. It exists for two situations: tests, and environments where every
6
+ * macro is written by the user themselves and isolation was explicitly
7
+ * waived (e.g. a CSP that blocks iframes). `MacroKit` defaults to the
8
+ * iframe runner.
9
+ *
10
+ * The time cap here is only a race on the promise: an infinite synchronous
11
+ * loop blocks the thread and will not be stopped. The call cap is enforced
12
+ * for real, through the bridge.
13
+ */
14
+ import { macroMessages } from '../messages.js';
1
15
  import { DEFAULT_MAX_API_CALLS, DEFAULT_TIMEOUT_MS, } from './runner.js';
2
16
  const AsyncFunction = Object.getPrototypeOf(async function () {
3
- /* טיפוס בלבד */
17
+ /* type only */
4
18
  }).constructor;
5
- /** עוטפת bridge בתקרת קריאות. חשופה כדי ששני המריצים ישתמשו באותה אכיפה. */
19
+ /** Wraps a bridge with a call cap. Exposed so both runners share the same enforcement. */
6
20
  export function limitCalls(bridge, maxCalls) {
7
21
  return {
8
22
  api: bridge.api,
9
23
  callCount: bridge.callCount,
10
24
  call(method, args) {
11
25
  if (bridge.callCount() >= maxCalls) {
12
- return Promise.reject(new Error(`המאקרו חצה את תקרת הקריאות (${maxCalls}) ונעצר`));
26
+ return Promise.reject(new Error(macroMessages().callLimitExceeded(maxCalls)));
13
27
  }
14
28
  return bridge.call(method, args);
15
29
  },
16
30
  };
17
31
  }
18
- /** proxy של api שמנתב הכול דרך `bridge.call`, כדי שהתקרה תיאכף גם כאן. */
32
+ /**
33
+ * Wraps a bridge with a kill switch. After `revoke()` every new call is
34
+ * rejected — so a script that keeps running past its timeout (the eval
35
+ * runner cannot stop it) can no longer touch the document.
36
+ *
37
+ * What this cannot do: abort a host call that already reached the engine.
38
+ * The engine's public surfaces expose no cancellation, so an in-flight
39
+ * operation completes; what is guaranteed is that nothing *new* starts.
40
+ */
41
+ export function revocable(bridge) {
42
+ let revoked = false;
43
+ return {
44
+ revoke: () => {
45
+ revoked = true;
46
+ },
47
+ bridge: {
48
+ api: bridge.api,
49
+ callCount: bridge.callCount,
50
+ call(method, args) {
51
+ if (revoked)
52
+ return Promise.reject(new Error(macroMessages().macroStopped));
53
+ return bridge.call(method, args);
54
+ },
55
+ },
56
+ };
57
+ }
58
+ /** An api proxy that routes everything through `bridge.call`, so the cap applies here too. */
19
59
  function apiThroughBridge(bridge) {
20
60
  return new Proxy({}, {
21
61
  get(_target, method) {
@@ -29,7 +69,9 @@ export function createEvalRunner() {
29
69
  return {
30
70
  async run(source, bridge, options = {}) {
31
71
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32
- const limited = limitCalls(bridge, options.maxApiCalls ?? DEFAULT_MAX_API_CALLS);
72
+ // The revocable wrapper is what contains a timed-out script: eval cannot
73
+ // stop it from running, but it can no longer reach the document.
74
+ const { bridge: guarded, revoke } = revocable(limitCalls(bridge, options.maxApiCalls ?? DEFAULT_MAX_API_CALLS));
33
75
  let fn;
34
76
  try {
35
77
  fn = new AsyncFunction('api', `"use strict";\n${source}`);
@@ -38,16 +80,16 @@ export function createEvalRunner() {
38
80
  return {
39
81
  ok: false,
40
82
  reason: 'error',
41
- message: `שגיאת תחביר במאקרו: ${error instanceof Error ? error.message : String(error)}`,
83
+ message: macroMessages().syntaxError(error instanceof Error ? error.message : String(error)),
42
84
  };
43
85
  }
44
86
  let timer;
45
87
  const timeout = new Promise((resolve) => {
46
- timer = setTimeout(() => resolve({ ok: false, reason: 'timeout', message: `המאקרו לא הסתיים תוך ${timeoutMs / 1000} שניות ונעצר` }), timeoutMs);
88
+ timer = setTimeout(() => resolve({ ok: false, reason: 'timeout', message: macroMessages().timedOut(timeoutMs / 1000) }), timeoutMs);
47
89
  });
48
90
  const run = (async () => {
49
91
  try {
50
- const value = await fn(apiThroughBridge(limited));
92
+ const value = await fn(apiThroughBridge(guarded));
51
93
  return { ok: true, value };
52
94
  }
53
95
  catch (error) {
@@ -63,6 +105,7 @@ export function createEvalRunner() {
63
105
  }
64
106
  finally {
65
107
  clearTimeout(timer);
108
+ revoke();
66
109
  }
67
110
  },
68
111
  };