superdoc-macros 0.2.0 → 0.3.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';
@@ -15,6 +15,7 @@ import { AutoText } from './snippets/autotext.js';
15
15
  import { expandSnippet } from './snippets/snippets.js';
16
16
  import { bindShortcuts } from './shortcuts.js';
17
17
  import { createLocalStorage, emptyState, parsePersistedState } from './storage.js';
18
+ import { macroMessages } from './messages.js';
18
19
  let idCounter = 0;
19
20
  function newId() {
20
21
  try {
@@ -47,7 +48,7 @@ export class MacroKit {
47
48
  this.recorder = new MacroRecorder(this.host);
48
49
  this.autoText = new AutoText(this.host, () => this.state.snippets, options.autoText);
49
50
  }
50
- /* ---------- סקריפטים ---------- */
51
+ /* ---------- Scripts ---------- */
51
52
  listScripts() {
52
53
  return this.state.scripts;
53
54
  }
@@ -69,10 +70,10 @@ export class MacroKit {
69
70
  async runScript(id) {
70
71
  const script = this.state.scripts.find((entry) => entry.id === id);
71
72
  if (!script)
72
- return { ok: false, reason: 'error', message: 'המאקרו לא נמצא' };
73
+ return { ok: false, reason: 'error', message: macroMessages().scriptNotFound };
73
74
  return this.runSource(script.source);
74
75
  }
75
- /** מריצה סקריפט שלא נשמרלמשל מתוך עורך המאקרו לפני שמירה. */
76
+ /** Runs an unsaved scripte.g. from the macro editor before saving. */
76
77
  async runSource(source) {
77
78
  const guard = this.guardRun();
78
79
  if (guard)
@@ -86,7 +87,7 @@ export class MacroKit {
86
87
  this.running = false;
87
88
  }
88
89
  }
89
- /* ---------- מקליט ---------- */
90
+ /* ---------- Recorder ---------- */
90
91
  get isRecording() {
91
92
  return this.recorder.recording;
92
93
  }
@@ -98,7 +99,7 @@ export class MacroKit {
98
99
  return;
99
100
  this.recorder.start();
100
101
  }
101
- /** עוצרת ושומרת. `null` כשלא הוקלט אף צעדאין מה לשמור. */
102
+ /** Stops and saves. `null` when no step was recorded there is nothing to save. */
102
103
  stopRecording(name, shortcut) {
103
104
  const steps = this.recorder.stop();
104
105
  if (steps.length === 0)
@@ -125,7 +126,7 @@ export class MacroKit {
125
126
  this.state.recordings = this.state.recordings.filter((recording) => recording.id !== id);
126
127
  this.persist();
127
128
  }
128
- /** עדכון שם או קיצור של הקלטה קיימת. `null` כשההקלטה לא נמצאה. */
129
+ /** Renames a recording or edits its shortcut. `null` when the recording was not found. */
129
130
  updateRecording(input) {
130
131
  const recording = this.state.recordings.find((entry) => entry.id === input.id);
131
132
  if (!recording)
@@ -143,8 +144,13 @@ export class MacroKit {
143
144
  }
144
145
  async replayRecording(id, options) {
145
146
  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: 'ההקלטה לא נמצאה' }] };
147
+ if (!recording) {
148
+ return {
149
+ ok: false,
150
+ completed: 0,
151
+ failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: macroMessages().recordingNotFound }],
152
+ };
153
+ }
148
154
  const guard = this.guardRun();
149
155
  if (guard) {
150
156
  return { ok: false, completed: 0, failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: guard.message }] };
@@ -157,7 +163,7 @@ export class MacroKit {
157
163
  this.running = false;
158
164
  }
159
165
  }
160
- /* ---------- קטעי טקסט ---------- */
166
+ /* ---------- Snippets ---------- */
161
167
  listSnippets() {
162
168
  return this.state.snippets;
163
169
  }
@@ -180,22 +186,23 @@ export class MacroKit {
180
186
  async expandSnippet(id, options) {
181
187
  const snippet = this.state.snippets.find((entry) => entry.id === id);
182
188
  if (!snippet)
183
- return { ok: false, message: 'הקטע לא נמצא' };
189
+ return { ok: false, message: macroMessages().snippetNotFound };
184
190
  const outcome = await expandSnippet(this.host, snippet, options);
185
191
  return outcome.ok ? { ok: true } : { ok: false, message: outcome.message };
186
192
  }
187
- /** מפעילה השלמה אוטומטית (trigger + רווח). מחזירה פונקציית כיבוי. */
193
+ /** Enables auto-text (trigger + space). Returns a disable function. */
188
194
  enableAutoText() {
189
195
  return this.autoText.attach();
190
196
  }
191
197
  disableAutoText() {
192
198
  this.autoText.detach();
193
199
  }
194
- /* ---------- קיצורי מקלדת ---------- */
200
+ /* ---------- Keyboard shortcuts ---------- */
195
201
  /**
196
- * קושרת את הקיצורים של כל מה ששמור (סקריפטים, הקלטות, קטעים) ליעד — בדרך
197
- * כלל ה-container של העורך או `window`. הרשימה חיה: שמירה חדשה נקלטת בלי
198
- * לקשור מחדש. מחזירה פונקציית ניתוק.
202
+ * Binds the shortcuts of everything saved (scripts, recordings, snippets)
203
+ * to a target usually the editor container or `window`. The list is
204
+ * live: a new save is picked up without rebinding. Returns a dispose
205
+ * function.
199
206
  */
200
207
  attachShortcuts(target) {
201
208
  return bindShortcuts(target, () => this.currentBindings());
@@ -216,18 +223,19 @@ export class MacroKit {
216
223
  }
217
224
  return bindings;
218
225
  }
219
- /* ---------- ייבוא/ייצוא ---------- */
226
+ /* ---------- Import/export ---------- */
220
227
  exportState() {
221
228
  return JSON.stringify(this.state, null, 2);
222
229
  }
223
230
  /**
224
- * ייבוא מ-JSON שיוצא ב-`exportState`. במיזוג (`merge: true`) פריט מיובא עם
225
- * `id` קיים מחליף את הקיים; בלי מיזוג המצב כולו מוחלף.
231
+ * Imports JSON produced by `exportState`. With `merge: true` an imported
232
+ * item with an existing `id` replaces it; without merge the whole state is
233
+ * replaced.
226
234
  */
227
235
  importState(json, options = {}) {
228
236
  const imported = parsePersistedState(json);
229
237
  if (!imported)
230
- return { ok: false, message: 'הקובץ אינו ייצוא מאקרו תקין' };
238
+ return { ok: false, message: macroMessages().invalidImport };
231
239
  if (options.merge) {
232
240
  for (const script of imported.scripts)
233
241
  this.upsert(this.state.scripts, script);
@@ -242,13 +250,13 @@ export class MacroKit {
242
250
  this.persist();
243
251
  return { ok: true };
244
252
  }
245
- /* ---------- פנימי ---------- */
253
+ /* ---------- Internal ---------- */
246
254
  guardRun() {
247
255
  if (this.recorder.recording) {
248
- return { ok: false, reason: 'error', message: 'אי אפשר להריץ מאקרו בזמן הקלטה' };
256
+ return { ok: false, reason: 'error', message: macroMessages().cannotRunWhileRecording };
249
257
  }
250
258
  if (this.running) {
251
- return { ok: false, reason: 'error', message: 'מאקרו אחר עדיין רץ' };
259
+ return { ok: false, reason: 'error', message: macroMessages().anotherMacroRunning };
252
260
  }
253
261
  return null;
254
262
  }
@@ -0,0 +1,50 @@
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
+ deleteForwardUnsupported: string;
31
+ scriptNotFound: string;
32
+ recordingNotFound: string;
33
+ snippetNotFound: string;
34
+ cannotRunWhileRecording: string;
35
+ anotherMacroRunning: string;
36
+ invalidImport: string;
37
+ noDocument: string;
38
+ unknownCommand: (id: string) => string;
39
+ actionFailed: string;
40
+ deletionUnavailable: string;
41
+ searchUnavailable: string;
42
+ searchUnavailableInDocument: string;
43
+ }
44
+ export declare const ENGLISH_MESSAGES: MacroMessages;
45
+ /** Hebrew locale — the strings the toolkit shipped with originally. */
46
+ export declare const HEBREW_MESSAGES: MacroMessages;
47
+ /** Replaces some or all runtime strings. Call once, before creating hosts/kits. */
48
+ export declare function setMacroMessages(messages: Partial<MacroMessages>): void;
49
+ /** The active locale. Internal — modules read strings through this. */
50
+ export declare function macroMessages(): MacroMessages;
@@ -0,0 +1,78 @@
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
+ deleteForwardUnsupported: 'Forward deletion is not supported during replay',
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
+ noDocument: 'No document is open',
38
+ unknownCommand: (id) => `The engine does not recognize the command ${id}`,
39
+ actionFailed: 'The operation failed',
40
+ deletionUnavailable: 'Deletion is not available in this document',
41
+ searchUnavailable: 'Search is not available',
42
+ searchUnavailableInDocument: 'Search is not available in this document',
43
+ };
44
+ /** Hebrew locale — the strings the toolkit shipped with originally. */
45
+ export const HEBREW_MESSAGES = {
46
+ unknownMethod: (method) => `מתודה לא מוכרת: ${method}`,
47
+ mustBeString: (name) => `${name} חייב להיות מחרוזת`,
48
+ commandFailed: (id) => `הפקודה ${id} נכשלה`,
49
+ insertTextFailed: 'הכנסת הטקסט נכשלה',
50
+ insertParagraphFailed: 'הכנסת הפסקה נכשלה',
51
+ deleteFailed: 'המחיקה נכשלה',
52
+ replaceFailed: 'ההחלפה נכשלה',
53
+ syntaxError: (detail) => `שגיאת תחביר במאקרו: ${detail}`,
54
+ timedOut: (seconds) => `המאקרו לא הסתיים תוך ${seconds} שניות ונעצר`,
55
+ callLimitExceeded: (limit) => `המאקרו חצה את תקרת הקריאות (${limit}) ונעצר`,
56
+ deleteForwardUnsupported: 'מחיקה קדימה אינה נתמכת בניגון',
57
+ scriptNotFound: 'המאקרו לא נמצא',
58
+ recordingNotFound: 'ההקלטה לא נמצאה',
59
+ snippetNotFound: 'הקטע לא נמצא',
60
+ cannotRunWhileRecording: 'אי אפשר להריץ מאקרו בזמן הקלטה',
61
+ anotherMacroRunning: 'מאקרו אחר עדיין רץ',
62
+ invalidImport: 'הקובץ אינו ייצוא מאקרו תקין',
63
+ noDocument: 'אין מסמך פתוח',
64
+ unknownCommand: (id) => `הפקודה ${id} אינה מוכרת למנוע`,
65
+ actionFailed: 'הפעולה נכשלה',
66
+ deletionUnavailable: 'מחיקה אינה זמינה במסמך הזה',
67
+ searchUnavailable: 'החיפוש אינו זמין',
68
+ searchUnavailableInDocument: 'החיפוש אינו זמין במסמך הזה',
69
+ };
70
+ let current = { ...ENGLISH_MESSAGES };
71
+ /** Replaces some or all runtime strings. Call once, before creating hosts/kits. */
72
+ export function setMacroMessages(messages) {
73
+ current = { ...current, ...messages };
74
+ }
75
+ /** The active locale. Internal — modules read strings through this. */
76
+ export function macroMessages() {
77
+ return current;
78
+ }
@@ -1,20 +1,8 @@
1
- /**
2
- * מקליט מאקרו בסגנון Word: מקליט **פקודות** והקלדה, לא מיקומי סמן.
3
- *
4
- * הבחירה הזאת מכוונת. הקלטת צעדי ProseMirror גולמיים (עם מיקומים אבסולוטיים)
5
- * נשברת ברגע שהמסמך שונה ממה שהיה בזמן ההקלטה; הקלטת פקודות ("bold",
6
- * "bullet-list", הקלדת "בס\"ד") מתנהגת כמו המקליט של Word — הפעולות חלות
7
- * במקום שבו הסמן נמצא בזמן הניגון. זה גם מה שהופך הקלטה לניתנת לשמירה
8
- * ולשיתוף: הצעדים JSON בלבד.
9
- *
10
- * מה לא נקלט: תנועת סמן ובחירה בעכבר. כמו ב-Word, מאקרו מוקלט פועל מהמקום
11
- * שבו הסמן עומד כשמריצים אותו.
12
- */
13
1
  import type { MacroHost, MacroStep } from '../types.js';
14
2
  export interface RecorderOptions {
15
- /** סינון פקודות. ברירת המחדל מקליטה הכול חוץ מ-undo/redo. */
3
+ /** Command filter. The default records everything except undo/redo. */
16
4
  shouldRecordCommand?: (id: string) => boolean;
17
- /** תקרת צעדים להקלטה אחת, נגד הקלטה שנשכחה פתוחה. */
5
+ /** Step cap per recording, against a recording left running by mistake. */
18
6
  maxSteps?: number;
19
7
  }
20
8
  export declare class MacroRecorder {
@@ -28,9 +16,9 @@ export declare class MacroRecorder {
28
16
  get recording(): boolean;
29
17
  get stepCount(): number;
30
18
  start(): void;
31
- /** עוצרת ומחזירה את הצעדים. ריקה כשלא הוקלט דבר. */
19
+ /** Stops and returns the steps. Empty when nothing was recorded. */
32
20
  stop(): MacroStep[];
33
- /** עוצרת וזורקת את מה שהוקלט. */
21
+ /** Stops and discards whatever was recorded. */
34
22
  cancel(): void;
35
23
  private teardown;
36
24
  private push;
@@ -44,14 +32,14 @@ export interface ReplayFailure {
44
32
  }
45
33
  export interface ReplayResult {
46
34
  ok: boolean;
47
- /** כמה צעדים הושלמו בהצלחה. */
35
+ /** How many steps completed successfully. */
48
36
  completed: number;
49
37
  failures: ReplayFailure[];
50
38
  }
51
39
  export interface ReplayOptions {
52
- /** עצירה בכשל הראשון. ברירת מחדל: true — מאקרו שנכשל באמצע לא ממשיך לרוץ עיוור. */
40
+ /** Stop at the first failure. Default: true — a macro that failed midway must not keep running blind. */
53
41
  stopOnError?: boolean;
54
- /** נקראת לפני כל צעד; מאפשרת מד התקדמות. */
42
+ /** Called before each step; enables a progress indicator. */
55
43
  onStep?: (index: number, step: MacroStep) => void;
56
44
  }
57
45
  export declare function replayMacro(host: MacroHost, steps: readonly MacroStep[], options?: ReplayOptions): Promise<ReplayResult>;
@@ -1,5 +1,20 @@
1
+ /**
2
+ * A Word-style macro recorder: records **commands** and typing, not caret
3
+ * positions.
4
+ *
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.
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.
14
+ */
15
+ import { macroMessages } from '../messages.js';
1
16
  const DEFAULT_MAX_STEPS = 5_000;
2
- /** undo/redo בזמן הקלטה מתקנים את ההקלטה עצמהניגון שלהם היה משחזר גם את הטעות. */
17
+ /** Undo/redo during recording fix the recording itselfreplaying them would replay the mistake too. */
3
18
  function defaultShouldRecord(id) {
4
19
  return id !== 'undo' && id !== 'redo';
5
20
  }
@@ -31,7 +46,7 @@ export class MacroRecorder {
31
46
  this.host.onTextInput((event) => this.recordTextInput(event)),
32
47
  ];
33
48
  }
34
- /** עוצרת ומחזירה את הצעדים. ריקה כשלא הוקלט דבר. */
49
+ /** Stops and returns the steps. Empty when nothing was recorded. */
35
50
  stop() {
36
51
  if (!this.active)
37
52
  return [];
@@ -40,7 +55,7 @@ export class MacroRecorder {
40
55
  this.steps = [];
41
56
  return recorded;
42
57
  }
43
- /** עוצרת וזורקת את מה שהוקלט. */
58
+ /** Stops and discards whatever was recorded. */
44
59
  cancel() {
45
60
  if (!this.active)
46
61
  return;
@@ -68,7 +83,7 @@ export class MacroRecorder {
68
83
  const last = this.steps[this.steps.length - 1];
69
84
  switch (event.kind) {
70
85
  case 'insert-text': {
71
- // הקשות רצופות מתלכדות לצעד אחדגם קריא יותר וגם ניגון מהיר יותר.
86
+ // Consecutive keystrokes coalesce into one step more readable, faster to replay.
72
87
  if (last?.type === 'insert-text') {
73
88
  last.text += event.text;
74
89
  return;
@@ -109,8 +124,8 @@ async function runStep(host, step) {
109
124
  case 'delete-backward':
110
125
  return host.deleteBackward(step.count);
111
126
  case 'delete-forward':
112
- // המנוע אינו חושף מחיקה קדימה נפרדת; מדווח ככשל מפורש ולא מדלג בשקט.
113
- return { ok: false, message: 'מחיקה קדימה אינה נתמכת בניגון', reason: 'unsupported-step' };
127
+ // The engine exposes no separate forward deletion; report an explicit failure rather than skipping silently.
128
+ return { ok: false, message: macroMessages().deleteForwardUnsupported, reason: 'unsupported-step' };
114
129
  }
115
130
  }
116
131
  export async function replayMacro(host, steps, options = {}) {
@@ -1,16 +1,5 @@
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;
16
5
  export declare function createEvalRunner(): MacroRunner;
@@ -1,21 +1,35 @@
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
+ /** An api proxy that routes everything through `bridge.call`, so the cap applies here too. */
19
33
  function apiThroughBridge(bridge) {
20
34
  return new Proxy({}, {
21
35
  get(_target, method) {
@@ -38,12 +52,12 @@ export function createEvalRunner() {
38
52
  return {
39
53
  ok: false,
40
54
  reason: 'error',
41
- message: `שגיאת תחביר במאקרו: ${error instanceof Error ? error.message : String(error)}`,
55
+ message: macroMessages().syntaxError(error instanceof Error ? error.message : String(error)),
42
56
  };
43
57
  }
44
58
  let timer;
45
59
  const timeout = new Promise((resolve) => {
46
- timer = setTimeout(() => resolve({ ok: false, reason: 'timeout', message: `המאקרו לא הסתיים תוך ${timeoutMs / 1000} שניות ונעצר` }), timeoutMs);
60
+ timer = setTimeout(() => resolve({ ok: false, reason: 'timeout', message: macroMessages().timedOut(timeoutMs / 1000) }), timeoutMs);
47
61
  });
48
62
  const run = (async () => {
49
63
  try {
@@ -1,5 +1,5 @@
1
1
  import { type MacroRunner } from './runner.js';
2
- /** סימון ההודעות של הפרוטוקול, כדי לא להתנגש בהודעות אחרות בדף. */
2
+ /** Protocol marker, so the messages cannot collide with others on the page. */
3
3
  export declare const PROTOCOL_MARK: "__otzariaMacro";
4
4
  export type SandboxMessage = {
5
5
  [PROTOCOL_MARK]: true;
@@ -31,11 +31,12 @@ export type HostMessage = {
31
31
  value?: unknown;
32
32
  message?: string;
33
33
  };
34
- /** האם הודעה שייכת לפרוטוקול. חשופה לבדיקות. */
34
+ /** Whether a message belongs to the protocol. Exposed for tests. */
35
35
  export declare function isProtocolMessage(data: unknown): data is SandboxMessage;
36
36
  /**
37
- * הקוד שרץ בתוך ה-iframe. מחרוזת ולא פונקציה מוסרלת כדי שה-build לא ישנה
38
- * אותו (minify של שמות היה שובר את הפרוטוקול).
37
+ * The code that runs inside the iframe. A string rather than a serialized
38
+ * function, so the build cannot touch it (minifying names would break the
39
+ * protocol).
39
40
  */
40
41
  export declare const SANDBOX_BOOTSTRAP: string;
41
42
  export declare function createIframeRunner(doc?: Document): MacroRunner;
@@ -1,8 +1,26 @@
1
+ /**
2
+ * Sandboxed script runner — an iframe with `sandbox="allow-scripts"` only.
3
+ *
4
+ * The iframe gets an opaque origin: no access to the page's DOM, to
5
+ * localStorage, to cookies, or to the network with the user's credentials.
6
+ * Its only way to touch the document is RPC over postMessage to the
7
+ * `MacroApi` methods — every call goes through `bridge.call`, which enforces
8
+ * a closed method list and a call cap.
9
+ *
10
+ * The time cap here is real: when it expires the iframe is removed from the
11
+ * DOM, which also kills an infinite synchronous loop — it runs on the
12
+ * iframe's event loop, not the page's.
13
+ *
14
+ * Return values and arguments cross a structured-clone boundary; the API is
15
+ * already shaped so everything it returns is JSON-safe (see
16
+ * `ScriptSelection`).
17
+ */
18
+ import { macroMessages } from '../messages.js';
1
19
  import { limitCalls } from './eval-runner.js';
2
20
  import { DEFAULT_MAX_API_CALLS, DEFAULT_TIMEOUT_MS, } from './runner.js';
3
- /** סימון ההודעות של הפרוטוקול, כדי לא להתנגש בהודעות אחרות בדף. */
21
+ /** Protocol marker, so the messages cannot collide with others on the page. */
4
22
  export const PROTOCOL_MARK = '__otzariaMacro';
5
- /** האם הודעה שייכת לפרוטוקול. חשופה לבדיקות. */
23
+ /** Whether a message belongs to the protocol. Exposed for tests. */
6
24
  export function isProtocolMessage(data) {
7
25
  return (typeof data === 'object' &&
8
26
  data !== null &&
@@ -10,8 +28,9 @@ export function isProtocolMessage(data) {
10
28
  typeof data.kind === 'string');
11
29
  }
12
30
  /**
13
- * הקוד שרץ בתוך ה-iframe. מחרוזת ולא פונקציה מוסרלת כדי שה-build לא ישנה
14
- * אותו (minify של שמות היה שובר את הפרוטוקול).
31
+ * The code that runs inside the iframe. A string rather than a serialized
32
+ * function, so the build cannot touch it (minifying names would break the
33
+ * protocol).
15
34
  */
16
35
  export const SANDBOX_BOOTSTRAP = `
17
36
  'use strict';
@@ -28,7 +47,7 @@ export const SANDBOX_BOOTSTRAP = `
28
47
  var api = new Proxy({}, {
29
48
  get: function (_target, method) {
30
49
  if (typeof method !== 'string') return undefined;
31
- if (method === 'then') return undefined; // ש-await api לא יתפרש כ-thenable
50
+ if (method === 'then') return undefined; // so "await api" is not treated as a thenable
32
51
  return function () {
33
52
  var args = Array.prototype.slice.call(arguments);
34
53
  return new Promise(function (resolve, reject) {
@@ -75,7 +94,7 @@ export const SANDBOX_BOOTSTRAP = `
75
94
  post({ kind: 'ready' });
76
95
  })();
77
96
  `;
78
- /** ערך בטוח למסירה חזרה ל-iframe (structured clone עלול להיכשל על אובייקטי מנוע). */
97
+ /** A value safe to hand back to the iframe (structured clone can fail on engine objects). */
79
98
  function toCloneSafe(value) {
80
99
  if (value === undefined || value === null)
81
100
  return value;
@@ -108,7 +127,7 @@ export function createIframeRunner(doc = document) {
108
127
  resolve(result);
109
128
  };
110
129
  const onMessage = (event) => {
111
- // רק הודעות מה-iframe הזה: דף יכול להריץ כמה מאקרו במקביל.
130
+ // Only messages from this iframe: a page may run several macros at once.
112
131
  if (event.source !== iframe.contentWindow)
113
132
  return;
114
133
  const data = event.data;
@@ -142,7 +161,7 @@ export function createIframeRunner(doc = document) {
142
161
  finish({ ok: false, reason: 'error', message: data.message });
143
162
  };
144
163
  addEventListener('message', onMessage);
145
- timer = setTimeout(() => finish({ ok: false, reason: 'timeout', message: `המאקרו לא הסתיים תוך ${timeoutMs / 1000} שניות ונעצר` }), timeoutMs);
164
+ timer = setTimeout(() => finish({ ok: false, reason: 'timeout', message: macroMessages().timedOut(timeoutMs / 1000) }), timeoutMs);
146
165
  doc.body.appendChild(iframe);
147
166
  });
148
167
  },