superdoc-macros 0.1.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 +70 -64
- 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 +29 -20
- package/dist/manager.js +51 -27
- 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,10 +126,31 @@ export class MacroKit {
|
|
|
125
126
|
this.state.recordings = this.state.recordings.filter((recording) => recording.id !== id);
|
|
126
127
|
this.persist();
|
|
127
128
|
}
|
|
129
|
+
/** Renames a recording or edits its shortcut. `null` when the recording was not found. */
|
|
130
|
+
updateRecording(input) {
|
|
131
|
+
const recording = this.state.recordings.find((entry) => entry.id === input.id);
|
|
132
|
+
if (!recording)
|
|
133
|
+
return null;
|
|
134
|
+
if (input.name !== undefined)
|
|
135
|
+
recording.name = input.name;
|
|
136
|
+
if (input.shortcut !== undefined) {
|
|
137
|
+
if (input.shortcut)
|
|
138
|
+
recording.shortcut = input.shortcut;
|
|
139
|
+
else
|
|
140
|
+
delete recording.shortcut;
|
|
141
|
+
}
|
|
142
|
+
this.persist();
|
|
143
|
+
return recording;
|
|
144
|
+
}
|
|
128
145
|
async replayRecording(id, options) {
|
|
129
146
|
const recording = this.state.recordings.find((entry) => entry.id === id);
|
|
130
|
-
if (!recording)
|
|
131
|
-
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
|
+
}
|
|
132
154
|
const guard = this.guardRun();
|
|
133
155
|
if (guard) {
|
|
134
156
|
return { ok: false, completed: 0, failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: guard.message }] };
|
|
@@ -141,7 +163,7 @@ export class MacroKit {
|
|
|
141
163
|
this.running = false;
|
|
142
164
|
}
|
|
143
165
|
}
|
|
144
|
-
/* ----------
|
|
166
|
+
/* ---------- Snippets ---------- */
|
|
145
167
|
listSnippets() {
|
|
146
168
|
return this.state.snippets;
|
|
147
169
|
}
|
|
@@ -164,22 +186,23 @@ export class MacroKit {
|
|
|
164
186
|
async expandSnippet(id, options) {
|
|
165
187
|
const snippet = this.state.snippets.find((entry) => entry.id === id);
|
|
166
188
|
if (!snippet)
|
|
167
|
-
return { ok: false, message:
|
|
189
|
+
return { ok: false, message: macroMessages().snippetNotFound };
|
|
168
190
|
const outcome = await expandSnippet(this.host, snippet, options);
|
|
169
191
|
return outcome.ok ? { ok: true } : { ok: false, message: outcome.message };
|
|
170
192
|
}
|
|
171
|
-
/**
|
|
193
|
+
/** Enables auto-text (trigger + space). Returns a disable function. */
|
|
172
194
|
enableAutoText() {
|
|
173
195
|
return this.autoText.attach();
|
|
174
196
|
}
|
|
175
197
|
disableAutoText() {
|
|
176
198
|
this.autoText.detach();
|
|
177
199
|
}
|
|
178
|
-
/* ----------
|
|
200
|
+
/* ---------- Keyboard shortcuts ---------- */
|
|
179
201
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
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.
|
|
183
206
|
*/
|
|
184
207
|
attachShortcuts(target) {
|
|
185
208
|
return bindShortcuts(target, () => this.currentBindings());
|
|
@@ -200,18 +223,19 @@ export class MacroKit {
|
|
|
200
223
|
}
|
|
201
224
|
return bindings;
|
|
202
225
|
}
|
|
203
|
-
/* ----------
|
|
226
|
+
/* ---------- Import/export ---------- */
|
|
204
227
|
exportState() {
|
|
205
228
|
return JSON.stringify(this.state, null, 2);
|
|
206
229
|
}
|
|
207
230
|
/**
|
|
208
|
-
*
|
|
209
|
-
* `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.
|
|
210
234
|
*/
|
|
211
235
|
importState(json, options = {}) {
|
|
212
236
|
const imported = parsePersistedState(json);
|
|
213
237
|
if (!imported)
|
|
214
|
-
return { ok: false, message:
|
|
238
|
+
return { ok: false, message: macroMessages().invalidImport };
|
|
215
239
|
if (options.merge) {
|
|
216
240
|
for (const script of imported.scripts)
|
|
217
241
|
this.upsert(this.state.scripts, script);
|
|
@@ -226,13 +250,13 @@ export class MacroKit {
|
|
|
226
250
|
this.persist();
|
|
227
251
|
return { ok: true };
|
|
228
252
|
}
|
|
229
|
-
/* ----------
|
|
253
|
+
/* ---------- Internal ---------- */
|
|
230
254
|
guardRun() {
|
|
231
255
|
if (this.recorder.recording) {
|
|
232
|
-
return { ok: false, reason: 'error', message:
|
|
256
|
+
return { ok: false, reason: 'error', message: macroMessages().cannotRunWhileRecording };
|
|
233
257
|
}
|
|
234
258
|
if (this.running) {
|
|
235
|
-
return { ok: false, reason: 'error', message:
|
|
259
|
+
return { ok: false, reason: 'error', message: macroMessages().anotherMacroRunning };
|
|
236
260
|
}
|
|
237
261
|
return null;
|
|
238
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
|
},
|