superdoc-macros 0.1.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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `MacroKit` — הפאסדה שמארח מתקין פעם אחת ומקבל את שלוש היכולות מחווטות:
3
+ * סקריפטים (עם ארגז חול), מקליט, וקטעי טקסט עם השלמה אוטומטית — פלוס שמירה,
4
+ * ייבוא/ייצוא וקיצורי מקלדת.
5
+ *
6
+ * כלל בטיחות אחד נאכף כאן: אין ריצה בזמן הקלטה ואין שתי ריצות במקביל.
7
+ * ניגון או סקריפט שרצים תוך כדי הקלטה היו מוקלטים בעצמם ומכפילים את עצמם
8
+ * בניגון הבא.
9
+ */
10
+ import { type MacroApiOptions } from './scripting/macro-api.js';
11
+ import type { MacroRunner, MacroRunOptions, MacroRunResult } from './scripting/runner.js';
12
+ import { type ReplayOptions, type ReplayResult } from './recorder/recorder.js';
13
+ import { type AutoTextOptions } from './snippets/autotext.js';
14
+ import { type ExpandOptions } from './snippets/snippets.js';
15
+ import { type ShortcutTarget } from './shortcuts.js';
16
+ import { type MacroStorage } from './storage.js';
17
+ import type { MacroHost, RecordedMacro, SavedScript, Snippet } from './types.js';
18
+ export interface MacroKitOptions {
19
+ host: MacroHost;
20
+ /** ברירת מחדל: localStorage. */
21
+ storage?: MacroStorage;
22
+ /**
23
+ * `'iframe'` (ברירת המחדל) מריץ סקריפטים בארגז חול; `'eval'` מריץ ישירות —
24
+ * ראו את האזהרה ב-eval-runner. אפשר גם למסור מריץ מותאם.
25
+ */
26
+ runner?: MacroRunner | 'iframe' | 'eval';
27
+ /** אפשרויות ריצה לסקריפטים (זמן, תקרת קריאות). */
28
+ runOptions?: MacroRunOptions;
29
+ /** אפשרויות ההשלמה האוטומטית. */
30
+ autoText?: Omit<AutoTextOptions, 'onExpand' | 'onError'> & AutoTextOptions;
31
+ /** יומן ריצה של `api.log`. */
32
+ onLog?: MacroApiOptions['onLog'];
33
+ }
34
+ export declare class MacroKit {
35
+ private readonly host;
36
+ private readonly storage;
37
+ private readonly runner;
38
+ private readonly runOptions;
39
+ private readonly onLog?;
40
+ private state;
41
+ private readonly recorder;
42
+ private readonly autoText;
43
+ private running;
44
+ constructor(options: MacroKitOptions);
45
+ listScripts(): readonly SavedScript[];
46
+ saveScript(input: {
47
+ id?: string;
48
+ name: string;
49
+ source: string;
50
+ shortcut?: string;
51
+ }): SavedScript;
52
+ removeScript(id: string): void;
53
+ runScript(id: string): Promise<MacroRunResult>;
54
+ /** מריצה סקריפט שלא נשמר — למשל מתוך עורך המאקרו לפני שמירה. */
55
+ runSource(source: string): Promise<MacroRunResult>;
56
+ get isRecording(): boolean;
57
+ get recordedStepCount(): number;
58
+ startRecording(): void;
59
+ /** עוצרת ושומרת. `null` כשלא הוקלט אף צעד — אין מה לשמור. */
60
+ stopRecording(name: string, shortcut?: string): RecordedMacro | null;
61
+ cancelRecording(): void;
62
+ listRecordings(): readonly RecordedMacro[];
63
+ removeRecording(id: string): void;
64
+ replayRecording(id: string, options?: ReplayOptions): Promise<ReplayResult>;
65
+ listSnippets(): readonly Snippet[];
66
+ saveSnippet(input: {
67
+ id?: string;
68
+ name: string;
69
+ text: string;
70
+ trigger?: string;
71
+ shortcut?: string;
72
+ }): Snippet;
73
+ removeSnippet(id: string): void;
74
+ expandSnippet(id: string, options?: ExpandOptions): Promise<{
75
+ ok: boolean;
76
+ message?: string;
77
+ }>;
78
+ /** מפעילה השלמה אוטומטית (trigger + רווח). מחזירה פונקציית כיבוי. */
79
+ enableAutoText(): () => void;
80
+ disableAutoText(): void;
81
+ /**
82
+ * קושרת את הקיצורים של כל מה ששמור (סקריפטים, הקלטות, קטעים) ליעד — בדרך
83
+ * כלל ה-container של העורך או `window`. הרשימה חיה: שמירה חדשה נקלטת בלי
84
+ * לקשור מחדש. מחזירה פונקציית ניתוק.
85
+ */
86
+ attachShortcuts(target: ShortcutTarget): () => void;
87
+ private currentBindings;
88
+ exportState(): string;
89
+ /**
90
+ * ייבוא מ-JSON שיוצא ב-`exportState`. במיזוג (`merge: true`) פריט מיובא עם
91
+ * `id` קיים מחליף את הקיים; בלי מיזוג המצב כולו מוחלף.
92
+ */
93
+ importState(json: string, options?: {
94
+ merge?: boolean;
95
+ }): {
96
+ ok: boolean;
97
+ message?: string;
98
+ };
99
+ private guardRun;
100
+ private upsert;
101
+ private persist;
102
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * `MacroKit` — הפאסדה שמארח מתקין פעם אחת ומקבל את שלוש היכולות מחווטות:
3
+ * סקריפטים (עם ארגז חול), מקליט, וקטעי טקסט עם השלמה אוטומטית — פלוס שמירה,
4
+ * ייבוא/ייצוא וקיצורי מקלדת.
5
+ *
6
+ * כלל בטיחות אחד נאכף כאן: אין ריצה בזמן הקלטה ואין שתי ריצות במקביל.
7
+ * ניגון או סקריפט שרצים תוך כדי הקלטה היו מוקלטים בעצמם ומכפילים את עצמם
8
+ * בניגון הבא.
9
+ */
10
+ import { createMacroApi } from './scripting/macro-api.js';
11
+ import { createEvalRunner } from './scripting/eval-runner.js';
12
+ import { createIframeRunner } from './scripting/iframe-runner.js';
13
+ import { MacroRecorder, replayMacro } from './recorder/recorder.js';
14
+ import { AutoText } from './snippets/autotext.js';
15
+ import { expandSnippet } from './snippets/snippets.js';
16
+ import { bindShortcuts } from './shortcuts.js';
17
+ import { createLocalStorage, emptyState, parsePersistedState } from './storage.js';
18
+ let idCounter = 0;
19
+ function newId() {
20
+ try {
21
+ return crypto.randomUUID();
22
+ }
23
+ catch {
24
+ idCounter += 1;
25
+ return `macro-${Date.now().toString(36)}-${idCounter}`;
26
+ }
27
+ }
28
+ export class MacroKit {
29
+ host;
30
+ storage;
31
+ runner;
32
+ runOptions;
33
+ onLog;
34
+ state;
35
+ recorder;
36
+ autoText;
37
+ running = false;
38
+ constructor(options) {
39
+ this.host = options.host;
40
+ this.storage = options.storage ?? createLocalStorage();
41
+ this.runOptions = options.runOptions ?? {};
42
+ this.onLog = options.onLog;
43
+ const runner = options.runner ?? 'iframe';
44
+ this.runner =
45
+ runner === 'iframe' ? createIframeRunner() : runner === 'eval' ? createEvalRunner() : runner;
46
+ this.state = this.storage.load() ?? emptyState();
47
+ this.recorder = new MacroRecorder(this.host);
48
+ this.autoText = new AutoText(this.host, () => this.state.snippets, options.autoText);
49
+ }
50
+ /* ---------- סקריפטים ---------- */
51
+ listScripts() {
52
+ return this.state.scripts;
53
+ }
54
+ saveScript(input) {
55
+ const script = {
56
+ id: input.id ?? newId(),
57
+ name: input.name,
58
+ source: input.source,
59
+ ...(input.shortcut ? { shortcut: input.shortcut } : {}),
60
+ };
61
+ this.upsert(this.state.scripts, script);
62
+ this.persist();
63
+ return script;
64
+ }
65
+ removeScript(id) {
66
+ this.state.scripts = this.state.scripts.filter((script) => script.id !== id);
67
+ this.persist();
68
+ }
69
+ async runScript(id) {
70
+ const script = this.state.scripts.find((entry) => entry.id === id);
71
+ if (!script)
72
+ return { ok: false, reason: 'error', message: 'המאקרו לא נמצא' };
73
+ return this.runSource(script.source);
74
+ }
75
+ /** מריצה סקריפט שלא נשמר — למשל מתוך עורך המאקרו לפני שמירה. */
76
+ async runSource(source) {
77
+ const guard = this.guardRun();
78
+ if (guard)
79
+ return guard;
80
+ this.running = true;
81
+ try {
82
+ const bridge = createMacroApi(this.host, { onLog: this.onLog });
83
+ return await this.runner.run(source, bridge, this.runOptions);
84
+ }
85
+ finally {
86
+ this.running = false;
87
+ }
88
+ }
89
+ /* ---------- מקליט ---------- */
90
+ get isRecording() {
91
+ return this.recorder.recording;
92
+ }
93
+ get recordedStepCount() {
94
+ return this.recorder.stepCount;
95
+ }
96
+ startRecording() {
97
+ if (this.running)
98
+ return;
99
+ this.recorder.start();
100
+ }
101
+ /** עוצרת ושומרת. `null` כשלא הוקלט אף צעד — אין מה לשמור. */
102
+ stopRecording(name, shortcut) {
103
+ const steps = this.recorder.stop();
104
+ if (steps.length === 0)
105
+ return null;
106
+ const recording = {
107
+ version: 1,
108
+ id: newId(),
109
+ name,
110
+ createdAt: new Date().toISOString(),
111
+ ...(shortcut ? { shortcut } : {}),
112
+ steps,
113
+ };
114
+ this.state.recordings.push(recording);
115
+ this.persist();
116
+ return recording;
117
+ }
118
+ cancelRecording() {
119
+ this.recorder.cancel();
120
+ }
121
+ listRecordings() {
122
+ return this.state.recordings;
123
+ }
124
+ removeRecording(id) {
125
+ this.state.recordings = this.state.recordings.filter((recording) => recording.id !== id);
126
+ this.persist();
127
+ }
128
+ async replayRecording(id, options) {
129
+ const recording = this.state.recordings.find((entry) => entry.id === id);
130
+ if (!recording)
131
+ return { ok: false, completed: 0, failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: 'ההקלטה לא נמצאה' }] };
132
+ const guard = this.guardRun();
133
+ if (guard) {
134
+ return { ok: false, completed: 0, failures: [{ stepIndex: -1, step: { type: 'insert-text', text: '' }, message: guard.message }] };
135
+ }
136
+ this.running = true;
137
+ try {
138
+ return await replayMacro(this.host, recording.steps, options);
139
+ }
140
+ finally {
141
+ this.running = false;
142
+ }
143
+ }
144
+ /* ---------- קטעי טקסט ---------- */
145
+ listSnippets() {
146
+ return this.state.snippets;
147
+ }
148
+ saveSnippet(input) {
149
+ const snippet = {
150
+ id: input.id ?? newId(),
151
+ name: input.name,
152
+ text: input.text,
153
+ ...(input.trigger ? { trigger: input.trigger } : {}),
154
+ ...(input.shortcut ? { shortcut: input.shortcut } : {}),
155
+ };
156
+ this.upsert(this.state.snippets, snippet);
157
+ this.persist();
158
+ return snippet;
159
+ }
160
+ removeSnippet(id) {
161
+ this.state.snippets = this.state.snippets.filter((snippet) => snippet.id !== id);
162
+ this.persist();
163
+ }
164
+ async expandSnippet(id, options) {
165
+ const snippet = this.state.snippets.find((entry) => entry.id === id);
166
+ if (!snippet)
167
+ return { ok: false, message: 'הקטע לא נמצא' };
168
+ const outcome = await expandSnippet(this.host, snippet, options);
169
+ return outcome.ok ? { ok: true } : { ok: false, message: outcome.message };
170
+ }
171
+ /** מפעילה השלמה אוטומטית (trigger + רווח). מחזירה פונקציית כיבוי. */
172
+ enableAutoText() {
173
+ return this.autoText.attach();
174
+ }
175
+ disableAutoText() {
176
+ this.autoText.detach();
177
+ }
178
+ /* ---------- קיצורי מקלדת ---------- */
179
+ /**
180
+ * קושרת את הקיצורים של כל מה ששמור (סקריפטים, הקלטות, קטעים) ליעד — בדרך
181
+ * כלל ה-container של העורך או `window`. הרשימה חיה: שמירה חדשה נקלטת בלי
182
+ * לקשור מחדש. מחזירה פונקציית ניתוק.
183
+ */
184
+ attachShortcuts(target) {
185
+ return bindShortcuts(target, () => this.currentBindings());
186
+ }
187
+ currentBindings() {
188
+ const bindings = [];
189
+ for (const script of this.state.scripts) {
190
+ if (script.shortcut)
191
+ bindings.push({ shortcut: script.shortcut, run: () => this.runScript(script.id) });
192
+ }
193
+ for (const recording of this.state.recordings) {
194
+ if (recording.shortcut)
195
+ bindings.push({ shortcut: recording.shortcut, run: () => this.replayRecording(recording.id) });
196
+ }
197
+ for (const snippet of this.state.snippets) {
198
+ if (snippet.shortcut)
199
+ bindings.push({ shortcut: snippet.shortcut, run: () => this.expandSnippet(snippet.id) });
200
+ }
201
+ return bindings;
202
+ }
203
+ /* ---------- ייבוא/ייצוא ---------- */
204
+ exportState() {
205
+ return JSON.stringify(this.state, null, 2);
206
+ }
207
+ /**
208
+ * ייבוא מ-JSON שיוצא ב-`exportState`. במיזוג (`merge: true`) פריט מיובא עם
209
+ * `id` קיים מחליף את הקיים; בלי מיזוג המצב כולו מוחלף.
210
+ */
211
+ importState(json, options = {}) {
212
+ const imported = parsePersistedState(json);
213
+ if (!imported)
214
+ return { ok: false, message: 'הקובץ אינו ייצוא מאקרו תקין' };
215
+ if (options.merge) {
216
+ for (const script of imported.scripts)
217
+ this.upsert(this.state.scripts, script);
218
+ for (const recording of imported.recordings)
219
+ this.upsert(this.state.recordings, recording);
220
+ for (const snippet of imported.snippets)
221
+ this.upsert(this.state.snippets, snippet);
222
+ }
223
+ else {
224
+ this.state = imported;
225
+ }
226
+ this.persist();
227
+ return { ok: true };
228
+ }
229
+ /* ---------- פנימי ---------- */
230
+ guardRun() {
231
+ if (this.recorder.recording) {
232
+ return { ok: false, reason: 'error', message: 'אי אפשר להריץ מאקרו בזמן הקלטה' };
233
+ }
234
+ if (this.running) {
235
+ return { ok: false, reason: 'error', message: 'מאקרו אחר עדיין רץ' };
236
+ }
237
+ return null;
238
+ }
239
+ upsert(list, item) {
240
+ const index = list.findIndex((entry) => entry.id === item.id);
241
+ if (index >= 0)
242
+ list[index] = item;
243
+ else
244
+ list.push(item);
245
+ }
246
+ persist() {
247
+ this.storage.save(this.state);
248
+ }
249
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * מקליט מאקרו בסגנון Word: מקליט **פקודות** והקלדה, לא מיקומי סמן.
3
+ *
4
+ * הבחירה הזאת מכוונת. הקלטת צעדי ProseMirror גולמיים (עם מיקומים אבסולוטיים)
5
+ * נשברת ברגע שהמסמך שונה ממה שהיה בזמן ההקלטה; הקלטת פקודות ("bold",
6
+ * "bullet-list", הקלדת "בס\"ד") מתנהגת כמו המקליט של Word — הפעולות חלות
7
+ * במקום שבו הסמן נמצא בזמן הניגון. זה גם מה שהופך הקלטה לניתנת לשמירה
8
+ * ולשיתוף: הצעדים JSON בלבד.
9
+ *
10
+ * מה לא נקלט: תנועת סמן ובחירה בעכבר. כמו ב-Word, מאקרו מוקלט פועל מהמקום
11
+ * שבו הסמן עומד כשמריצים אותו.
12
+ */
13
+ import type { MacroHost, MacroStep } from '../types.js';
14
+ export interface RecorderOptions {
15
+ /** סינון פקודות. ברירת המחדל מקליטה הכול חוץ מ-undo/redo. */
16
+ shouldRecordCommand?: (id: string) => boolean;
17
+ /** תקרת צעדים להקלטה אחת, נגד הקלטה שנשכחה פתוחה. */
18
+ maxSteps?: number;
19
+ }
20
+ export declare class MacroRecorder {
21
+ private readonly host;
22
+ private readonly shouldRecordCommand;
23
+ private readonly maxSteps;
24
+ private steps;
25
+ private disposers;
26
+ private active;
27
+ constructor(host: MacroHost, options?: RecorderOptions);
28
+ get recording(): boolean;
29
+ get stepCount(): number;
30
+ start(): void;
31
+ /** עוצרת ומחזירה את הצעדים. ריקה כשלא הוקלט דבר. */
32
+ stop(): MacroStep[];
33
+ /** עוצרת וזורקת את מה שהוקלט. */
34
+ cancel(): void;
35
+ private teardown;
36
+ private push;
37
+ private recordCommand;
38
+ private recordTextInput;
39
+ }
40
+ export interface ReplayFailure {
41
+ stepIndex: number;
42
+ step: MacroStep;
43
+ message: string;
44
+ }
45
+ export interface ReplayResult {
46
+ ok: boolean;
47
+ /** כמה צעדים הושלמו בהצלחה. */
48
+ completed: number;
49
+ failures: ReplayFailure[];
50
+ }
51
+ export interface ReplayOptions {
52
+ /** עצירה בכשל הראשון. ברירת מחדל: true — מאקרו שנכשל באמצע לא ממשיך לרוץ עיוור. */
53
+ stopOnError?: boolean;
54
+ /** נקראת לפני כל צעד; מאפשרת מד התקדמות. */
55
+ onStep?: (index: number, step: MacroStep) => void;
56
+ }
57
+ export declare function replayMacro(host: MacroHost, steps: readonly MacroStep[], options?: ReplayOptions): Promise<ReplayResult>;
@@ -0,0 +1,141 @@
1
+ const DEFAULT_MAX_STEPS = 5_000;
2
+ /** undo/redo בזמן הקלטה מתקנים את ההקלטה עצמה — ניגון שלהם היה משחזר גם את הטעות. */
3
+ function defaultShouldRecord(id) {
4
+ return id !== 'undo' && id !== 'redo';
5
+ }
6
+ export class MacroRecorder {
7
+ host;
8
+ shouldRecordCommand;
9
+ maxSteps;
10
+ steps = [];
11
+ disposers = [];
12
+ active = false;
13
+ constructor(host, options = {}) {
14
+ this.host = host;
15
+ this.shouldRecordCommand = options.shouldRecordCommand ?? defaultShouldRecord;
16
+ this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
17
+ }
18
+ get recording() {
19
+ return this.active;
20
+ }
21
+ get stepCount() {
22
+ return this.steps.length;
23
+ }
24
+ start() {
25
+ if (this.active)
26
+ return;
27
+ this.active = true;
28
+ this.steps = [];
29
+ this.disposers = [
30
+ this.host.onCommand((id, payload) => this.recordCommand(id, payload)),
31
+ this.host.onTextInput((event) => this.recordTextInput(event)),
32
+ ];
33
+ }
34
+ /** עוצרת ומחזירה את הצעדים. ריקה כשלא הוקלט דבר. */
35
+ stop() {
36
+ if (!this.active)
37
+ return [];
38
+ this.teardown();
39
+ const recorded = this.steps;
40
+ this.steps = [];
41
+ return recorded;
42
+ }
43
+ /** עוצרת וזורקת את מה שהוקלט. */
44
+ cancel() {
45
+ if (!this.active)
46
+ return;
47
+ this.teardown();
48
+ this.steps = [];
49
+ }
50
+ teardown() {
51
+ this.active = false;
52
+ for (const dispose of this.disposers.splice(0))
53
+ dispose();
54
+ }
55
+ push(step) {
56
+ if (this.steps.length >= this.maxSteps) {
57
+ this.teardown();
58
+ return;
59
+ }
60
+ this.steps.push(step);
61
+ }
62
+ recordCommand(id, payload) {
63
+ if (!this.shouldRecordCommand(id))
64
+ return;
65
+ this.push(payload === undefined ? { type: 'command', id } : { type: 'command', id, payload });
66
+ }
67
+ recordTextInput(event) {
68
+ const last = this.steps[this.steps.length - 1];
69
+ switch (event.kind) {
70
+ case 'insert-text': {
71
+ // הקשות רצופות מתלכדות לצעד אחד — גם קריא יותר וגם ניגון מהיר יותר.
72
+ if (last?.type === 'insert-text') {
73
+ last.text += event.text;
74
+ return;
75
+ }
76
+ this.push({ type: 'insert-text', text: event.text });
77
+ return;
78
+ }
79
+ case 'insert-paragraph':
80
+ this.push({ type: 'insert-paragraph' });
81
+ return;
82
+ case 'delete-backward': {
83
+ if (last?.type === 'delete-backward') {
84
+ last.count += 1;
85
+ return;
86
+ }
87
+ this.push({ type: 'delete-backward', count: 1 });
88
+ return;
89
+ }
90
+ case 'delete-forward': {
91
+ if (last?.type === 'delete-forward') {
92
+ last.count += 1;
93
+ return;
94
+ }
95
+ this.push({ type: 'delete-forward', count: 1 });
96
+ return;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ async function runStep(host, step) {
102
+ switch (step.type) {
103
+ case 'command':
104
+ return host.commands.execute(step.id, step.payload);
105
+ case 'insert-text':
106
+ return host.insertText(step.text);
107
+ case 'insert-paragraph':
108
+ return host.insertText('\n');
109
+ case 'delete-backward':
110
+ return host.deleteBackward(step.count);
111
+ case 'delete-forward':
112
+ // המנוע אינו חושף מחיקה קדימה נפרדת; מדווח ככשל מפורש ולא מדלג בשקט.
113
+ return { ok: false, message: 'מחיקה קדימה אינה נתמכת בניגון', reason: 'unsupported-step' };
114
+ }
115
+ }
116
+ export async function replayMacro(host, steps, options = {}) {
117
+ const stopOnError = options.stopOnError ?? true;
118
+ const failures = [];
119
+ let completed = 0;
120
+ for (let index = 0; index < steps.length; index += 1) {
121
+ const step = steps[index];
122
+ if (!step)
123
+ continue;
124
+ options.onStep?.(index, step);
125
+ let outcome;
126
+ try {
127
+ outcome = await runStep(host, step);
128
+ }
129
+ catch (error) {
130
+ outcome = { ok: false, message: error instanceof Error ? error.message : String(error) };
131
+ }
132
+ if (outcome.ok) {
133
+ completed += 1;
134
+ continue;
135
+ }
136
+ failures.push({ stepIndex: index, step, message: outcome.message });
137
+ if (stopOnError)
138
+ break;
139
+ }
140
+ return { ok: failures.length === 0, completed, failures };
141
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * מריץ סקריפטים ישיר — `AsyncFunction` באותו הקשר של הדף.
3
+ *
4
+ * **אינו ארגז חול.** סקריפט שרץ כאן מקבל גישה לכל מה שהדף מכיר. מיועד לשני
5
+ * מצבים: בדיקות, וסביבה שבה כל המאקרו נכתבים בידי המשתמש עצמו והוחלט
6
+ * במפורש לוותר על בידוד (למשל בגלל CSP שחוסם iframe). ברירת המחדל של
7
+ * `MacroKit` היא מריץ ה-iframe.
8
+ *
9
+ * תקרת הזמן כאן היא race על ההבטחה בלבד: לולאה סינכרונית אינסופית תחסום את
10
+ * ה-thread ולא תיעצר. תקרת הקריאות כן נאכפת, דרך ה-bridge.
11
+ */
12
+ import type { MacroBridge } from './macro-api.js';
13
+ import { type MacroRunner } from './runner.js';
14
+ /** עוטפת bridge בתקרת קריאות. חשופה כדי ששני המריצים ישתמשו באותה אכיפה. */
15
+ export declare function limitCalls(bridge: MacroBridge, maxCalls: number): MacroBridge;
16
+ export declare function createEvalRunner(): MacroRunner;
@@ -0,0 +1,69 @@
1
+ import { DEFAULT_MAX_API_CALLS, DEFAULT_TIMEOUT_MS, } from './runner.js';
2
+ const AsyncFunction = Object.getPrototypeOf(async function () {
3
+ /* טיפוס בלבד */
4
+ }).constructor;
5
+ /** עוטפת bridge בתקרת קריאות. חשופה כדי ששני המריצים ישתמשו באותה אכיפה. */
6
+ export function limitCalls(bridge, maxCalls) {
7
+ return {
8
+ api: bridge.api,
9
+ callCount: bridge.callCount,
10
+ call(method, args) {
11
+ if (bridge.callCount() >= maxCalls) {
12
+ return Promise.reject(new Error(`המאקרו חצה את תקרת הקריאות (${maxCalls}) ונעצר`));
13
+ }
14
+ return bridge.call(method, args);
15
+ },
16
+ };
17
+ }
18
+ /** proxy של api שמנתב הכול דרך `bridge.call`, כדי שהתקרה תיאכף גם כאן. */
19
+ function apiThroughBridge(bridge) {
20
+ return new Proxy({}, {
21
+ get(_target, method) {
22
+ if (typeof method !== 'string')
23
+ return undefined;
24
+ return (...args) => bridge.call(method, args);
25
+ },
26
+ });
27
+ }
28
+ export function createEvalRunner() {
29
+ return {
30
+ async run(source, bridge, options = {}) {
31
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32
+ const limited = limitCalls(bridge, options.maxApiCalls ?? DEFAULT_MAX_API_CALLS);
33
+ let fn;
34
+ try {
35
+ fn = new AsyncFunction('api', `"use strict";\n${source}`);
36
+ }
37
+ catch (error) {
38
+ return {
39
+ ok: false,
40
+ reason: 'error',
41
+ message: `שגיאת תחביר במאקרו: ${error instanceof Error ? error.message : String(error)}`,
42
+ };
43
+ }
44
+ let timer;
45
+ const timeout = new Promise((resolve) => {
46
+ timer = setTimeout(() => resolve({ ok: false, reason: 'timeout', message: `המאקרו לא הסתיים תוך ${timeoutMs / 1000} שניות ונעצר` }), timeoutMs);
47
+ });
48
+ const run = (async () => {
49
+ try {
50
+ const value = await fn(apiThroughBridge(limited));
51
+ return { ok: true, value };
52
+ }
53
+ catch (error) {
54
+ return {
55
+ ok: false,
56
+ reason: 'error',
57
+ message: error instanceof Error ? error.message : String(error),
58
+ };
59
+ }
60
+ })();
61
+ try {
62
+ return await Promise.race([run, timeout]);
63
+ }
64
+ finally {
65
+ clearTimeout(timer);
66
+ }
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,41 @@
1
+ import { type MacroRunner } from './runner.js';
2
+ /** סימון ההודעות של הפרוטוקול, כדי לא להתנגש בהודעות אחרות בדף. */
3
+ export declare const PROTOCOL_MARK: "__otzariaMacro";
4
+ export type SandboxMessage = {
5
+ [PROTOCOL_MARK]: true;
6
+ kind: 'ready';
7
+ } | {
8
+ [PROTOCOL_MARK]: true;
9
+ kind: 'call';
10
+ id: number;
11
+ method: string;
12
+ args: unknown[];
13
+ } | {
14
+ [PROTOCOL_MARK]: true;
15
+ kind: 'done';
16
+ value: unknown;
17
+ } | {
18
+ [PROTOCOL_MARK]: true;
19
+ kind: 'error';
20
+ message: string;
21
+ };
22
+ export type HostMessage = {
23
+ [PROTOCOL_MARK]: true;
24
+ kind: 'run';
25
+ source: string;
26
+ } | {
27
+ [PROTOCOL_MARK]: true;
28
+ kind: 'result';
29
+ id: number;
30
+ ok: boolean;
31
+ value?: unknown;
32
+ message?: string;
33
+ };
34
+ /** האם הודעה שייכת לפרוטוקול. חשופה לבדיקות. */
35
+ export declare function isProtocolMessage(data: unknown): data is SandboxMessage;
36
+ /**
37
+ * הקוד שרץ בתוך ה-iframe. מחרוזת ולא פונקציה מוסרלת — כדי שה-build לא ישנה
38
+ * אותו (minify של שמות היה שובר את הפרוטוקול).
39
+ */
40
+ export declare const SANDBOX_BOOTSTRAP: string;
41
+ export declare function createIframeRunner(doc?: Document): MacroRunner;