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/README.md +69 -66
- package/dist/host/superdoc-host.d.ts +2 -21
- package/dist/host/superdoc-host.js +56 -28
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/manager.d.ts +24 -21
- package/dist/manager.js +36 -28
- package/dist/messages.d.ts +50 -0
- package/dist/messages.js +78 -0
- package/dist/recorder/recorder.d.ts +7 -19
- package/dist/recorder/recorder.js +21 -6
- package/dist/scripting/eval-runner.d.ts +1 -12
- package/dist/scripting/eval-runner.js +20 -6
- package/dist/scripting/iframe-runner.d.ts +5 -4
- package/dist/scripting/iframe-runner.js +27 -8
- package/dist/scripting/macro-api.d.ts +12 -23
- package/dist/scripting/macro-api.js +24 -9
- package/dist/scripting/runner.d.ts +3 -3
- package/dist/shortcuts.d.ts +8 -6
- package/dist/shortcuts.js +5 -4
- package/dist/snippets/autotext.d.ts +16 -13
- package/dist/snippets/autotext.js +8 -6
- package/dist/snippets/snippets.d.ts +14 -10
- package/dist/snippets/snippets.js +8 -6
- package/dist/storage.d.ts +7 -6
- package/dist/storage.js +4 -4
- package/dist/types.d.ts +33 -32
- package/dist/types.js +6 -5
- package/package.json +1 -1
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 script — e.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
|
-
/**
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/messages.js
ADDED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
17
|
+
/** Undo/redo during recording fix the recording itself — replaying 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:
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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; //
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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:
|
|
164
|
+
timer = setTimeout(() => finish({ ok: false, reason: 'timeout', message: macroMessages().timedOut(timeoutMs / 1000) }), timeoutMs);
|
|
146
165
|
doc.body.appendChild(iframe);
|
|
147
166
|
});
|
|
148
167
|
},
|