superdoc-macros 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -66
- package/dist/host/superdoc-host.d.ts +2 -21
- package/dist/host/superdoc-host.js +56 -28
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/manager.d.ts +24 -21
- package/dist/manager.js +36 -28
- package/dist/messages.d.ts +50 -0
- package/dist/messages.js +78 -0
- package/dist/recorder/recorder.d.ts +7 -19
- package/dist/recorder/recorder.js +21 -6
- package/dist/scripting/eval-runner.d.ts +1 -12
- package/dist/scripting/eval-runner.js +20 -6
- package/dist/scripting/iframe-runner.d.ts +5 -4
- package/dist/scripting/iframe-runner.js +27 -8
- package/dist/scripting/macro-api.d.ts +12 -23
- package/dist/scripting/macro-api.js +24 -9
- package/dist/scripting/runner.d.ts +3 -3
- package/dist/shortcuts.d.ts +8 -6
- package/dist/shortcuts.js +5 -4
- package/dist/snippets/autotext.d.ts +16 -13
- package/dist/snippets/autotext.js +8 -6
- package/dist/snippets/snippets.d.ts +14 -10
- package/dist/snippets/snippets.js +8 -6
- package/dist/storage.d.ts +7 -6
- package/dist/storage.js +4 -4
- package/dist/types.d.ts +33 -32
- package/dist/types.js +6 -5
- package/package.json +1 -1
|
@@ -1,34 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ה-API שסקריפט מאקרו מקבל.
|
|
3
|
-
*
|
|
4
|
-
* שני צרכנים לאותו מימוש: מריץ ה-eval מקבל את האובייקט `api` ישירות, ומריץ
|
|
5
|
-
* ה-iframe מדבר איתו דרך `call(method, args)` — RPC על postMessage. לכן כל
|
|
6
|
-
* מתודה רשומה במילון אחד, וה-proxy בתוך ה-iframe פונה לאותם שמות בדיוק.
|
|
7
|
-
*
|
|
8
|
-
* כללי כשל: פעולות כתיבה זורקות `MacroError` כשהן נכשלות, כדי שסקריפט ייעצר
|
|
9
|
-
* במקום להמשיך על מסמך במצב לא צפוי. `command()` הגולמית מחזירה את התוצאה
|
|
10
|
-
* ואינה זורקת — למי שרוצה לבדוק בעצמו.
|
|
11
|
-
*/
|
|
12
1
|
import type { MacroHost, MacroOutcome } from '../types.js';
|
|
13
|
-
/**
|
|
2
|
+
/** A macro operation failure. Named so a script can tell it apart from its own TypeError. */
|
|
14
3
|
export declare class MacroError extends Error {
|
|
15
4
|
readonly reason?: string;
|
|
16
5
|
constructor(message: string, reason?: string);
|
|
17
6
|
}
|
|
18
|
-
/**
|
|
7
|
+
/** Selection snapshot that is safe to hand to the iframe (without the engine's opaque target). */
|
|
19
8
|
export interface ScriptSelection {
|
|
20
9
|
text: string;
|
|
21
10
|
hasRange: boolean;
|
|
22
11
|
blockId: string | null;
|
|
23
12
|
empty: boolean;
|
|
24
13
|
}
|
|
25
|
-
/**
|
|
14
|
+
/** What a script receives as `api`. Every method is async. */
|
|
26
15
|
export interface MacroApi {
|
|
27
|
-
/**
|
|
16
|
+
/** Runs a command from the engine catalog. Returns the outcome, never throws. */
|
|
28
17
|
command(id: string, payload?: unknown): Promise<MacroOutcome>;
|
|
29
|
-
/**
|
|
18
|
+
/** Whether the engine recognizes the command. */
|
|
30
19
|
hasCommand(id: string): Promise<boolean>;
|
|
31
|
-
/**
|
|
20
|
+
/** The known command ids. */
|
|
32
21
|
commandIds(): Promise<readonly string[]>;
|
|
33
22
|
insertText(text: string): Promise<void>;
|
|
34
23
|
insertParagraph(): Promise<void>;
|
|
@@ -36,7 +25,7 @@ export interface MacroApi {
|
|
|
36
25
|
getSelection(): Promise<ScriptSelection>;
|
|
37
26
|
getSelectionText(): Promise<string>;
|
|
38
27
|
getDocumentText(): Promise<string>;
|
|
39
|
-
/**
|
|
28
|
+
/** Replaces every occurrence. Returns how many were replaced. */
|
|
40
29
|
replaceAll(query: string, replacement: string): Promise<number>;
|
|
41
30
|
bold(): Promise<void>;
|
|
42
31
|
italic(): Promise<void>;
|
|
@@ -51,19 +40,19 @@ export interface MacroApi {
|
|
|
51
40
|
directionLtr(): Promise<void>;
|
|
52
41
|
undo(): Promise<void>;
|
|
53
42
|
redo(): Promise<void>;
|
|
54
|
-
/**
|
|
43
|
+
/** Writes a line to the run log (shown to the user, not to the console). */
|
|
55
44
|
log(...parts: unknown[]): Promise<void>;
|
|
56
45
|
}
|
|
57
46
|
export interface MacroApiOptions {
|
|
58
|
-
/**
|
|
47
|
+
/** Receives every `api.log` line. Default: console.info. */
|
|
59
48
|
onLog?: (line: string) => void;
|
|
60
49
|
}
|
|
61
50
|
export interface MacroBridge {
|
|
62
51
|
api: MacroApi;
|
|
63
|
-
/**
|
|
52
|
+
/** The RPC path: invokes a method by name. Throws on an unknown method. */
|
|
64
53
|
call(method: string, args: readonly unknown[]): Promise<unknown>;
|
|
65
|
-
/**
|
|
54
|
+
/** Number of calls made so far. Used by the runners' call limit. */
|
|
66
55
|
callCount(): number;
|
|
67
56
|
}
|
|
68
|
-
/**
|
|
57
|
+
/** Builds the API on top of a host. */
|
|
69
58
|
export declare function createMacroApi(host: MacroHost, options?: MacroApiOptions): MacroBridge;
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The API a macro script receives.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers share one implementation: the eval runner hands the `api`
|
|
5
|
+
* object to the script directly, and the iframe runner talks to it through
|
|
6
|
+
* `call(method, args)` — RPC over postMessage. Every method therefore lives
|
|
7
|
+
* in a single dictionary, and the proxy inside the iframe addresses exactly
|
|
8
|
+
* the same names.
|
|
9
|
+
*
|
|
10
|
+
* Failure rules: write operations throw a `MacroError` when they fail, so a
|
|
11
|
+
* script stops instead of continuing against a document in an unexpected
|
|
12
|
+
* state. The raw `command()` returns the outcome and does not throw — for
|
|
13
|
+
* scripts that want to check it themselves.
|
|
14
|
+
*/
|
|
15
|
+
import { macroMessages } from '../messages.js';
|
|
16
|
+
/** A macro operation failure. Named so a script can tell it apart from its own TypeError. */
|
|
2
17
|
export class MacroError extends Error {
|
|
3
18
|
reason;
|
|
4
19
|
constructor(message, reason) {
|
|
@@ -14,7 +29,7 @@ function requireOk(outcome, action) {
|
|
|
14
29
|
}
|
|
15
30
|
function asText(value, name) {
|
|
16
31
|
if (typeof value !== 'string')
|
|
17
|
-
throw new MacroError(
|
|
32
|
+
throw new MacroError(macroMessages().mustBeString(name));
|
|
18
33
|
return value;
|
|
19
34
|
}
|
|
20
35
|
function formatLogPart(part) {
|
|
@@ -27,27 +42,27 @@ function formatLogPart(part) {
|
|
|
27
42
|
return String(part);
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
|
-
/**
|
|
45
|
+
/** Builds the API on top of a host. */
|
|
31
46
|
export function createMacroApi(host, options = {}) {
|
|
32
47
|
const onLog = options.onLog ?? ((line) => console.info('[superdoc-macros]', line));
|
|
33
48
|
const commandSugar = async (id) => {
|
|
34
|
-
requireOk(await host.commands.execute(id),
|
|
49
|
+
requireOk(await host.commands.execute(id), macroMessages().commandFailed(id));
|
|
35
50
|
};
|
|
36
51
|
const api = {
|
|
37
52
|
command: (id, payload) => host.commands.execute(asText(id, 'id'), payload),
|
|
38
53
|
hasCommand: async (id) => host.commands.has(asText(id, 'id')),
|
|
39
54
|
commandIds: async () => host.commands.ids(),
|
|
40
55
|
async insertText(text) {
|
|
41
|
-
requireOk(await host.insertText(asText(text, 'text')),
|
|
56
|
+
requireOk(await host.insertText(asText(text, 'text')), macroMessages().insertTextFailed);
|
|
42
57
|
},
|
|
43
58
|
async insertParagraph() {
|
|
44
|
-
requireOk(await host.insertText('\n'),
|
|
59
|
+
requireOk(await host.insertText('\n'), macroMessages().insertParagraphFailed);
|
|
45
60
|
},
|
|
46
61
|
async deleteBackward(count = 1) {
|
|
47
62
|
const n = Math.max(0, Math.trunc(Number(count)));
|
|
48
63
|
if (n === 0)
|
|
49
64
|
return;
|
|
50
|
-
requireOk(await host.deleteBackward(n),
|
|
65
|
+
requireOk(await host.deleteBackward(n), macroMessages().deleteFailed);
|
|
51
66
|
},
|
|
52
67
|
async getSelection() {
|
|
53
68
|
const snapshot = await host.getSelection({ includeText: true });
|
|
@@ -65,7 +80,7 @@ export function createMacroApi(host, options = {}) {
|
|
|
65
80
|
async replaceAll(query, replacement) {
|
|
66
81
|
const result = await host.replaceAll(asText(query, 'query'), asText(replacement, 'replacement'));
|
|
67
82
|
if (!result.ok)
|
|
68
|
-
throw new MacroError(result.message ??
|
|
83
|
+
throw new MacroError(result.message ?? macroMessages().replaceFailed);
|
|
69
84
|
return result.replaced;
|
|
70
85
|
},
|
|
71
86
|
bold: () => commandSugar('bold'),
|
|
@@ -92,7 +107,7 @@ export function createMacroApi(host, options = {}) {
|
|
|
92
107
|
async call(method, args) {
|
|
93
108
|
const fn = methods[method];
|
|
94
109
|
if (typeof fn !== 'function' || !Object.prototype.hasOwnProperty.call(api, method)) {
|
|
95
|
-
throw new MacroError(
|
|
110
|
+
throw new MacroError(macroMessages().unknownMethod(String(method)));
|
|
96
111
|
}
|
|
97
112
|
calls += 1;
|
|
98
113
|
return fn.apply(api, args);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Contract shared by the two runners (eval and iframe). */
|
|
2
2
|
import type { MacroBridge } from './macro-api.js';
|
|
3
3
|
export type MacroRunResult = {
|
|
4
4
|
ok: true;
|
|
@@ -9,9 +9,9 @@ export type MacroRunResult = {
|
|
|
9
9
|
reason?: 'timeout' | 'error' | 'call-limit';
|
|
10
10
|
};
|
|
11
11
|
export interface MacroRunOptions {
|
|
12
|
-
/**
|
|
12
|
+
/** Time cap for the whole run. Default: 30 seconds. */
|
|
13
13
|
timeoutMs?: number;
|
|
14
|
-
/**
|
|
14
|
+
/** API call cap, against runaway loops. Default: 10,000. */
|
|
15
15
|
maxApiCalls?: number;
|
|
16
16
|
}
|
|
17
17
|
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
package/dist/shortcuts.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Keyboard shortcuts for macros and snippets: parsing a `Ctrl+Alt+M` string
|
|
3
|
+
* and matching it against an event.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* Matching is by lowercased `event.key`. `Mod` means Ctrl (or ⌘ on macOS).
|
|
5
6
|
*/
|
|
6
7
|
export interface ParsedShortcut {
|
|
7
8
|
key: string;
|
|
@@ -9,10 +10,10 @@ export interface ParsedShortcut {
|
|
|
9
10
|
alt: boolean;
|
|
10
11
|
shift: boolean;
|
|
11
12
|
meta: boolean;
|
|
12
|
-
/** Ctrl
|
|
13
|
+
/** Ctrl or Meta — for shortcuts written with `Mod`. */
|
|
13
14
|
mod: boolean;
|
|
14
15
|
}
|
|
15
|
-
/**
|
|
16
|
+
/** The subset of KeyboardEvent that matching needs. Enables DOM-free tests. */
|
|
16
17
|
export interface KeyEventLike {
|
|
17
18
|
key: string;
|
|
18
19
|
ctrlKey: boolean;
|
|
@@ -33,7 +34,8 @@ export interface ShortcutTarget {
|
|
|
33
34
|
removeEventListener(type: 'keydown', listener: (event: KeyboardEvent) => void, options?: boolean): void;
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* Binds shortcuts to a target. `getBindings` is called on every keystroke —
|
|
38
|
+
* so the macro list can change without rebinding. Returns a dispose
|
|
39
|
+
* function.
|
|
38
40
|
*/
|
|
39
41
|
export declare function bindShortcuts(target: ShortcutTarget, getBindings: () => readonly ShortcutBinding[]): () => void;
|
package/dist/shortcuts.js
CHANGED
|
@@ -29,7 +29,7 @@ export function parseShortcut(shortcut) {
|
|
|
29
29
|
break;
|
|
30
30
|
default: {
|
|
31
31
|
if (parsed.key)
|
|
32
|
-
return null; //
|
|
32
|
+
return null; // two non-modifier keys — an invalid shortcut.
|
|
33
33
|
parsed.key = normalizeKey(part);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -50,7 +50,7 @@ export function eventMatches(parsed, event) {
|
|
|
50
50
|
if (parsed.mod) {
|
|
51
51
|
if (!event.ctrlKey && !event.metaKey)
|
|
52
52
|
return false;
|
|
53
|
-
//
|
|
53
|
+
// With Mod, ctrl/meta are not checked individually — but alt/shift must match exactly.
|
|
54
54
|
return event.altKey === parsed.alt && event.shiftKey === parsed.shift;
|
|
55
55
|
}
|
|
56
56
|
return (event.ctrlKey === parsed.ctrl &&
|
|
@@ -59,8 +59,9 @@ export function eventMatches(parsed, event) {
|
|
|
59
59
|
event.metaKey === parsed.meta);
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* Binds shortcuts to a target. `getBindings` is called on every keystroke —
|
|
63
|
+
* so the macro list can change without rebinding. Returns a dispose
|
|
64
|
+
* function.
|
|
64
65
|
*/
|
|
65
66
|
export function bindShortcuts(target, getBindings) {
|
|
66
67
|
const listener = (event) => {
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Auto-text: typing a snippet's trigger word followed by a space replaces
|
|
3
|
+
* the word with the snippet's content.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `TextInputEvent`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* How it works: a small buffer keeps the characters typed in the current run
|
|
6
|
+
* (from the host's `TextInputEvent`s). When an expansion character (space by
|
|
7
|
+
* default) is typed and the word before it is some snippet's trigger, the
|
|
8
|
+
* word and the expansion character are deleted backwards and the rendered
|
|
9
|
+
* content is inserted in their place, with the expansion character restored
|
|
10
|
+
* at the end.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The buffer resets on a new paragraph, on forward deletion and on a command
|
|
13
|
+
* running mid-typing — anything that breaks the correspondence between what
|
|
14
|
+
* was typed and what actually sits before the caret. A missed expansion is
|
|
15
|
+
* better than an expansion that deletes the wrong text.
|
|
13
16
|
*/
|
|
14
17
|
import type { MacroHost, Snippet } from '../types.js';
|
|
15
18
|
export interface AutoTextOptions {
|
|
16
|
-
/**
|
|
19
|
+
/** The expansion characters. Default: space only. */
|
|
17
20
|
expandOn?: readonly string[];
|
|
18
|
-
/**
|
|
21
|
+
/** Buffer size. A trigger word longer than this will not be recognized. */
|
|
19
22
|
bufferSize?: number;
|
|
20
|
-
/**
|
|
23
|
+
/** Called after a successful expansion. */
|
|
21
24
|
onExpand?: (snippet: Snippet) => void;
|
|
22
|
-
/**
|
|
25
|
+
/** Called when an expansion failed (e.g. a read-only document). */
|
|
23
26
|
onError?: (message: string) => void;
|
|
24
27
|
}
|
|
25
28
|
export declare class AutoText {
|
|
@@ -27,7 +27,7 @@ export class AutoText {
|
|
|
27
27
|
return () => this.detach();
|
|
28
28
|
this.buffer = '';
|
|
29
29
|
this.disposeInput = this.host.onTextInput((event) => void this.handleInput(event));
|
|
30
|
-
//
|
|
30
|
+
// A command mid-typing (formatting, paste) breaks the buffer's link to the document.
|
|
31
31
|
this.disposeCommand = this.host.onCommand(() => {
|
|
32
32
|
if (!this.busy)
|
|
33
33
|
this.buffer = '';
|
|
@@ -42,7 +42,7 @@ export class AutoText {
|
|
|
42
42
|
this.buffer = '';
|
|
43
43
|
}
|
|
44
44
|
async handleInput(event) {
|
|
45
|
-
//
|
|
45
|
+
// Input generated while the expansion itself is writing — not the user's typing.
|
|
46
46
|
if (this.busy)
|
|
47
47
|
return;
|
|
48
48
|
switch (event.kind) {
|
|
@@ -74,14 +74,16 @@ export class AutoText {
|
|
|
74
74
|
async expand(snippet, trigger, expandChar) {
|
|
75
75
|
this.busy = true;
|
|
76
76
|
try {
|
|
77
|
-
//
|
|
78
|
-
//
|
|
77
|
+
// The input event (beforeinput) fires before the character is written
|
|
78
|
+
// to the document. Deferring to the task queue guarantees the expansion
|
|
79
|
+
// character is already in before it is deleted along with the trigger.
|
|
79
80
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
80
81
|
const selectionText = usesSelection(snippet.text)
|
|
81
82
|
? (await this.host.getSelection({ includeText: true })).text
|
|
82
83
|
: undefined;
|
|
83
84
|
const rendered = renderSnippet(snippet.text, { selectionText });
|
|
84
|
-
//
|
|
85
|
+
// The expansion character is already in the document by now, so it is
|
|
86
|
+
// included in the deletion and restored at the end.
|
|
85
87
|
const deleted = await this.host.deleteBackward(trigger.length + 1);
|
|
86
88
|
if (!deleted.ok) {
|
|
87
89
|
this.onError?.(deleted.message);
|
|
@@ -99,7 +101,7 @@ export class AutoText {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
|
-
/**
|
|
104
|
+
/** The word at the end of the buffer — a run of non-whitespace. */
|
|
103
105
|
function trailingWord(buffer) {
|
|
104
106
|
const match = /(\S+)$/u.exec(buffer);
|
|
105
107
|
return match?.[1] ?? null;
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Text snippets: templates inserted at the caret, with `{{...}}` variables.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Built-in variables: `{{date}}`, `{{time}}`, `{{datetime}}` (system clock,
|
|
5
|
+
* formatted with the configured locale) and `{{selection}}` (the selected
|
|
6
|
+
* text at expansion time). Any other name resolves from the `variables`
|
|
7
|
+
* passed to the call; a variable with no value stays visible in the text —
|
|
8
|
+
* so a typo shows up in the document instead of vanishing silently.
|
|
8
9
|
*/
|
|
9
10
|
import type { MacroHost, MacroOutcome, Snippet } from '../types.js';
|
|
10
11
|
export interface RenderContext {
|
|
11
|
-
/**
|
|
12
|
+
/** Values for custom variables. */
|
|
12
13
|
variables?: Readonly<Record<string, string>>;
|
|
13
|
-
/**
|
|
14
|
+
/** The text substituted for `{{selection}}`. */
|
|
14
15
|
selectionText?: string;
|
|
15
|
-
/**
|
|
16
|
+
/** The time for `{{date}}`/`{{time}}`. Default: now. Exists for tests. */
|
|
16
17
|
now?: Date;
|
|
18
|
+
/** BCP-47 locale for date/time formatting. Default: the browser's. */
|
|
19
|
+
locale?: string;
|
|
17
20
|
}
|
|
18
21
|
export declare function renderSnippet(text: string, context?: RenderContext): string;
|
|
19
|
-
/**
|
|
22
|
+
/** Whether the snippet uses `{{selection}}` — in which case expansion must read the selection. */
|
|
20
23
|
export declare function usesSelection(text: string): boolean;
|
|
21
24
|
export interface ExpandOptions {
|
|
22
25
|
variables?: Readonly<Record<string, string>>;
|
|
23
26
|
now?: Date;
|
|
27
|
+
locale?: string;
|
|
24
28
|
}
|
|
25
|
-
/**
|
|
29
|
+
/** Expands a snippet at the caret. */
|
|
26
30
|
export declare function expandSnippet(host: MacroHost, snippet: Pick<Snippet, 'text'>, options?: ExpandOptions): Promise<MacroOutcome>;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
const VARIABLE_PATTERN = /\{\{\s*([\p{L}\p{N}_-]+)\s*\}\}/gu;
|
|
2
2
|
export function renderSnippet(text, context = {}) {
|
|
3
3
|
const now = context.now ?? new Date();
|
|
4
|
+
const locale = context.locale;
|
|
4
5
|
return text.replace(VARIABLE_PATTERN, (whole, rawName) => {
|
|
5
6
|
const name = rawName.toLowerCase();
|
|
6
7
|
switch (name) {
|
|
7
8
|
case 'date':
|
|
8
|
-
return now.toLocaleDateString(
|
|
9
|
+
return now.toLocaleDateString(locale);
|
|
9
10
|
case 'time':
|
|
10
|
-
return now.toLocaleTimeString(
|
|
11
|
+
return now.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
|
|
11
12
|
case 'datetime':
|
|
12
|
-
return `${now.toLocaleDateString(
|
|
13
|
+
return `${now.toLocaleDateString(locale)} ${now.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })}`;
|
|
13
14
|
case 'selection':
|
|
14
15
|
return context.selectionText ?? '';
|
|
15
16
|
default: {
|
|
@@ -19,13 +20,13 @@ export function renderSnippet(text, context = {}) {
|
|
|
19
20
|
}
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
23
|
+
/** Whether the snippet uses `{{selection}}` — in which case expansion must read the selection. */
|
|
23
24
|
export function usesSelection(text) {
|
|
24
25
|
return /\{\{\s*selection\s*\}\}/iu.test(text);
|
|
25
26
|
}
|
|
26
|
-
/**
|
|
27
|
+
/** Expands a snippet at the caret. */
|
|
27
28
|
export async function expandSnippet(host, snippet, options = {}) {
|
|
28
|
-
//
|
|
29
|
+
// The selection is read only when needed: extracting its text has an engine cost.
|
|
29
30
|
const selectionText = usesSelection(snippet.text)
|
|
30
31
|
? (await host.getSelection({ includeText: true })).text
|
|
31
32
|
: undefined;
|
|
@@ -33,6 +34,7 @@ export async function expandSnippet(host, snippet, options = {}) {
|
|
|
33
34
|
variables: options.variables,
|
|
34
35
|
selectionText,
|
|
35
36
|
now: options.now,
|
|
37
|
+
locale: options.locale,
|
|
36
38
|
});
|
|
37
39
|
return host.insertText(rendered);
|
|
38
40
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Persistence for macros, recordings and snippets. Default: localStorage;
|
|
3
|
+
* the interface is swappable so a host can persist to a file (e.g. a
|
|
4
|
+
* plugin workspace).
|
|
4
5
|
*/
|
|
5
6
|
import type { RecordedMacro, SavedScript, Snippet } from './types.js';
|
|
6
7
|
export interface PersistedMacroState {
|
|
@@ -10,15 +11,15 @@ export interface PersistedMacroState {
|
|
|
10
11
|
snippets: Snippet[];
|
|
11
12
|
}
|
|
12
13
|
export interface MacroStorage {
|
|
13
|
-
/** `null`
|
|
14
|
+
/** `null` when there is no saved state or the saved state is unreadable. */
|
|
14
15
|
load(): PersistedMacroState | null;
|
|
15
16
|
save(state: PersistedMacroState): void;
|
|
16
17
|
}
|
|
17
18
|
export declare function emptyState(): PersistedMacroState;
|
|
18
|
-
/**
|
|
19
|
+
/** Parses saved state. `null` on any unexpected shape — never throws. */
|
|
19
20
|
export declare function parsePersistedState(json: string): PersistedMacroState | null;
|
|
20
21
|
export declare const DEFAULT_STORAGE_KEY = "superdoc-macros:v1";
|
|
21
|
-
/** localStorage
|
|
22
|
+
/** localStorage with guards: blocked or full storage must not take the toolkit down. */
|
|
22
23
|
export declare function createLocalStorage(key?: string, storage?: Pick<Storage, 'getItem' | 'setItem'>): MacroStorage;
|
|
23
|
-
/**
|
|
24
|
+
/** In-memory storage — for tests and for setups with no persistence. */
|
|
24
25
|
export declare function createMemoryStorage(): MacroStorage;
|
package/dist/storage.js
CHANGED
|
@@ -10,7 +10,7 @@ function isValidState(value) {
|
|
|
10
10
|
Array.isArray(state.recordings) &&
|
|
11
11
|
Array.isArray(state.snippets));
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/** Parses saved state. `null` on any unexpected shape — never throws. */
|
|
14
14
|
export function parsePersistedState(json) {
|
|
15
15
|
try {
|
|
16
16
|
const parsed = JSON.parse(json);
|
|
@@ -21,7 +21,7 @@ export function parsePersistedState(json) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
export const DEFAULT_STORAGE_KEY = 'superdoc-macros:v1';
|
|
24
|
-
/** localStorage
|
|
24
|
+
/** localStorage with guards: blocked or full storage must not take the toolkit down. */
|
|
25
25
|
export function createLocalStorage(key = DEFAULT_STORAGE_KEY, storage) {
|
|
26
26
|
const backing = () => {
|
|
27
27
|
if (storage)
|
|
@@ -48,12 +48,12 @@ export function createLocalStorage(key = DEFAULT_STORAGE_KEY, storage) {
|
|
|
48
48
|
backing()?.setItem(key, JSON.stringify(state));
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
|
-
console.warn('[superdoc-macros]
|
|
51
|
+
console.warn('[superdoc-macros] saving macros failed', error);
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/** In-memory storage — for tests and for setups with no persistence. */
|
|
57
57
|
export function createMemoryStorage() {
|
|
58
58
|
let saved = null;
|
|
59
59
|
return {
|