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/README.md +81 -66
- package/dist/host/superdoc-host.d.ts +10 -21
- package/dist/host/superdoc-host.js +168 -51
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/manager.d.ts +49 -21
- package/dist/manager.js +102 -29
- package/dist/messages.d.ts +55 -0
- package/dist/messages.js +88 -0
- package/dist/recorder/recorder.d.ts +17 -15
- package/dist/recorder/recorder.js +5 -6
- package/dist/scripting/eval-runner.d.ts +14 -12
- package/dist/scripting/eval-runner.js +51 -8
- package/dist/scripting/iframe-runner.d.ts +5 -4
- package/dist/scripting/iframe-runner.js +42 -12
- 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 +20 -6
- package/dist/shortcuts.js +24 -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 +30 -6
- package/dist/storage.js +93 -5
- package/dist/types.d.ts +35 -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';
|
|
@@ -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 script — e.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
|
-
/**
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/messages.js
ADDED
|
@@ -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
|
-
*
|
|
2
|
+
* A Word-style macro recorder: records **commands** and typing, not caret
|
|
3
|
+
* positions.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* "bullet-list",
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
2
|
+
/** Undo/redo during recording fix the recording itself — replaying 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
};
|