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.
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/host/superdoc-host.d.ts +130 -0
- package/dist/host/superdoc-host.js +257 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/manager.d.ts +102 -0
- package/dist/manager.js +249 -0
- package/dist/recorder/recorder.d.ts +57 -0
- package/dist/recorder/recorder.js +141 -0
- package/dist/scripting/eval-runner.d.ts +16 -0
- package/dist/scripting/eval-runner.js +69 -0
- package/dist/scripting/iframe-runner.d.ts +41 -0
- package/dist/scripting/iframe-runner.js +150 -0
- package/dist/scripting/macro-api.d.ts +69 -0
- package/dist/scripting/macro-api.js +102 -0
- package/dist/scripting/runner.d.ts +21 -0
- package/dist/scripting/runner.js +2 -0
- package/dist/shortcuts.d.ts +39 -0
- package/dist/shortcuts.js +79 -0
- package/dist/snippets/autotext.d.ts +43 -0
- package/dist/snippets/autotext.js +106 -0
- package/dist/snippets/snippets.d.ts +26 -0
- package/dist/snippets/snippets.js +38 -0
- package/dist/storage.d.ts +24 -0
- package/dist/storage.js +65 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +9 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 palmoni5
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# superdoc-macros
|
|
2
|
+
|
|
3
|
+
ערכת מאקרו לעורכים מבוססי **SuperDoc v2**. נכתבה במקור עבור [otzaria-word-editor](https://github.com/Y-PLONI/otzaria-word-editor), אך כללית לחלוטין: אין תלות בו ואף לא בחבילת superdoc עצמה (החיבור למנוע מבני, דרך המשטחים הציבוריים שלו), והליבה עובדת מול כל עורך שמממש ממשק `MacroHost` קטן.
|
|
4
|
+
|
|
5
|
+
שלוש יכולות, בדומה למאקרו של Word:
|
|
6
|
+
|
|
7
|
+
| יכולת | מה זה נותן |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| **סקריפטים** | מאקרו כתובים ב-JavaScript שרצים בארגז חול (iframe מבודד) מול API מצומצם ובטוח של המסמך |
|
|
10
|
+
| **מקליט מאקרו** | "הקלט → עשה פעולות → עצור → נגן" — מקליט פקודות והקלדה, כמו המקליט של Word |
|
|
11
|
+
| **קטעי טקסט (Snippets)** | תבניות עם משתנים (`{{date}}`, `{{selection}}`…), קיצורי מקלדת, והשלמה אוטומטית בהקלדה (הקלדת `בסד` + רווח ← `בס"ד`) |
|
|
12
|
+
|
|
13
|
+
בנוסף: שמירה מתמשכת (localStorage או אחסון מותאם), ייבוא/ייצוא JSON, וקישור קיצורי מקלדת.
|
|
14
|
+
|
|
15
|
+
> **הערה על VBA:** החבילה אינה מריצה מאקרו VBA מתוך קובצי `.docm` — אין מנוע VBA בדפדפן. היא נותנת מערכת מאקרו מקבילה, מבוססת JavaScript, שמתאימה לעורך רץ-בדפדפן.
|
|
16
|
+
|
|
17
|
+
## התקנה
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install superdoc-macros
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
או ישירות מגיטהאב (עד הפרסום ב-npm):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install github:palmoni5/superdoc-macros
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## התחלה מהירה (עם SuperDoc)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { MacroKit, createSuperdocHost } from 'superdoc-macros';
|
|
33
|
+
|
|
34
|
+
// superdoc — מופע SuperDoc מוכן (אחרי onReady); container — האלמנט שהמסמך מרונדר בו.
|
|
35
|
+
const host = createSuperdocHost({ superdoc, container });
|
|
36
|
+
const kit = new MacroKit({ host });
|
|
37
|
+
|
|
38
|
+
// קיצורי מקלדת לכל מה ששמור, והשלמה אוטומטית:
|
|
39
|
+
const unbindKeys = kit.attachShortcuts(container);
|
|
40
|
+
const disableAutoText = kit.enableAutoText();
|
|
41
|
+
|
|
42
|
+
// בהחלפת מסמך / פירוק:
|
|
43
|
+
unbindKeys();
|
|
44
|
+
disableAutoText();
|
|
45
|
+
host.dispose();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 1. מאקרו כתובים (סקריפטים)
|
|
49
|
+
|
|
50
|
+
סקריפט מקבל אובייקט `api` וכל המתודות שלו א-סינכרוניות:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
kit.saveScript({
|
|
54
|
+
name: 'כותרת דבר תורה',
|
|
55
|
+
shortcut: 'Ctrl+Alt+D',
|
|
56
|
+
source: `
|
|
57
|
+
await api.bold();
|
|
58
|
+
await api.insertText('בעניין ');
|
|
59
|
+
const selected = await api.getSelectionText();
|
|
60
|
+
if (selected) await api.insertText(selected);
|
|
61
|
+
await api.bold();
|
|
62
|
+
await api.insertParagraph();
|
|
63
|
+
`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = await kit.runScript(kit.listScripts()[0].id);
|
|
67
|
+
if (!result.ok) console.warn(result.message);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### ה-API שסקריפט מקבל
|
|
71
|
+
|
|
72
|
+
| מתודה | תיאור |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `api.command(id, payload?)` | כל פקודה מקטלוג SuperDoc (`'text-align'`, `'font-size'`…). מחזירה `{ok}` ואינה זורקת |
|
|
75
|
+
| `api.hasCommand(id)` / `api.commandIds()` | בירור יכולות |
|
|
76
|
+
| `api.insertText(text)` / `api.insertParagraph()` | הכנסה במיקום הסמן |
|
|
77
|
+
| `api.deleteBackward(count?)` | מחיקה לאחור |
|
|
78
|
+
| `api.getSelection()` / `api.getSelectionText()` | הבחירה הנוכחית |
|
|
79
|
+
| `api.getDocumentText()` | הטקסט המלא |
|
|
80
|
+
| `api.replaceAll(find, replace)` | החלפה גורפת; מחזירה כמה הוחלפו |
|
|
81
|
+
| `api.bold()` / `italic()` / `underline()` / `bulletList()` / `directionRtl()` … | סוכר לפקודות נפוצות — זורקות בכשל, כדי שהסקריפט ייעצר |
|
|
82
|
+
| `api.log(...)` | יומן ריצה (מגיע ל-`onLog` של ה-Kit) |
|
|
83
|
+
|
|
84
|
+
### אבטחה
|
|
85
|
+
|
|
86
|
+
ברירת המחדל היא **ארגז חול אמיתי**: הסקריפט רץ ב-iframe עם `sandbox="allow-scripts"` בלבד — origin אטום, בלי גישה ל-DOM של האפליקציה, ל-localStorage, ל-cookies או לרשת עם אישורי המשתמש. הדרך היחידה שלו לגעת במסמך היא ה-API שלמעלה, עם תקרת זמן (ברירת מחדל 30 שניות — נאכפת גם על לולאה אינסופית, ע"י הסרת ה-iframe) ותקרת קריאות (10,000).
|
|
87
|
+
|
|
88
|
+
מי שחייב לוותר על הבידוד (למשל CSP שחוסם `srcdoc`) יכול לעבור למריץ ישיר: `new MacroKit({ host, runner: 'eval' })` — ראו האזהרות בקוד.
|
|
89
|
+
|
|
90
|
+
## 2. מקליט מאקרו
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
kit.startRecording();
|
|
94
|
+
// המשתמש עובד רגיל: מקליד, מדגיש, ממספר...
|
|
95
|
+
const recording = kit.stopRecording('פתיח סטנדרטי', 'Ctrl+Alt+1');
|
|
96
|
+
|
|
97
|
+
// מאוחר יותר, מכל מקום במסמך:
|
|
98
|
+
await kit.replayRecording(recording.id);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
המקליט מתעד **פקודות והקלדה**, לא מיקומי סמן — בדיוק כמו המקליט של Word: הניגון חל במקום שבו הסמן עומד. הקשות רצופות מתלכדות לצעד אחד, `undo`/`redo` אינם מוקלטים (ניתן לשינוי ב-`RecorderOptions`), וההקלטה נשמרת כ-JSON נקי שאפשר לייצא ולשתף.
|
|
102
|
+
|
|
103
|
+
## 3. קטעי טקסט והשלמה אוטומטית
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
kit.saveSnippet({ name: 'בס"ד', text: 'בס"ד', trigger: 'בסד' });
|
|
107
|
+
kit.saveSnippet({
|
|
108
|
+
name: 'חתימה',
|
|
109
|
+
text: 'ונשלם בעז"ה, {{date}}',
|
|
110
|
+
shortcut: 'Ctrl+Alt+S',
|
|
111
|
+
});
|
|
112
|
+
kit.saveSnippet({ name: 'ציטוט', text: '(עיין {{selection}})' });
|
|
113
|
+
|
|
114
|
+
kit.enableAutoText(); // מעכשיו: הקלדת "בסד" + רווח ⟵ בס"ד
|
|
115
|
+
await kit.expandSnippet(id); // או הרחבה יזומה / דרך הקיצור
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
משתנים מובנים: `{{date}}`, `{{time}}`, `{{datetime}}` (עברית), `{{selection}}`. כל שם אחר נפתר מ-`variables` שנמסרו ל-`expandSnippet`; משתנה ללא ערך נשאר גלוי בטקסט.
|
|
119
|
+
|
|
120
|
+
## שמירה, ייבוא וייצוא
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { createLocalStorage } from 'superdoc-macros';
|
|
124
|
+
|
|
125
|
+
const kit = new MacroKit({ host, storage: createLocalStorage('my-key') });
|
|
126
|
+
|
|
127
|
+
const json = kit.exportState(); // גיבוי / שיתוף
|
|
128
|
+
kit.importState(json, { merge: true });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`MacroStorage` הוא ממשק בן שתי מתודות — אפשר לממש שמירה לקובץ (למשל ב-workspace של תוסף אוצריא).
|
|
132
|
+
|
|
133
|
+
## חיבור למארח אחר
|
|
134
|
+
|
|
135
|
+
כל הערכה עובדת מול ממשק `MacroHost` אחד (פקודות, הכנסת טקסט, בחירה, החלפה, אירועי הקלדה). `createSuperdocHost` הוא המימוש ל-SuperDoc v2 במצב `ui: false`; עורך אחר מתחבר במימוש משלו של הממשק — ראו `src/types.ts` ואת הכפיל ב-`tests/fake-host.ts`.
|
|
136
|
+
|
|
137
|
+
## מגבלות ידועות
|
|
138
|
+
|
|
139
|
+
- אין הרצת VBA. קובצי `.docm` נפתחים כרגיל אבל המאקרו שבהם אינו מורץ.
|
|
140
|
+
- המקליט אינו מתעד תנועת סמן ובחירה בעכבר (כמו ב-Word — הניגון פועל מהסמן הנוכחי).
|
|
141
|
+
- `deleteBackward` וטקסט-מלא-של-המסמך משתמשים ב-view הפנימי של המנוע (ProseMirror) — זמינים בדפדפן, לא ב-headless.
|
|
142
|
+
- תקרת הזמן במריץ `eval` אינה עוצרת לולאה סינכרונית אינסופית (במריץ ה-iframe — כן).
|
|
143
|
+
|
|
144
|
+
## פיתוח
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm install
|
|
148
|
+
npm test # vitest — 43 בדיקות
|
|
149
|
+
npm run build # tsc ⟵ dist/
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## רישיון
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* מימוש `MacroHost` מעל SuperDoc v2 במצב מנוע-בלבד (`ui: false`) — הקונפיגורציה
|
|
3
|
+
* של otzaria-word-editor.
|
|
4
|
+
*
|
|
5
|
+
* המשטחים שבשימוש, לפי סדר עדיפות:
|
|
6
|
+
* 1. `superdoc.ui.commands` — קטלוג הפקודות של ה-controller (הרצה + תצפית).
|
|
7
|
+
* 2. `superdoc.activeEditor.doc` — ה-Document API הציבורי (בחירה, הכנסה, בלוקים).
|
|
8
|
+
* 3. `superdoc.ui.search` — חיפוש/החלפה.
|
|
9
|
+
* 4. `superdoc.activeEditor.view` — מופע ProseMirror הפנימי, **רק** לפערים
|
|
10
|
+
* שאין להם משטח ציבורי: מחיקה לאחור וטקסט מלא של המסמך. קיים בדפדפן
|
|
11
|
+
* ו-null ב-headless.
|
|
12
|
+
*
|
|
13
|
+
* הטיפוסים כאן מבניים (structural) ואינם מייבאים מ-superdoc: הערכה לא תלויה
|
|
14
|
+
* בחבילה, וגרסת מנוע שמשנה שדה תיכשל סגור (הפונקציה תחזיר כשל) ולא תקרוס.
|
|
15
|
+
*
|
|
16
|
+
* תצפית הפקודות למקליט נעשית בעטיפת `executeAsync` על אובייקט ה-commands.
|
|
17
|
+
* זה מכסה כל מסלול שקורא לו — כולל ה-CommandAdapter של otzaria — בלי לשנות
|
|
18
|
+
* את הקוד הקורא. `dispose()` מחזיר את המתודה המקורית.
|
|
19
|
+
*/
|
|
20
|
+
import type { MacroHost } from '../types.js';
|
|
21
|
+
interface CommandStateLike {
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
interface CommandsLike {
|
|
25
|
+
has(id: string): boolean;
|
|
26
|
+
ids?: readonly string[] | (() => readonly string[]);
|
|
27
|
+
get(id: string): {
|
|
28
|
+
getState(): CommandStateLike;
|
|
29
|
+
};
|
|
30
|
+
executeAsync(id: string, payload?: unknown): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
interface SearchSliceLike {
|
|
33
|
+
available?: boolean;
|
|
34
|
+
total?: number;
|
|
35
|
+
query?: string;
|
|
36
|
+
}
|
|
37
|
+
interface SearchHandleLike {
|
|
38
|
+
getSnapshot(): SearchSliceLike;
|
|
39
|
+
search(query: string): SearchSliceLike;
|
|
40
|
+
clear?(): void;
|
|
41
|
+
open?(): unknown;
|
|
42
|
+
close?(): unknown;
|
|
43
|
+
replaceAll(replacement: string): Promise<{
|
|
44
|
+
ok?: boolean;
|
|
45
|
+
reason?: string;
|
|
46
|
+
} | undefined> | {
|
|
47
|
+
ok?: boolean;
|
|
48
|
+
reason?: string;
|
|
49
|
+
} | undefined;
|
|
50
|
+
}
|
|
51
|
+
interface DocReceiptLike {
|
|
52
|
+
success?: boolean;
|
|
53
|
+
failure?: {
|
|
54
|
+
code?: string;
|
|
55
|
+
message?: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
interface SelectionInfoLike {
|
|
59
|
+
empty?: boolean;
|
|
60
|
+
text?: string;
|
|
61
|
+
target?: {
|
|
62
|
+
segments?: readonly {
|
|
63
|
+
blockId?: string;
|
|
64
|
+
range?: {
|
|
65
|
+
start?: number;
|
|
66
|
+
end?: number;
|
|
67
|
+
};
|
|
68
|
+
}[];
|
|
69
|
+
} | null;
|
|
70
|
+
selectionTarget?: unknown;
|
|
71
|
+
}
|
|
72
|
+
interface DocLike {
|
|
73
|
+
insert?(input: {
|
|
74
|
+
value: string;
|
|
75
|
+
type: 'text';
|
|
76
|
+
target?: unknown;
|
|
77
|
+
}): Promise<DocReceiptLike | undefined> | DocReceiptLike | undefined;
|
|
78
|
+
selection?: {
|
|
79
|
+
current?(input?: {
|
|
80
|
+
includeText?: boolean;
|
|
81
|
+
}): Promise<SelectionInfoLike | undefined> | SelectionInfoLike | undefined;
|
|
82
|
+
};
|
|
83
|
+
blocks?: {
|
|
84
|
+
list?(input?: {
|
|
85
|
+
offset?: number;
|
|
86
|
+
limit?: number;
|
|
87
|
+
}): Promise<unknown> | unknown;
|
|
88
|
+
} | null;
|
|
89
|
+
}
|
|
90
|
+
interface ProseMirrorViewLike {
|
|
91
|
+
state: {
|
|
92
|
+
doc: {
|
|
93
|
+
content: {
|
|
94
|
+
size: number;
|
|
95
|
+
};
|
|
96
|
+
textBetween(from: number, to: number, blockSeparator?: string): string;
|
|
97
|
+
};
|
|
98
|
+
selection: {
|
|
99
|
+
from: number;
|
|
100
|
+
empty: boolean;
|
|
101
|
+
};
|
|
102
|
+
tr: {
|
|
103
|
+
delete(from: number, to: number): unknown;
|
|
104
|
+
insertText(text: string, from?: number, to?: number): unknown;
|
|
105
|
+
scrollIntoView(): unknown;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
dispatch(tr: unknown): void;
|
|
109
|
+
}
|
|
110
|
+
export interface SuperdocLike {
|
|
111
|
+
ui?: {
|
|
112
|
+
commands?: CommandsLike;
|
|
113
|
+
search?: SearchHandleLike;
|
|
114
|
+
} | null;
|
|
115
|
+
activeEditor?: {
|
|
116
|
+
doc?: DocLike | null;
|
|
117
|
+
view?: ProseMirrorViewLike | null;
|
|
118
|
+
} | null;
|
|
119
|
+
}
|
|
120
|
+
export interface SuperdocHostOptions {
|
|
121
|
+
superdoc: SuperdocLike;
|
|
122
|
+
/** האלמנט שהמסמך מרונדר בתוכו — עליו נקלטים אירועי ההקלדה. */
|
|
123
|
+
container?: HTMLElement | null;
|
|
124
|
+
}
|
|
125
|
+
export interface SuperdocMacroHost extends MacroHost {
|
|
126
|
+
/** מסירה את עטיפת התצפית ואת מאזיני ה-DOM. לקרוא לפני החלפת מסמך. */
|
|
127
|
+
dispose(): void;
|
|
128
|
+
}
|
|
129
|
+
export declare function createSuperdocHost(options: SuperdocHostOptions): SuperdocMacroHost;
|
|
130
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/* ---------- עזרים ---------- */
|
|
2
|
+
const NOT_READY = { ok: false, message: 'אין מסמך פתוח', reason: 'not-ready' };
|
|
3
|
+
function failed(message, reason) {
|
|
4
|
+
return { ok: false, message, reason };
|
|
5
|
+
}
|
|
6
|
+
function receiptOutcome(receipt, failedAction) {
|
|
7
|
+
if (receipt && receipt.success === false) {
|
|
8
|
+
const code = receipt.failure?.code;
|
|
9
|
+
if (code === 'NO_OP')
|
|
10
|
+
return { ok: true };
|
|
11
|
+
const detail = receipt.failure?.message;
|
|
12
|
+
return failed(detail ? `${failedAction}: ${detail}` : `${failedAction}${code ? ` (${code})` : ''}`, code);
|
|
13
|
+
}
|
|
14
|
+
return { ok: true };
|
|
15
|
+
}
|
|
16
|
+
function emptySelection() {
|
|
17
|
+
return { text: '', hasRange: false, blockId: null, selectionTarget: null, empty: true };
|
|
18
|
+
}
|
|
19
|
+
/* ---------- המימוש ---------- */
|
|
20
|
+
export function createSuperdocHost(options) {
|
|
21
|
+
const { superdoc, container } = options;
|
|
22
|
+
// נקראים ברגע השימוש ולא נשמרים: activeEditor מוחלף בכל פתיחת מסמך.
|
|
23
|
+
const commands = () => superdoc.ui?.commands ?? null;
|
|
24
|
+
const doc = () => superdoc.activeEditor?.doc ?? null;
|
|
25
|
+
const view = () => superdoc.activeEditor?.view ?? null;
|
|
26
|
+
const search = () => superdoc.ui?.search ?? null;
|
|
27
|
+
const commandListeners = new Set();
|
|
28
|
+
const inputListeners = new Set();
|
|
29
|
+
/* תצפית פקודות: עטיפת executeAsync, פעם אחת, עם שחזור ב-dispose. */
|
|
30
|
+
const wrapped = commands();
|
|
31
|
+
const originalExecuteAsync = wrapped?.executeAsync;
|
|
32
|
+
if (wrapped && originalExecuteAsync) {
|
|
33
|
+
wrapped.executeAsync = function (id, payload) {
|
|
34
|
+
for (const listener of commandListeners) {
|
|
35
|
+
try {
|
|
36
|
+
listener(id, payload);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.warn('[superdoc-macros] מאזין פקודות זרק', error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return originalExecuteAsync.call(wrapped, id, payload);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/* הקלדה: beforeinput על ה-container, בשלב הלכידה. */
|
|
46
|
+
const onBeforeInput = (event) => {
|
|
47
|
+
const input = event;
|
|
48
|
+
let mapped = null;
|
|
49
|
+
switch (input.inputType) {
|
|
50
|
+
case 'insertText':
|
|
51
|
+
case 'insertCompositionText':
|
|
52
|
+
if (typeof input.data === 'string' && input.data.length > 0) {
|
|
53
|
+
mapped = { kind: 'insert-text', text: input.data };
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case 'insertParagraph':
|
|
57
|
+
mapped = { kind: 'insert-paragraph' };
|
|
58
|
+
break;
|
|
59
|
+
case 'deleteContentBackward':
|
|
60
|
+
mapped = { kind: 'delete-backward' };
|
|
61
|
+
break;
|
|
62
|
+
case 'deleteContentForward':
|
|
63
|
+
mapped = { kind: 'delete-forward' };
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (!mapped)
|
|
69
|
+
return;
|
|
70
|
+
for (const listener of inputListeners) {
|
|
71
|
+
try {
|
|
72
|
+
listener(mapped);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.warn('[superdoc-macros] מאזין הקלדה זרק', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
container?.addEventListener('beforeinput', onBeforeInput, true);
|
|
80
|
+
async function readSelection(includeText) {
|
|
81
|
+
const current = doc()?.selection?.current;
|
|
82
|
+
if (typeof current !== 'function')
|
|
83
|
+
return emptySelection();
|
|
84
|
+
let info;
|
|
85
|
+
try {
|
|
86
|
+
info = await current(includeText ? { includeText: true } : undefined);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return emptySelection();
|
|
90
|
+
}
|
|
91
|
+
if (!info || typeof info !== 'object')
|
|
92
|
+
return emptySelection();
|
|
93
|
+
const segments = Array.isArray(info.target?.segments) ? info.target.segments : [];
|
|
94
|
+
const first = segments.find((segment) => typeof segment?.blockId === 'string');
|
|
95
|
+
const hasRange = segments.some((segment) => typeof segment?.blockId === 'string' &&
|
|
96
|
+
typeof segment.range?.start === 'number' &&
|
|
97
|
+
typeof segment.range?.end === 'number' &&
|
|
98
|
+
segment.range.start !== segment.range.end);
|
|
99
|
+
const text = typeof info.text === 'string' ? info.text : '';
|
|
100
|
+
return {
|
|
101
|
+
text,
|
|
102
|
+
hasRange,
|
|
103
|
+
blockId: first?.blockId ?? null,
|
|
104
|
+
selectionTarget: info.selectionTarget !== null && typeof info.selectionTarget === 'object' ? info.selectionTarget : null,
|
|
105
|
+
empty: typeof info.empty === 'boolean' ? info.empty : !(hasRange || text.length > 0),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
commands: {
|
|
110
|
+
has(id) {
|
|
111
|
+
return commands()?.has(id) ?? false;
|
|
112
|
+
},
|
|
113
|
+
ids() {
|
|
114
|
+
const raw = commands()?.ids;
|
|
115
|
+
if (typeof raw === 'function')
|
|
116
|
+
return raw();
|
|
117
|
+
return Array.isArray(raw) ? raw : [];
|
|
118
|
+
},
|
|
119
|
+
async execute(id, payload) {
|
|
120
|
+
const bus = commands();
|
|
121
|
+
if (!bus)
|
|
122
|
+
return NOT_READY;
|
|
123
|
+
if (!bus.has(id))
|
|
124
|
+
return failed(`הפקודה ${id} אינה מוכרת למנוע`, 'unknown-command');
|
|
125
|
+
let result;
|
|
126
|
+
try {
|
|
127
|
+
result = await bus.executeAsync(id, payload);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return failed(error instanceof Error ? error.message : 'הפעולה נכשלה', 'threw');
|
|
131
|
+
}
|
|
132
|
+
// false = ה-controller לא ניתב את הפקודה; מצב הפקד מסביר למה.
|
|
133
|
+
if (result === false) {
|
|
134
|
+
const reason = bus.get(id).getState().reason;
|
|
135
|
+
return failed(reason ? `הפעולה נכשלה (${reason})` : 'הפעולה נכשלה', reason);
|
|
136
|
+
}
|
|
137
|
+
if (typeof result === 'object' && result !== null) {
|
|
138
|
+
return receiptOutcome(result, `הפקודה ${id} נכשלה`);
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
async insertText(text) {
|
|
144
|
+
const insert = doc()?.insert;
|
|
145
|
+
if (typeof insert === 'function') {
|
|
146
|
+
// בלי target ההכנסה נופלת לסוף המסמך — לכן היעד נלקח מהבחירה החיה.
|
|
147
|
+
const snapshot = await readSelection(false);
|
|
148
|
+
try {
|
|
149
|
+
const receipt = await insert({
|
|
150
|
+
value: text,
|
|
151
|
+
type: 'text',
|
|
152
|
+
...(snapshot.selectionTarget ? { target: snapshot.selectionTarget } : {}),
|
|
153
|
+
});
|
|
154
|
+
return receiptOutcome(receipt, 'הכנסת הטקסט נכשלה');
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return failed(error instanceof Error ? error.message : 'הכנסת הטקסט נכשלה', 'threw');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// נפילה לאחור: ProseMirror ישיר, כשה-Document API אינו זמין.
|
|
161
|
+
const pm = view();
|
|
162
|
+
if (pm) {
|
|
163
|
+
try {
|
|
164
|
+
const tr = pm.state.tr;
|
|
165
|
+
tr.insertText(text);
|
|
166
|
+
tr.scrollIntoView();
|
|
167
|
+
pm.dispatch(tr);
|
|
168
|
+
return { ok: true };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return failed(error instanceof Error ? error.message : 'הכנסת הטקסט נכשלה', 'threw');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return NOT_READY;
|
|
175
|
+
},
|
|
176
|
+
async deleteBackward(count) {
|
|
177
|
+
// אין משטח ציבורי למחיקה — זה השימוש המרכזי ב-escape hatch של ProseMirror.
|
|
178
|
+
const pm = view();
|
|
179
|
+
if (!pm)
|
|
180
|
+
return failed('מחיקה אינה זמינה במסמך הזה', 'view-unavailable');
|
|
181
|
+
try {
|
|
182
|
+
const { from } = pm.state.selection;
|
|
183
|
+
const start = Math.max(0, from - Math.max(0, Math.trunc(count)));
|
|
184
|
+
if (start === from)
|
|
185
|
+
return { ok: true };
|
|
186
|
+
const tr = pm.state.tr;
|
|
187
|
+
tr.delete(start, from);
|
|
188
|
+
pm.dispatch(tr);
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return failed(error instanceof Error ? error.message : 'המחיקה נכשלה', 'threw');
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
getSelection(options) {
|
|
196
|
+
return readSelection(options?.includeText ?? false);
|
|
197
|
+
},
|
|
198
|
+
async replaceAll(query, replacement) {
|
|
199
|
+
const handle = search();
|
|
200
|
+
if (!handle)
|
|
201
|
+
return { ok: false, replaced: 0, message: 'החיפוש אינו זמין' };
|
|
202
|
+
try {
|
|
203
|
+
handle.open?.();
|
|
204
|
+
const slice = handle.search(query);
|
|
205
|
+
if (slice?.available === false) {
|
|
206
|
+
return { ok: false, replaced: 0, message: 'החיפוש אינו זמין במסמך הזה' };
|
|
207
|
+
}
|
|
208
|
+
const total = typeof slice?.total === 'number' ? slice.total : 0;
|
|
209
|
+
if (total === 0)
|
|
210
|
+
return { ok: true, replaced: 0 };
|
|
211
|
+
const result = await handle.replaceAll(replacement);
|
|
212
|
+
if (result && result.ok === false) {
|
|
213
|
+
return { ok: false, replaced: 0, message: `ההחלפה נכשלה${result.reason ? ` (${result.reason})` : ''}` };
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, replaced: total };
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return { ok: false, replaced: 0, message: error instanceof Error ? error.message : 'ההחלפה נכשלה' };
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
try {
|
|
222
|
+
handle.clear?.();
|
|
223
|
+
handle.close?.();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* ניקוי בלבד */
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
async getDocumentText() {
|
|
231
|
+
const pm = view();
|
|
232
|
+
if (!pm)
|
|
233
|
+
return '';
|
|
234
|
+
try {
|
|
235
|
+
return pm.state.doc.textBetween(0, pm.state.doc.content.size, '\n');
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
onCommand(listener) {
|
|
242
|
+
commandListeners.add(listener);
|
|
243
|
+
return () => commandListeners.delete(listener);
|
|
244
|
+
},
|
|
245
|
+
onTextInput(listener) {
|
|
246
|
+
inputListeners.add(listener);
|
|
247
|
+
return () => inputListeners.delete(listener);
|
|
248
|
+
},
|
|
249
|
+
dispose() {
|
|
250
|
+
if (wrapped && originalExecuteAsync)
|
|
251
|
+
wrapped.executeAsync = originalExecuteAsync;
|
|
252
|
+
container?.removeEventListener('beforeinput', onBeforeInput, true);
|
|
253
|
+
commandListeners.clear();
|
|
254
|
+
inputListeners.clear();
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { MacroHost, MacroOutcome, MacroStep, RecordedMacro, SavedScript, SelectionSnapshot, Snippet, TextInputEvent, } from './types.js';
|
|
2
|
+
export { MacroKit, type MacroKitOptions } from './manager.js';
|
|
3
|
+
export { createSuperdocHost, type SuperdocHostOptions, type SuperdocLike, type SuperdocMacroHost } from './host/superdoc-host.js';
|
|
4
|
+
export { createMacroApi, MacroError, type MacroApi, type MacroBridge, type ScriptSelection } from './scripting/macro-api.js';
|
|
5
|
+
export { createEvalRunner } from './scripting/eval-runner.js';
|
|
6
|
+
export { createIframeRunner, SANDBOX_BOOTSTRAP, isProtocolMessage } from './scripting/iframe-runner.js';
|
|
7
|
+
export type { MacroRunner, MacroRunOptions, MacroRunResult } from './scripting/runner.js';
|
|
8
|
+
export { MacroRecorder, replayMacro, type RecorderOptions, type ReplayOptions, type ReplayResult } from './recorder/recorder.js';
|
|
9
|
+
export { renderSnippet, expandSnippet, usesSelection, type ExpandOptions, type RenderContext } from './snippets/snippets.js';
|
|
10
|
+
export { AutoText, type AutoTextOptions } from './snippets/autotext.js';
|
|
11
|
+
export { parseShortcut, eventMatches, bindShortcuts, type ParsedShortcut, type ShortcutBinding, type ShortcutTarget } from './shortcuts.js';
|
|
12
|
+
export { createLocalStorage, createMemoryStorage, emptyState, parsePersistedState, DEFAULT_STORAGE_KEY, type MacroStorage, type PersistedMacroState, } from './storage.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { MacroKit } from './manager.js';
|
|
2
|
+
export { createSuperdocHost } from './host/superdoc-host.js';
|
|
3
|
+
export { createMacroApi, MacroError } from './scripting/macro-api.js';
|
|
4
|
+
export { createEvalRunner } from './scripting/eval-runner.js';
|
|
5
|
+
export { createIframeRunner, SANDBOX_BOOTSTRAP, isProtocolMessage } from './scripting/iframe-runner.js';
|
|
6
|
+
export { MacroRecorder, replayMacro } from './recorder/recorder.js';
|
|
7
|
+
export { renderSnippet, expandSnippet, usesSelection } from './snippets/snippets.js';
|
|
8
|
+
export { AutoText } from './snippets/autotext.js';
|
|
9
|
+
export { parseShortcut, eventMatches, bindShortcuts } from './shortcuts.js';
|
|
10
|
+
export { createLocalStorage, createMemoryStorage, emptyState, parsePersistedState, DEFAULT_STORAGE_KEY, } from './storage.js';
|