superdoc-macros 0.8.0 → 0.9.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A macro toolkit for **SuperDoc v2**-based editors. Originally built for [otzaria-word-editor](https://github.com/Y-PLONI/otzaria-word-editor), but fully generic: it has no dependency on that project — nor on the superdoc package itself (the engine is consumed structurally, through its public surfaces), and the core works against any editor that implements a small `MacroHost` interface.
4
4
 
5
- Three capabilities, in the spirit of Word macros:
5
+ The capabilities, in the spirit of Word macros:
6
6
 
7
7
  | Capability | What it gives you |
8
8
  | --- | --- |
@@ -10,10 +10,11 @@ Three capabilities, in the spirit of Word macros:
10
10
  | **Macro recorder** | "Record → work normally → stop → replay" — records commands and typing, like Word's recorder |
11
11
  | **Snippets (AutoText)** | Templates with variables (`{{date}}`, `{{selection}}`…), keyboard shortcuts, and auto-expansion while typing (type a trigger word + space) |
12
12
  | **VBA import (read-only)** | Reads the macros already inside a `.docm` and shows the user their real source, so they can port it. Nothing is executed |
13
+ | **Built-in tools** | Host-implemented document actions registered on the kit — listed, run and shortcut-bound alongside everything else |
13
14
 
14
15
  Plus: persistence (localStorage or custom storage), JSON import/export, keyboard shortcut binding, and localizable runtime messages (English by default, Hebrew locale included).
15
16
 
16
- > **A note on VBA:** the toolkit does not *execute* VBA — there is no VBA engine in the browser, and running document-supplied code is not something it will grow into. What it does instead is two things: it lets a user **see** the macros a `.docm` already contains (section 4), and it provides a parallel, JavaScript-based macro system to rewrite them in.
17
+ > **A note on VBA:** the toolkit does not *execute* VBA — there is no VBA engine in the browser, and running document-supplied code is not something it will grow into. What it does instead is two things: it lets a user **see** the macros a `.docm` already contains (section 5), and it provides a parallel, JavaScript-based macro system to rewrite them in.
17
18
 
18
19
  ## Installation
19
20
 
@@ -119,7 +120,26 @@ await kit.expandSnippet(id); // or expand explicitly / via the shortcut
119
120
 
120
121
  Built-in variables: `{{date}}`, `{{time}}`, `{{datetime}}` (formatted with the browser locale, or an explicit `locale` option), `{{selection}}`. Any other name resolves from the `variables` passed to `expandSnippet`; a variable with no value stays visible in the text.
121
122
 
122
- ## 4. Reading the VBA in an existing `.docm`
123
+ ## 4. Built-in tools
124
+
125
+ A host often ships native document-processing actions of its own — implemented against its full engine access, beyond what the sandboxed script API exposes. Registering them on the kit puts them next to recordings and scripts: one management UI, one run-at-a-time guard, and one shortcut system with persistence and collision rules.
126
+
127
+ ```ts
128
+ kit.registerTool({
129
+ id: 'typography.first-word',
130
+ name: 'Format first word',
131
+ description: 'Enlarges the first word of every selected paragraph',
132
+ run: () => applyFirstWordDesign(editor), // host code, returns { ok } / { ok: false, message }
133
+ });
134
+
135
+ kit.listTools(); // [{ id, name, description, shortcut? }]
136
+ await kit.runTool('typography.first-word'); // refused while recording or while another macro runs
137
+ kit.setToolShortcut('typography.first-word', 'Ctrl+Alt+1'); // persisted; validated like every binding
138
+ ```
139
+
140
+ Tools are runtime registrations — they are never persisted or exported. Only their shortcuts are, keyed by the tool id, so a shortcut survives restarts and waits for the day its tool is registered again; an unregistered tool's shortcut is never bound.
141
+
142
+ ## 5. Reading the VBA in an existing `.docm`
123
143
 
124
144
  A user who has relied on a macro-enabled document for years should not be told their macros are simply gone. This reads the macro project out of the package and hands back each module's real source text:
125
145
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { MacroHost, MacroOutcome, MacroStep, RecordedMacro, SavedScript, SelectionSnapshot, Snippet, TextInputEvent, } from './types.js';
1
+ export type { BuiltinTool, BuiltinToolInfo, MacroHost, MacroOutcome, MacroStep, RecordedMacro, SavedScript, SelectionSnapshot, Snippet, TextInputEvent, } from './types.js';
2
2
  export { MacroKit, type MacroKitOptions, type ShortcutValidation } from './manager.js';
3
3
  export { ENGLISH_MESSAGES, HEBREW_MESSAGES, setMacroMessages, type MacroMessages, } from './messages.js';
4
4
  export { createSuperdocHost, type SuperdocHostOptions, type SuperdocLike, type SuperdocMacroHost } from './host/superdoc-host.js';
package/dist/manager.d.ts CHANGED
@@ -14,7 +14,7 @@ import { type AutoTextOptions } from './snippets/autotext.js';
14
14
  import { type ExpandOptions } from './snippets/snippets.js';
15
15
  import { type ShortcutTarget } from './shortcuts.js';
16
16
  import { type MacroStorage } from './storage.js';
17
- import type { MacroHost, RecordedMacro, SavedScript, Snippet } from './types.js';
17
+ import type { BuiltinTool, BuiltinToolInfo, MacroHost, MacroOutcome, RecordedMacro, SavedScript, Snippet } from './types.js';
18
18
  export interface MacroKitOptions {
19
19
  host: MacroHost;
20
20
  /** Default: localStorage. */
@@ -66,6 +66,8 @@ export declare class MacroKit {
66
66
  private readonly runOptions;
67
67
  private readonly onLog?;
68
68
  private state;
69
+ /** Built-in tools, by id. Runtime registrations — never persisted (see BuiltinTool). */
70
+ private readonly tools;
69
71
  private readonly recorder;
70
72
  private readonly autoText;
71
73
  private readonly reservedSignatures;
@@ -160,6 +162,25 @@ export declare class MacroKit {
160
162
  /** Enables auto-text (trigger + space). Returns a disable function. */
161
163
  enableAutoText(): () => void;
162
164
  disableAutoText(): void;
165
+ /**
166
+ * Registers a built-in tool (see BuiltinTool in types.ts). Throws on a
167
+ * duplicate id or an invalid name — a silent replace would let two host
168
+ * modules fight over one id without anyone noticing.
169
+ */
170
+ registerTool(tool: BuiltinTool): void;
171
+ /** The registered tools, each with its persisted shortcut (if any). */
172
+ listTools(): readonly BuiltinToolInfo[];
173
+ /**
174
+ * Runs a registered tool under the same guard as scripts and replays: not
175
+ * while recording, and never two runs at once. A thrown error is reported
176
+ * as a failed outcome, never rethrown.
177
+ */
178
+ runTool(id: string): Promise<MacroOutcome>;
179
+ /**
180
+ * Sets or clears (undefined/empty) the persisted shortcut of a registered
181
+ * tool, under the exact validation of every other saved binding.
182
+ */
183
+ setToolShortcut(id: string, shortcut: string | undefined): void;
163
184
  /**
164
185
  * Binds the shortcuts of everything saved (scripts, recordings, snippets)
165
186
  * to a target — usually the editor container or `window`. The list is
package/dist/manager.js CHANGED
@@ -35,6 +35,8 @@ export class MacroKit {
35
35
  runOptions;
36
36
  onLog;
37
37
  state;
38
+ /** Built-in tools, by id. Runtime registrations — never persisted (see BuiltinTool). */
39
+ tools = new Map();
38
40
  recorder;
39
41
  autoText;
40
42
  reservedSignatures;
@@ -142,6 +144,22 @@ export class MacroKit {
142
144
  for (const signature of signatures)
143
145
  seen.add(signature);
144
146
  }
147
+ const toolShortcuts = this.state.toolShortcuts;
148
+ if (!toolShortcuts)
149
+ return;
150
+ for (const [toolId, shortcut] of Object.entries(toolShortcuts)) {
151
+ if (this.bindingIssue(shortcut)) {
152
+ delete toolShortcuts[toolId];
153
+ continue;
154
+ }
155
+ const signatures = shortcutSignatures(parseShortcut(shortcut));
156
+ if (signatures.some((signature) => seen.has(signature))) {
157
+ delete toolShortcuts[toolId];
158
+ continue;
159
+ }
160
+ for (const signature of signatures)
161
+ seen.add(signature);
162
+ }
145
163
  }
146
164
  findShortcutOwner(signatures, excludeId) {
147
165
  const items = [
@@ -159,6 +177,19 @@ export class MacroKit {
159
177
  if (existing.some((signature) => signatures.includes(signature)))
160
178
  return item.name;
161
179
  }
180
+ for (const [toolId, shortcut] of Object.entries(this.state.toolShortcuts ?? {})) {
181
+ if (toolId === excludeId)
182
+ continue;
183
+ const parsed = parseShortcut(shortcut);
184
+ if (!parsed)
185
+ continue;
186
+ const existing = shortcutSignatures(parsed);
187
+ if (existing.some((signature) => signatures.includes(signature))) {
188
+ // A stored shortcut can belong to a tool that is not registered in
189
+ // this session; its id is then the only name there is.
190
+ return this.tools.get(toolId)?.name ?? toolId;
191
+ }
192
+ }
162
193
  return null;
163
194
  }
164
195
  /** Throws a MacroError when the shortcut is unacceptable. The save paths call this. */
@@ -399,6 +430,80 @@ export class MacroKit {
399
430
  disableAutoText() {
400
431
  this.autoText.detach();
401
432
  }
433
+ /* ---------- Built-in tools ---------- */
434
+ /**
435
+ * Registers a built-in tool (see BuiltinTool in types.ts). Throws on a
436
+ * duplicate id or an invalid name — a silent replace would let two host
437
+ * modules fight over one id without anyone noticing.
438
+ */
439
+ registerTool(tool) {
440
+ if (!tool.id || tool.id.length > IMPORT_LIMITS.maxNameLength) {
441
+ throw new MacroError(macroMessages().fieldTooLong('id', IMPORT_LIMITS.maxNameLength), 'invalid-item');
442
+ }
443
+ this.requireItemLimits({ name: tool.name });
444
+ if (this.tools.has(tool.id)) {
445
+ const messages = macroMessages();
446
+ throw new MacroError(messages.toolAlreadyRegistered?.(tool.id) ?? messages.saveFailed, 'invalid-item');
447
+ }
448
+ this.tools.set(tool.id, tool);
449
+ }
450
+ /** The registered tools, each with its persisted shortcut (if any). */
451
+ listTools() {
452
+ return [...this.tools.values()].map((tool) => ({
453
+ id: tool.id,
454
+ name: tool.name,
455
+ ...(tool.description ? { description: tool.description } : {}),
456
+ ...(this.state.toolShortcuts?.[tool.id] ? { shortcut: this.state.toolShortcuts[tool.id] } : {}),
457
+ }));
458
+ }
459
+ /**
460
+ * Runs a registered tool under the same guard as scripts and replays: not
461
+ * while recording, and never two runs at once. A thrown error is reported
462
+ * as a failed outcome, never rethrown.
463
+ */
464
+ async runTool(id) {
465
+ const tool = this.tools.get(id);
466
+ if (!tool) {
467
+ const messages = macroMessages();
468
+ return { ok: false, message: messages.toolNotFound ?? messages.actionFailed, reason: 'tool-not-found' };
469
+ }
470
+ const guard = this.guardRun();
471
+ if (guard)
472
+ return { ok: false, message: guard.message };
473
+ this.running = true;
474
+ try {
475
+ return await tool.run();
476
+ }
477
+ catch (error) {
478
+ return { ok: false, message: error instanceof Error ? error.message : macroMessages().actionFailed, reason: 'threw' };
479
+ }
480
+ finally {
481
+ this.running = false;
482
+ }
483
+ }
484
+ /**
485
+ * Sets or clears (undefined/empty) the persisted shortcut of a registered
486
+ * tool, under the exact validation of every other saved binding.
487
+ */
488
+ setToolShortcut(id, shortcut) {
489
+ if (!this.tools.has(id)) {
490
+ const messages = macroMessages();
491
+ throw new MacroError(messages.toolNotFound ?? messages.actionFailed, 'tool-not-found');
492
+ }
493
+ const trimmed = shortcut?.trim();
494
+ if (trimmed) {
495
+ this.requireValidShortcut(trimmed, id);
496
+ this.requireItemLimits({ name: this.tools.get(id).name, shortcut: trimmed });
497
+ }
498
+ this.commit((draft) => {
499
+ const map = draft.toolShortcuts ?? {};
500
+ if (trimmed)
501
+ map[id] = trimmed;
502
+ else
503
+ delete map[id];
504
+ draft.toolShortcuts = map;
505
+ });
506
+ }
402
507
  /* ---------- Keyboard shortcuts ---------- */
403
508
  /**
404
509
  * Binds the shortcuts of everything saved (scripts, recordings, snippets)
@@ -428,6 +533,12 @@ export class MacroKit {
428
533
  if (snippet.shortcut)
429
534
  bindings.push({ shortcut: snippet.shortcut, run: () => this.expandSnippet(snippet.id) });
430
535
  }
536
+ for (const [toolId, shortcut] of Object.entries(this.state.toolShortcuts ?? {})) {
537
+ // Only registered tools bind: a stored shortcut of a tool the host did
538
+ // not register this session must not swallow the key.
539
+ if (this.tools.has(toolId))
540
+ bindings.push({ shortcut, run: () => this.runTool(toolId) });
541
+ }
431
542
  return bindings;
432
543
  }
433
544
  /* ---------- Import/export ---------- */
@@ -456,6 +567,9 @@ export class MacroKit {
456
567
  scripts: [...this.state.scripts.map((item) => ({ ...item }))],
457
568
  recordings: [...this.state.recordings.map((item) => ({ ...item }))],
458
569
  snippets: [...this.state.snippets.map((item) => ({ ...item }))],
570
+ ...(this.state.toolShortcuts || imported.toolShortcuts
571
+ ? { toolShortcuts: { ...this.state.toolShortcuts, ...imported.toolShortcuts } }
572
+ : {}),
459
573
  };
460
574
  for (const script of imported.scripts)
461
575
  this.upsert(candidate.scripts, script);
@@ -493,6 +607,10 @@ export class MacroKit {
493
607
  ...candidate.scripts,
494
608
  ...candidate.recordings,
495
609
  ...candidate.snippets,
610
+ ...Object.entries(candidate.toolShortcuts ?? {}).map(([toolId, shortcut]) => ({
611
+ name: this.tools.get(toolId)?.name ?? toolId,
612
+ shortcut,
613
+ })),
496
614
  ];
497
615
  for (const item of items) {
498
616
  if (!item.shortcut)
@@ -49,6 +49,12 @@ export interface MacroMessages {
49
49
  * objects compiled against 0.7.0.
50
50
  */
51
51
  recordingUncapturable?: (commandIds: string) => string;
52
+ /**
53
+ * Built-in tools. Optional for source compatibility with full locale
54
+ * objects compiled against 0.8.0 (same reasoning as recordingUncapturable).
55
+ */
56
+ toolNotFound?: string;
57
+ toolAlreadyRegistered?: (id: string) => string;
52
58
  shortcutInvalid: string;
53
59
  shortcutNeedsModifier: string;
54
60
  shortcutReserved: string;
package/dist/messages.js CHANGED
@@ -44,6 +44,8 @@ export const ENGLISH_MESSAGES = {
44
44
  recordingTooLarge: 'The recording is too large to save',
45
45
  recordingIncomplete: (commandIds) => `The recording is missing actions that cannot be recorded (${commandIds})`,
46
46
  recordingUncapturable: (commandIds) => `The recording contains only actions that cannot be recorded (${commandIds})`,
47
+ toolNotFound: 'Tool not found',
48
+ toolAlreadyRegistered: (id) => `A tool with the id "${id}" is already registered`,
47
49
  shortcutInvalid: 'Invalid shortcut — use a form like Ctrl+Alt+M',
48
50
  shortcutNeedsModifier: 'A shortcut must include Ctrl, Alt or Meta',
49
51
  shortcutReserved: 'This shortcut is reserved by the editor',
@@ -85,6 +87,8 @@ export const HEBREW_MESSAGES = {
85
87
  recordingTooLarge: 'ההקלטה גדולה מכדי להישמר',
86
88
  recordingIncomplete: (commandIds) => `בהקלטה חסרות פעולות שאינן ניתנות להקלטה (${commandIds})`,
87
89
  recordingUncapturable: (commandIds) => `ההקלטה מכילה רק פעולות שאינן ניתנות להקלטה (${commandIds})`,
90
+ toolNotFound: 'הכלי לא נמצא',
91
+ toolAlreadyRegistered: (id) => `כלי עם המזהה "${id}" כבר רשום`,
88
92
  shortcutInvalid: 'קיצור לא תקין — הצורה הנדרשת היא למשל Ctrl+Alt+M',
89
93
  shortcutNeedsModifier: 'קיצור חייב לכלול Ctrl,‏ Alt או Meta',
90
94
  shortcutReserved: 'הקיצור הזה שמור לעורך',
package/dist/storage.d.ts CHANGED
@@ -9,6 +9,12 @@ export interface PersistedMacroState {
9
9
  scripts: SavedScript[];
10
10
  recordings: RecordedMacro[];
11
11
  snippets: Snippet[];
12
+ /**
13
+ * Keyboard shortcuts of built-in tools, keyed by tool id. Optional for
14
+ * backward compatibility: stores written before tools existed lack it,
15
+ * and they must keep loading as-is.
16
+ */
17
+ toolShortcuts?: Record<string, string>;
12
18
  }
13
19
  export interface MacroStorage {
14
20
  /** `null` when there is no saved state or the saved state is unreadable. */
package/dist/storage.js CHANGED
@@ -90,11 +90,24 @@ function isValidSnippet(value) {
90
90
  optionalBoundedString(snippet.trigger, IMPORT_LIMITS.maxTriggerLength) &&
91
91
  optionalBoundedString(snippet.shortcut, IMPORT_LIMITS.maxShortcutLength));
92
92
  }
93
+ /** Tool-shortcut map: bounded ids to bounded shortcut strings, capped like the item lists. */
94
+ function isValidToolShortcuts(value) {
95
+ if (value === undefined)
96
+ return true;
97
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
98
+ return false;
99
+ const entries = Object.entries(value);
100
+ if (entries.length > IMPORT_LIMITS.maxItems)
101
+ return false;
102
+ return entries.every(([id, shortcut]) => boundedString(id, IMPORT_LIMITS.maxNameLength) &&
103
+ boundedString(shortcut, IMPORT_LIMITS.maxShortcutLength));
104
+ }
93
105
  function isValidState(value) {
94
106
  if (typeof value !== 'object' || value === null)
95
107
  return false;
96
108
  const state = value;
97
109
  return (state.version === 1 &&
110
+ isValidToolShortcuts(state.toolShortcuts) &&
98
111
  Array.isArray(state.scripts) &&
99
112
  state.scripts.length <= IMPORT_LIMITS.maxItems &&
100
113
  state.scripts.every(isValidScript) &&
package/dist/types.d.ts CHANGED
@@ -125,6 +125,31 @@ export interface RecordedMacro {
125
125
  shortcut?: string;
126
126
  steps: MacroStep[];
127
127
  }
128
+ /**
129
+ * A built-in tool the host registers on the kit: a native document-processing
130
+ * action (implemented by the host against its own engine access) that the kit
131
+ * exposes alongside recordings and scripts — listable in a management UI,
132
+ * runnable under the same one-run-at-a-time guard, and bindable to a
133
+ * persisted keyboard shortcut. Tools are runtime registrations, never
134
+ * persisted themselves; only their shortcuts are.
135
+ */
136
+ export interface BuiltinTool {
137
+ /** Stable identifier, e.g. `'shulchan.first-word'`. Shortcut persistence is keyed by it. */
138
+ id: string;
139
+ /** Display name. */
140
+ name: string;
141
+ /** One-line description for management UIs. */
142
+ description?: string;
143
+ /** Runs the tool. A thrown error is reported as a failed outcome. */
144
+ run(): Promise<MacroOutcome> | MacroOutcome;
145
+ }
146
+ /** A registered tool as listed to UIs — the registration plus its persisted shortcut. */
147
+ export interface BuiltinToolInfo {
148
+ id: string;
149
+ name: string;
150
+ description?: string;
151
+ shortcut?: string;
152
+ }
128
153
  /** A written macro — a JavaScript script that runs against the toolkit's API. */
129
154
  export interface SavedScript {
130
155
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superdoc-macros",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Macro toolkit for SuperDoc-based editors: sandboxed scripted macros, a Word-style macro recorder, snippets with auto-text expansion, and read-only VBA import from .docm files.",
5
5
  "license": "MIT",
6
6
  "type": "module",