superdoc-macros 0.3.0 → 0.5.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 +17 -1
- package/dist/host/superdoc-host.d.ts +8 -0
- package/dist/host/superdoc-host.js +116 -27
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/manager.d.ts +62 -0
- package/dist/manager.js +226 -11
- package/dist/messages.d.ts +12 -1
- package/dist/messages.js +24 -2
- package/dist/recorder/recorder.d.ts +41 -1
- package/dist/recorder/recorder.js +54 -24
- package/dist/scripting/eval-runner.d.ts +13 -0
- package/dist/scripting/eval-runner.js +31 -2
- package/dist/scripting/iframe-runner.js +15 -4
- package/dist/shortcuts.d.ts +12 -0
- package/dist/shortcuts.js +19 -0
- package/dist/snippets/autotext.d.ts +10 -1
- package/dist/snippets/autotext.js +1 -1
- package/dist/storage.d.ts +31 -1
- package/dist/storage.js +99 -2
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,10 +76,14 @@ if (!result.ok) console.warn(result.message);
|
|
|
76
76
|
|
|
77
77
|
### Security
|
|
78
78
|
|
|
79
|
-
The default is a **real sandbox**: scripts run in an iframe with `sandbox="allow-scripts"` only — an opaque origin, no access to the application's DOM, localStorage
|
|
79
|
+
The default is a **real sandbox**: scripts run in an iframe with `sandbox="allow-scripts"` only — an opaque origin, no access to the application's DOM, localStorage or cookies — plus a `default-src 'none'` CSP inside the iframe, so the script cannot fetch or open sockets to the public internet either. The script's only way to touch the document is the API above, with a time cap (default 30 s — enforced even against infinite loops, by removing the iframe) and a call cap (10,000). When a run ends (result, error or timeout), its bridge is revoked: any late call is rejected and can no longer touch the document.
|
|
80
|
+
|
|
81
|
+
Honest limits: the browser offers no per-iframe memory cap, and a host call that already reached the engine cannot be aborted mid-flight (the engine exposes no cancellation) — what is guaranteed is that nothing new starts.
|
|
80
82
|
|
|
81
83
|
If you must waive isolation (e.g. a CSP that blocks `srcdoc`), switch to the direct runner: `new MacroKit({ host, runner: 'eval' })` — see the warnings in the code.
|
|
82
84
|
|
|
85
|
+
**A real off switch:** `new MacroKit({ host, scriptsEnabled: false })` gates script *execution*, not just UI — `runScript`/`runSource` refuse and saved script shortcuts are not bound, so a pre-existing or imported script cannot run through any path. Recordings and snippets are unaffected.
|
|
86
|
+
|
|
83
87
|
## 2. Macro recorder
|
|
84
88
|
|
|
85
89
|
```ts
|
|
@@ -134,6 +138,18 @@ kit.importState(json, { merge: true });
|
|
|
134
138
|
|
|
135
139
|
`MacroStorage` is a two-method interface — implement it to persist to a file (e.g. a plugin workspace).
|
|
136
140
|
|
|
141
|
+
Imports are strictly validated **atomically on the final result**: every item and every recorded step is type-checked and size-bounded (see `IMPORT_LIMITS`), every shortcut in the merged state must pass the same binding rules the save paths enforce (modifier required, not host-reserved, no duplicates), and any failure rejects the whole file with the current state untouched — no partial imports.
|
|
142
|
+
|
|
143
|
+
The same limits hold as a persistence invariant: the save paths refuse oversized fields and full lists, an oversized recorded paste is split into loadable steps, and state that the loader would reject is never written — so a single bad save can never wipe the store on the next startup.
|
|
144
|
+
|
|
145
|
+
## Shortcut safety
|
|
146
|
+
|
|
147
|
+
Saved bindings go through `kit.validateShortcut(shortcut, excludeId?)` — enforced on every save: a binding must parse, must carry a real modifier (Ctrl/Alt/Meta — a bare letter would fire on ordinary typing), must not collide with another saved item, and must not collide with shortcuts the host declared as reserved:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const kit = new MacroKit({ host, reservedShortcuts: ['Ctrl+S', 'Ctrl+P', /* … the editor's registry … */] });
|
|
151
|
+
```
|
|
152
|
+
|
|
137
153
|
## Connecting a different host
|
|
138
154
|
|
|
139
155
|
The whole toolkit works against a single `MacroHost` interface (commands, text insertion, selection, replace, typing events). `createSuperdocHost` is the implementation for SuperDoc v2 in `ui: false` mode; another editor plugs in with its own implementation — see `src/types.ts` and the double in `tests/fake-host.ts`.
|
|
@@ -102,6 +102,14 @@ export interface SuperdocHostOptions {
|
|
|
102
102
|
superdoc: SuperdocLike;
|
|
103
103
|
/** The element the document renders in — typing events are captured on it. */
|
|
104
104
|
container?: HTMLElement | null;
|
|
105
|
+
/**
|
|
106
|
+
* Whether the adapter may fall back to the engine's internal ProseMirror
|
|
107
|
+
* view for the operations that have no public surface: backward/forward
|
|
108
|
+
* deletion, full-document text, and insertion when the Document API is
|
|
109
|
+
* missing. Default: true. Hosts that want to stay strictly on public
|
|
110
|
+
* surfaces set false — those operations then fail closed.
|
|
111
|
+
*/
|
|
112
|
+
viewFallback?: boolean;
|
|
105
113
|
}
|
|
106
114
|
export interface SuperdocMacroHost extends MacroHost {
|
|
107
115
|
/** Removes the observation wrapper and the DOM listeners. Call before swapping documents. */
|
|
@@ -43,40 +43,101 @@ function emptySelection() {
|
|
|
43
43
|
/* ---------- The implementation ---------- */
|
|
44
44
|
export function createSuperdocHost(options) {
|
|
45
45
|
const { superdoc, container } = options;
|
|
46
|
+
const viewFallback = options.viewFallback ?? true;
|
|
46
47
|
// Read at call time, never cached: activeEditor is replaced on every document open.
|
|
47
48
|
const commands = () => superdoc.ui?.commands ?? null;
|
|
48
49
|
const doc = () => superdoc.activeEditor?.doc ?? null;
|
|
49
|
-
const view = () => superdoc.activeEditor?.view ?? null;
|
|
50
50
|
const search = () => superdoc.ui?.search ?? null;
|
|
51
|
+
/**
|
|
52
|
+
* The single gate to the internal ProseMirror view. Every use of a
|
|
53
|
+
* non-public surface goes through here, so disabling the fallback (or a
|
|
54
|
+
* future engine that hides the view) degrades to closed failures in one
|
|
55
|
+
* place.
|
|
56
|
+
*/
|
|
57
|
+
const view = () => viewFallback ? (superdoc.activeEditor?.view ?? null) : null;
|
|
51
58
|
const commandListeners = new Set();
|
|
52
59
|
const inputListeners = new Set();
|
|
53
|
-
|
|
60
|
+
const emitInput = (mapped) => {
|
|
61
|
+
for (const listener of inputListeners) {
|
|
62
|
+
try {
|
|
63
|
+
listener(mapped);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.warn('[superdoc-macros] input listener threw', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/* Command observation: wrap executeAsync, once, restored on dispose.
|
|
71
|
+
Listeners are notified only after the engine reports success — a command
|
|
72
|
+
that was refused (not routed, or a failed receipt) must not enter a
|
|
73
|
+
recording, or replay would re-fail it or, worse, apply it in a context
|
|
74
|
+
where it now succeeds unintended. */
|
|
54
75
|
const wrapped = commands();
|
|
55
76
|
const originalExecuteAsync = wrapped?.executeAsync;
|
|
56
77
|
if (wrapped && originalExecuteAsync) {
|
|
57
78
|
wrapped.executeAsync = function (id, payload) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
79
|
+
const result = originalExecuteAsync.call(wrapped, id, payload);
|
|
80
|
+
void Promise.resolve(result)
|
|
81
|
+
.then((value) => {
|
|
82
|
+
if (value === false)
|
|
83
|
+
return;
|
|
84
|
+
if (typeof value === 'object' && value !== null && value.success === false)
|
|
85
|
+
return;
|
|
86
|
+
for (const listener of commandListeners) {
|
|
87
|
+
try {
|
|
88
|
+
listener(id, payload);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.warn('[superdoc-macros] command listener threw', error);
|
|
92
|
+
}
|
|
61
93
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
return originalExecuteAsync.call(wrapped, id, payload);
|
|
94
|
+
})
|
|
95
|
+
.catch(() => undefined); // a thrown command is a failure — nothing to record.
|
|
96
|
+
return result;
|
|
67
97
|
};
|
|
68
98
|
}
|
|
69
|
-
/* Typing: beforeinput on the container, capture phase.
|
|
99
|
+
/* Typing: beforeinput on the container, capture phase.
|
|
100
|
+
|
|
101
|
+
IME composition is folded to a single event: while composing, the
|
|
102
|
+
engine fires insertCompositionText repeatedly with the growing
|
|
103
|
+
candidate text, and mapping each one would record the word once per
|
|
104
|
+
keystroke. The events are suppressed during composition and the final
|
|
105
|
+
text is emitted once, from compositionend. */
|
|
106
|
+
let composing = false;
|
|
107
|
+
const onCompositionStart = () => {
|
|
108
|
+
composing = true;
|
|
109
|
+
};
|
|
110
|
+
const onCompositionEnd = (event) => {
|
|
111
|
+
composing = false;
|
|
112
|
+
const data = event.data;
|
|
113
|
+
if (typeof data === 'string' && data.length > 0)
|
|
114
|
+
emitInput({ kind: 'insert-text', text: data });
|
|
115
|
+
};
|
|
70
116
|
const onBeforeInput = (event) => {
|
|
71
117
|
const input = event;
|
|
72
118
|
let mapped = null;
|
|
73
119
|
switch (input.inputType) {
|
|
74
120
|
case 'insertText':
|
|
75
121
|
case 'insertCompositionText':
|
|
122
|
+
if (composing)
|
|
123
|
+
break; // the final text arrives from compositionend.
|
|
76
124
|
if (typeof input.data === 'string' && input.data.length > 0) {
|
|
77
125
|
mapped = { kind: 'insert-text', text: input.data };
|
|
78
126
|
}
|
|
79
127
|
break;
|
|
128
|
+
// Paste, drop and spellcheck replacement carry their text either in
|
|
129
|
+
// `data` or in a dataTransfer — without this branch a recording
|
|
130
|
+
// silently missed everything the user pasted.
|
|
131
|
+
case 'insertFromPaste':
|
|
132
|
+
case 'insertFromDrop':
|
|
133
|
+
case 'insertReplacementText': {
|
|
134
|
+
const text = typeof input.data === 'string' && input.data.length > 0
|
|
135
|
+
? input.data
|
|
136
|
+
: input.dataTransfer?.getData('text/plain') ?? '';
|
|
137
|
+
if (text.length > 0)
|
|
138
|
+
mapped = { kind: 'insert-text', text };
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
80
141
|
case 'insertParagraph':
|
|
81
142
|
mapped = { kind: 'insert-paragraph' };
|
|
82
143
|
break;
|
|
@@ -89,29 +150,32 @@ export function createSuperdocHost(options) {
|
|
|
89
150
|
default:
|
|
90
151
|
break;
|
|
91
152
|
}
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
for (const listener of inputListeners) {
|
|
95
|
-
try {
|
|
96
|
-
listener(mapped);
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
console.warn('[superdoc-macros] input listener threw', error);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
153
|
+
if (mapped)
|
|
154
|
+
emitInput(mapped);
|
|
102
155
|
};
|
|
103
156
|
container?.addEventListener('beforeinput', onBeforeInput, true);
|
|
104
|
-
|
|
157
|
+
container?.addEventListener('compositionstart', onCompositionStart, true);
|
|
158
|
+
container?.addEventListener('compositionend', onCompositionEnd, true);
|
|
159
|
+
/**
|
|
160
|
+
* `failed: true` means the engine call itself threw — as opposed to a
|
|
161
|
+
* clean "no selection" answer. Callers that write relative to the caret
|
|
162
|
+
* must fail closed on it: falling back to "no target" would send the text
|
|
163
|
+
* to the end of the document, far from where the user is looking.
|
|
164
|
+
*/
|
|
165
|
+
async function readSelectionDetailed(includeText) {
|
|
105
166
|
const current = doc()?.selection?.current;
|
|
106
167
|
if (typeof current !== 'function')
|
|
107
|
-
return emptySelection();
|
|
168
|
+
return { snapshot: emptySelection(), failed: false };
|
|
108
169
|
let info;
|
|
109
170
|
try {
|
|
110
171
|
info = await current(includeText ? { includeText: true } : undefined);
|
|
111
172
|
}
|
|
112
173
|
catch {
|
|
113
|
-
return emptySelection();
|
|
174
|
+
return { snapshot: emptySelection(), failed: true };
|
|
114
175
|
}
|
|
176
|
+
return { snapshot: parseSelectionInfo(info), failed: false };
|
|
177
|
+
}
|
|
178
|
+
function parseSelectionInfo(info) {
|
|
115
179
|
if (!info || typeof info !== 'object')
|
|
116
180
|
return emptySelection();
|
|
117
181
|
const segments = Array.isArray(info.target?.segments) ? info.target.segments : [];
|
|
@@ -168,7 +232,11 @@ export function createSuperdocHost(options) {
|
|
|
168
232
|
const insert = doc()?.insert;
|
|
169
233
|
if (typeof insert === 'function') {
|
|
170
234
|
// Without a target the insertion falls to the end of the document — so the target comes from the live selection.
|
|
171
|
-
const snapshot = await
|
|
235
|
+
const { snapshot, failed: selectionFailed } = await readSelectionDetailed(false);
|
|
236
|
+
// A failed read is not "no selection": inserting without a target
|
|
237
|
+
// would land the text at the end of the document. Fail closed.
|
|
238
|
+
if (selectionFailed)
|
|
239
|
+
return failed(macroMessages().selectionUnavailable, 'selection-read-failed');
|
|
172
240
|
try {
|
|
173
241
|
const receipt = await insert({
|
|
174
242
|
value: text,
|
|
@@ -216,8 +284,27 @@ export function createSuperdocHost(options) {
|
|
|
216
284
|
return failed(error instanceof Error ? error.message : macroMessages().deleteFailed, 'threw');
|
|
217
285
|
}
|
|
218
286
|
},
|
|
219
|
-
|
|
220
|
-
|
|
287
|
+
async deleteForward(count) {
|
|
288
|
+
const pm = view();
|
|
289
|
+
if (!pm)
|
|
290
|
+
return failed(macroMessages().deletionUnavailable, 'view-unavailable');
|
|
291
|
+
try {
|
|
292
|
+
const { from } = pm.state.selection;
|
|
293
|
+
const size = pm.state.doc.content.size;
|
|
294
|
+
const end = Math.min(size, from + Math.max(0, Math.trunc(count)));
|
|
295
|
+
if (end === from)
|
|
296
|
+
return { ok: true };
|
|
297
|
+
const tr = pm.state.tr;
|
|
298
|
+
tr.delete(from, end);
|
|
299
|
+
pm.dispatch(tr);
|
|
300
|
+
return { ok: true };
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
return failed(error instanceof Error ? error.message : macroMessages().deleteFailed, 'threw');
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
async getSelection(options) {
|
|
307
|
+
return (await readSelectionDetailed(options?.includeText ?? false)).snapshot;
|
|
221
308
|
},
|
|
222
309
|
async replaceAll(query, replacement) {
|
|
223
310
|
const handle = search();
|
|
@@ -278,6 +365,8 @@ export function createSuperdocHost(options) {
|
|
|
278
365
|
if (wrapped && originalExecuteAsync)
|
|
279
366
|
wrapped.executeAsync = originalExecuteAsync;
|
|
280
367
|
container?.removeEventListener('beforeinput', onBeforeInput, true);
|
|
368
|
+
container?.removeEventListener('compositionstart', onCompositionStart, true);
|
|
369
|
+
container?.removeEventListener('compositionend', onCompositionEnd, true);
|
|
281
370
|
commandListeners.clear();
|
|
282
371
|
inputListeners.clear();
|
|
283
372
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { MacroHost, MacroOutcome, MacroStep, RecordedMacro, SavedScript, SelectionSnapshot, Snippet, TextInputEvent, } from './types.js';
|
|
2
|
-
export { MacroKit, type MacroKitOptions } from './manager.js';
|
|
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';
|
|
5
5
|
export { createMacroApi, MacroError, type MacroApi, type MacroBridge, type ScriptSelection } from './scripting/macro-api.js';
|
|
@@ -8,6 +8,6 @@ export { createIframeRunner, SANDBOX_BOOTSTRAP, isProtocolMessage } from './scri
|
|
|
8
8
|
export type { MacroRunner, MacroRunOptions, MacroRunResult } from './scripting/runner.js';
|
|
9
9
|
export { MacroRecorder, replayMacro, type RecorderOptions, type ReplayOptions, type ReplayResult } from './recorder/recorder.js';
|
|
10
10
|
export { renderSnippet, expandSnippet, usesSelection, type ExpandOptions, type RenderContext } from './snippets/snippets.js';
|
|
11
|
-
export { AutoText, type AutoTextOptions } from './snippets/autotext.js';
|
|
12
|
-
export { parseShortcut, eventMatches, bindShortcuts, type ParsedShortcut, type ShortcutBinding, type ShortcutTarget } from './shortcuts.js';
|
|
13
|
-
export { createLocalStorage, createMemoryStorage, emptyState, parsePersistedState, DEFAULT_STORAGE_KEY, type MacroStorage, type PersistedMacroState, } from './storage.js';
|
|
11
|
+
export { AutoText, type AutoTextOptions, type AutoTextExpansion } from './snippets/autotext.js';
|
|
12
|
+
export { parseShortcut, eventMatches, bindShortcuts, shortcutSignatures, hasBindingModifier, type ParsedShortcut, type ShortcutBinding, type ShortcutTarget, } from './shortcuts.js';
|
|
13
|
+
export { createLocalStorage, createMemoryStorage, emptyState, parsePersistedState, DEFAULT_STORAGE_KEY, IMPORT_LIMITS, isPersistableState, type MacroStorage, type PersistedMacroState, } from './storage.js';
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,5 @@ export { createIframeRunner, SANDBOX_BOOTSTRAP, isProtocolMessage } from './scri
|
|
|
7
7
|
export { MacroRecorder, replayMacro } from './recorder/recorder.js';
|
|
8
8
|
export { renderSnippet, expandSnippet, usesSelection } from './snippets/snippets.js';
|
|
9
9
|
export { AutoText } from './snippets/autotext.js';
|
|
10
|
-
export { parseShortcut, eventMatches, bindShortcuts } from './shortcuts.js';
|
|
11
|
-
export { createLocalStorage, createMemoryStorage, emptyState, parsePersistedState, DEFAULT_STORAGE_KEY, } from './storage.js';
|
|
10
|
+
export { parseShortcut, eventMatches, bindShortcuts, shortcutSignatures, hasBindingModifier, } from './shortcuts.js';
|
|
11
|
+
export { createLocalStorage, createMemoryStorage, emptyState, parsePersistedState, DEFAULT_STORAGE_KEY, IMPORT_LIMITS, isPersistableState, } from './storage.js';
|
package/dist/manager.d.ts
CHANGED
|
@@ -31,7 +31,34 @@ export interface MacroKitOptions {
|
|
|
31
31
|
autoText?: Omit<AutoTextOptions, 'onExpand' | 'onError'> & AutoTextOptions;
|
|
32
32
|
/** Run log for `api.log`. */
|
|
33
33
|
onLog?: MacroApiOptions['onLog'];
|
|
34
|
+
/**
|
|
35
|
+
* Shortcuts the host already owns (e.g. its ribbon registry). A saved
|
|
36
|
+
* binding that collides with any of these is rejected — otherwise the
|
|
37
|
+
* macro binding, attached in the capture phase, would silently shadow an
|
|
38
|
+
* editor shortcut. Unparseable entries are ignored.
|
|
39
|
+
*/
|
|
40
|
+
reservedShortcuts?: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Whether scripted macros may *execute*. Default: true. When false the
|
|
43
|
+
* gate is real, not cosmetic: `runScript`/`runSource` refuse, and saved
|
|
44
|
+
* script shortcuts are not bound — so a pre-existing or imported script
|
|
45
|
+
* cannot run through any path. Saving and listing still work (a UI may
|
|
46
|
+
* let the user manage scripts it will not run).
|
|
47
|
+
*/
|
|
48
|
+
scriptsEnabled?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Called when a recording hits the step cap and stops on its own. The
|
|
51
|
+
* steps are kept — `stopRecording(name)` still saves them — but a UI that
|
|
52
|
+
* shows a live "recording" indicator must update it.
|
|
53
|
+
*/
|
|
54
|
+
onRecordingAutoStop?: () => void;
|
|
34
55
|
}
|
|
56
|
+
export type ShortcutValidation = {
|
|
57
|
+
ok: true;
|
|
58
|
+
} | {
|
|
59
|
+
ok: false;
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
35
62
|
export declare class MacroKit {
|
|
36
63
|
private readonly host;
|
|
37
64
|
private readonly storage;
|
|
@@ -41,8 +68,21 @@ export declare class MacroKit {
|
|
|
41
68
|
private state;
|
|
42
69
|
private readonly recorder;
|
|
43
70
|
private readonly autoText;
|
|
71
|
+
private readonly reservedSignatures;
|
|
72
|
+
private readonly scriptsEnabled;
|
|
44
73
|
private running;
|
|
45
74
|
constructor(options: MacroKitOptions);
|
|
75
|
+
/**
|
|
76
|
+
* Whether a shortcut is acceptable for a saved binding: parseable, carries
|
|
77
|
+
* a real modifier, not reserved by the host, and not already used by
|
|
78
|
+
* another saved item (`excludeId` skips the item being edited). Empty or
|
|
79
|
+
* undefined means "no shortcut" and is fine. The save paths enforce this;
|
|
80
|
+
* UIs call it directly to show the message before saving.
|
|
81
|
+
*/
|
|
82
|
+
validateShortcut(shortcut: string | undefined, excludeId?: string): ShortcutValidation;
|
|
83
|
+
private findShortcutOwner;
|
|
84
|
+
/** Throws a MacroError when the shortcut is unacceptable. The save paths call this. */
|
|
85
|
+
private requireValidShortcut;
|
|
46
86
|
listScripts(): readonly SavedScript[];
|
|
47
87
|
saveScript(input: {
|
|
48
88
|
id?: string;
|
|
@@ -98,6 +138,12 @@ export declare class MacroKit {
|
|
|
98
138
|
* Imports JSON produced by `exportState`. With `merge: true` an imported
|
|
99
139
|
* item with an existing `id` replaces it; without merge the whole state is
|
|
100
140
|
* replaced.
|
|
141
|
+
*
|
|
142
|
+
* The check is **atomic, on the final result**: a candidate state is built
|
|
143
|
+
* first, its size limits and every shortcut in it are validated (the same
|
|
144
|
+
* rules the save paths enforce — a file cannot smuggle in what typing
|
|
145
|
+
* cannot), and only a candidate that passed in full is committed. On any
|
|
146
|
+
* failure the current state is untouched.
|
|
101
147
|
*/
|
|
102
148
|
importState(json: string, options?: {
|
|
103
149
|
merge?: boolean;
|
|
@@ -105,7 +151,23 @@ export declare class MacroKit {
|
|
|
105
151
|
ok: boolean;
|
|
106
152
|
message?: string;
|
|
107
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* Every shortcut in a candidate state, under the exact rules of
|
|
156
|
+
* `validateShortcut`: parseable, real modifier, not host-reserved, and
|
|
157
|
+
* unique within the candidate. `importState` is the only caller — the
|
|
158
|
+
* save paths enforce the same rules one item at a time.
|
|
159
|
+
*/
|
|
160
|
+
private validateStateShortcuts;
|
|
108
161
|
private guardRun;
|
|
162
|
+
/**
|
|
163
|
+
* The save-path half of the persistence invariant: field lengths that the
|
|
164
|
+
* loader would reject are refused at the door. Without this, one oversized
|
|
165
|
+
* save would make the whole store unloadable — and the next startup would
|
|
166
|
+
* silently fall back to an empty state, losing everything.
|
|
167
|
+
*/
|
|
168
|
+
private requireItemLimits;
|
|
169
|
+
/** The item-count half of the invariant. `existingId` exempts an in-place update. */
|
|
170
|
+
private requireRoom;
|
|
109
171
|
private upsert;
|
|
110
172
|
private persist;
|
|
111
173
|
}
|