superdoc-macros 0.2.0 → 0.4.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 +81 -66
- package/dist/host/superdoc-host.d.ts +10 -21
- package/dist/host/superdoc-host.js +168 -51
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/manager.d.ts +49 -21
- package/dist/manager.js +102 -29
- package/dist/messages.d.ts +55 -0
- package/dist/messages.js +88 -0
- package/dist/recorder/recorder.d.ts +17 -15
- package/dist/recorder/recorder.js +5 -6
- package/dist/scripting/eval-runner.d.ts +14 -12
- package/dist/scripting/eval-runner.js +51 -8
- package/dist/scripting/iframe-runner.d.ts +5 -4
- package/dist/scripting/iframe-runner.js +42 -12
- 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 +20 -6
- package/dist/shortcuts.js +24 -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 +30 -6
- package/dist/storage.js +93 -5
- package/dist/types.d.ts +35 -32
- package/dist/types.js +6 -5
- package/package.json +1 -1
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,38 @@ 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
|
+
/**
|
|
20
|
+
* Caps on imported data. Imports come from files users share with each
|
|
21
|
+
* other, so every field is validated and bounded — a malformed or oversized
|
|
22
|
+
* export must fail closed, not wedge the store or the UI.
|
|
23
|
+
*/
|
|
24
|
+
export declare const IMPORT_LIMITS: {
|
|
25
|
+
/** Whole-file size, in UTF-16 code units of the JSON string. */
|
|
26
|
+
readonly maxJsonLength: 5000000;
|
|
27
|
+
/** Per list: scripts, recordings, snippets. */
|
|
28
|
+
readonly maxItems: 500;
|
|
29
|
+
readonly maxStepsPerRecording: 5000;
|
|
30
|
+
readonly maxNameLength: 200;
|
|
31
|
+
readonly maxShortcutLength: 60;
|
|
32
|
+
readonly maxTriggerLength: 60;
|
|
33
|
+
/** Snippet text and single recorded insert-text step. */
|
|
34
|
+
readonly maxTextLength: 100000;
|
|
35
|
+
readonly maxSourceLength: 200000;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Parses saved/imported state. `null` on any unexpected shape, oversized
|
|
39
|
+
* field or oversized file — never throws, never partially accepts: one
|
|
40
|
+
* invalid item rejects the whole document, so the caller can tell the user
|
|
41
|
+
* the file is bad instead of silently importing a subset.
|
|
42
|
+
*/
|
|
19
43
|
export declare function parsePersistedState(json: string): PersistedMacroState | null;
|
|
20
44
|
export declare const DEFAULT_STORAGE_KEY = "superdoc-macros:v1";
|
|
21
|
-
/** localStorage
|
|
45
|
+
/** localStorage with guards: blocked or full storage must not take the toolkit down. */
|
|
22
46
|
export declare function createLocalStorage(key?: string, storage?: Pick<Storage, 'getItem' | 'setItem'>): MacroStorage;
|
|
23
|
-
/**
|
|
47
|
+
/** In-memory storage — for tests and for setups with no persistence. */
|
|
24
48
|
export declare function createMemoryStorage(): MacroStorage;
|
package/dist/storage.js
CHANGED
|
@@ -1,17 +1,105 @@
|
|
|
1
1
|
export function emptyState() {
|
|
2
2
|
return { version: 1, scripts: [], recordings: [], snippets: [] };
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Caps on imported data. Imports come from files users share with each
|
|
6
|
+
* other, so every field is validated and bounded — a malformed or oversized
|
|
7
|
+
* export must fail closed, not wedge the store or the UI.
|
|
8
|
+
*/
|
|
9
|
+
export const IMPORT_LIMITS = {
|
|
10
|
+
/** Whole-file size, in UTF-16 code units of the JSON string. */
|
|
11
|
+
maxJsonLength: 5_000_000,
|
|
12
|
+
/** Per list: scripts, recordings, snippets. */
|
|
13
|
+
maxItems: 500,
|
|
14
|
+
maxStepsPerRecording: 5_000,
|
|
15
|
+
maxNameLength: 200,
|
|
16
|
+
maxShortcutLength: 60,
|
|
17
|
+
maxTriggerLength: 60,
|
|
18
|
+
/** Snippet text and single recorded insert-text step. */
|
|
19
|
+
maxTextLength: 100_000,
|
|
20
|
+
maxSourceLength: 200_000,
|
|
21
|
+
};
|
|
22
|
+
function boundedString(value, maxLength, allowEmpty = false) {
|
|
23
|
+
return typeof value === 'string' && value.length <= maxLength && (allowEmpty || value.length > 0);
|
|
24
|
+
}
|
|
25
|
+
function optionalBoundedString(value, maxLength) {
|
|
26
|
+
return value === undefined || boundedString(value, maxLength);
|
|
27
|
+
}
|
|
28
|
+
function isValidStep(value) {
|
|
29
|
+
if (typeof value !== 'object' || value === null)
|
|
30
|
+
return false;
|
|
31
|
+
const step = value;
|
|
32
|
+
switch (step.type) {
|
|
33
|
+
case 'command':
|
|
34
|
+
// The payload is opaque engine data; the id is what replay dispatches on.
|
|
35
|
+
return boundedString(step.id, IMPORT_LIMITS.maxNameLength);
|
|
36
|
+
case 'insert-text':
|
|
37
|
+
return boundedString(step.text, IMPORT_LIMITS.maxTextLength, true);
|
|
38
|
+
case 'insert-paragraph':
|
|
39
|
+
return true;
|
|
40
|
+
case 'delete-backward':
|
|
41
|
+
case 'delete-forward':
|
|
42
|
+
return typeof step.count === 'number' && Number.isInteger(step.count) && step.count > 0 && step.count <= IMPORT_LIMITS.maxTextLength;
|
|
43
|
+
default:
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isValidScript(value) {
|
|
48
|
+
if (typeof value !== 'object' || value === null)
|
|
49
|
+
return false;
|
|
50
|
+
const script = value;
|
|
51
|
+
return (boundedString(script.id, IMPORT_LIMITS.maxNameLength) &&
|
|
52
|
+
boundedString(script.name, IMPORT_LIMITS.maxNameLength) &&
|
|
53
|
+
boundedString(script.source, IMPORT_LIMITS.maxSourceLength, true) &&
|
|
54
|
+
optionalBoundedString(script.shortcut, IMPORT_LIMITS.maxShortcutLength));
|
|
55
|
+
}
|
|
56
|
+
function isValidRecording(value) {
|
|
57
|
+
if (typeof value !== 'object' || value === null)
|
|
58
|
+
return false;
|
|
59
|
+
const recording = value;
|
|
60
|
+
return (recording.version === 1 &&
|
|
61
|
+
boundedString(recording.id, IMPORT_LIMITS.maxNameLength) &&
|
|
62
|
+
boundedString(recording.name, IMPORT_LIMITS.maxNameLength) &&
|
|
63
|
+
optionalBoundedString(recording.createdAt, IMPORT_LIMITS.maxNameLength) &&
|
|
64
|
+
optionalBoundedString(recording.shortcut, IMPORT_LIMITS.maxShortcutLength) &&
|
|
65
|
+
Array.isArray(recording.steps) &&
|
|
66
|
+
recording.steps.length <= IMPORT_LIMITS.maxStepsPerRecording &&
|
|
67
|
+
recording.steps.every(isValidStep));
|
|
68
|
+
}
|
|
69
|
+
function isValidSnippet(value) {
|
|
70
|
+
if (typeof value !== 'object' || value === null)
|
|
71
|
+
return false;
|
|
72
|
+
const snippet = value;
|
|
73
|
+
return (boundedString(snippet.id, IMPORT_LIMITS.maxNameLength) &&
|
|
74
|
+
boundedString(snippet.name, IMPORT_LIMITS.maxNameLength) &&
|
|
75
|
+
boundedString(snippet.text, IMPORT_LIMITS.maxTextLength, true) &&
|
|
76
|
+
optionalBoundedString(snippet.trigger, IMPORT_LIMITS.maxTriggerLength) &&
|
|
77
|
+
optionalBoundedString(snippet.shortcut, IMPORT_LIMITS.maxShortcutLength));
|
|
78
|
+
}
|
|
4
79
|
function isValidState(value) {
|
|
5
80
|
if (typeof value !== 'object' || value === null)
|
|
6
81
|
return false;
|
|
7
82
|
const state = value;
|
|
8
83
|
return (state.version === 1 &&
|
|
9
84
|
Array.isArray(state.scripts) &&
|
|
85
|
+
state.scripts.length <= IMPORT_LIMITS.maxItems &&
|
|
86
|
+
state.scripts.every(isValidScript) &&
|
|
10
87
|
Array.isArray(state.recordings) &&
|
|
11
|
-
|
|
88
|
+
state.recordings.length <= IMPORT_LIMITS.maxItems &&
|
|
89
|
+
state.recordings.every(isValidRecording) &&
|
|
90
|
+
Array.isArray(state.snippets) &&
|
|
91
|
+
state.snippets.length <= IMPORT_LIMITS.maxItems &&
|
|
92
|
+
state.snippets.every(isValidSnippet));
|
|
12
93
|
}
|
|
13
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Parses saved/imported state. `null` on any unexpected shape, oversized
|
|
96
|
+
* field or oversized file — never throws, never partially accepts: one
|
|
97
|
+
* invalid item rejects the whole document, so the caller can tell the user
|
|
98
|
+
* the file is bad instead of silently importing a subset.
|
|
99
|
+
*/
|
|
14
100
|
export function parsePersistedState(json) {
|
|
101
|
+
if (json.length > IMPORT_LIMITS.maxJsonLength)
|
|
102
|
+
return null;
|
|
15
103
|
try {
|
|
16
104
|
const parsed = JSON.parse(json);
|
|
17
105
|
return isValidState(parsed) ? parsed : null;
|
|
@@ -21,7 +109,7 @@ export function parsePersistedState(json) {
|
|
|
21
109
|
}
|
|
22
110
|
}
|
|
23
111
|
export const DEFAULT_STORAGE_KEY = 'superdoc-macros:v1';
|
|
24
|
-
/** localStorage
|
|
112
|
+
/** localStorage with guards: blocked or full storage must not take the toolkit down. */
|
|
25
113
|
export function createLocalStorage(key = DEFAULT_STORAGE_KEY, storage) {
|
|
26
114
|
const backing = () => {
|
|
27
115
|
if (storage)
|
|
@@ -48,12 +136,12 @@ export function createLocalStorage(key = DEFAULT_STORAGE_KEY, storage) {
|
|
|
48
136
|
backing()?.setItem(key, JSON.stringify(state));
|
|
49
137
|
}
|
|
50
138
|
catch (error) {
|
|
51
|
-
console.warn('[superdoc-macros]
|
|
139
|
+
console.warn('[superdoc-macros] saving macros failed', error);
|
|
52
140
|
}
|
|
53
141
|
},
|
|
54
142
|
};
|
|
55
143
|
}
|
|
56
|
-
/**
|
|
144
|
+
/** In-memory storage — for tests and for setups with no persistence. */
|
|
57
145
|
export function createMemoryStorage() {
|
|
58
146
|
let saved = null;
|
|
59
147
|
return {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Shared contracts.
|
|
3
3
|
*
|
|
4
|
-
* `MacroHost`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `MacroHost` is the single connection point to the editor: all three
|
|
5
|
+
* capabilities (scripts, recorder, snippets) work against it rather than
|
|
6
|
+
* against SuperDoc directly. That is what makes the toolkit testable with an
|
|
7
|
+
* in-memory double, and what lets another host (a different engine version,
|
|
8
|
+
* a different editor) plug in with one implementation of this interface.
|
|
8
9
|
*/
|
|
9
|
-
/**
|
|
10
|
+
/** Result of an operation. Same shape as otzaria-word-editor's `CommandOutcome`. */
|
|
10
11
|
export type MacroOutcome = {
|
|
11
12
|
ok: true;
|
|
12
13
|
} | {
|
|
@@ -14,20 +15,20 @@ export type MacroOutcome = {
|
|
|
14
15
|
message: string;
|
|
15
16
|
reason?: string;
|
|
16
17
|
};
|
|
17
|
-
/**
|
|
18
|
+
/** Snapshot of the document selection at the moment of the call. */
|
|
18
19
|
export interface SelectionSnapshot {
|
|
19
|
-
/**
|
|
20
|
+
/** The selected text. `''` when there is no selection or it was not requested. */
|
|
20
21
|
text: string;
|
|
21
|
-
/**
|
|
22
|
+
/** Whether a range is selected, as opposed to a caret only. */
|
|
22
23
|
hasRange: boolean;
|
|
23
|
-
/**
|
|
24
|
+
/** Id of the block the selection starts in, or `null`. */
|
|
24
25
|
blockId: string | null;
|
|
25
|
-
/**
|
|
26
|
+
/** The target that write operations (`insert`) consume. Opaque — handed back to the engine as-is. */
|
|
26
27
|
selectionTarget: unknown | null;
|
|
27
|
-
/**
|
|
28
|
+
/** Whether the selection is empty (caret only). */
|
|
28
29
|
empty: boolean;
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
+
/** A typing event the host reports to the recorder and to auto-text. */
|
|
31
32
|
export type TextInputEvent = {
|
|
32
33
|
kind: 'insert-text';
|
|
33
34
|
text: string;
|
|
@@ -39,40 +40,42 @@ export type TextInputEvent = {
|
|
|
39
40
|
kind: 'delete-forward';
|
|
40
41
|
};
|
|
41
42
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* What the toolkit needs from the editor. The SuperDoc v2 implementation is
|
|
44
|
+
* `createSuperdocHost`; tests use an in-memory double.
|
|
44
45
|
*/
|
|
45
46
|
export interface MacroHost {
|
|
46
47
|
commands: {
|
|
47
|
-
/**
|
|
48
|
+
/** Whether the engine recognizes the command. */
|
|
48
49
|
has(id: string): boolean;
|
|
49
|
-
/**
|
|
50
|
+
/** Runs a command from the engine's catalog and returns a normalized outcome. */
|
|
50
51
|
execute(id: string, payload?: unknown): Promise<MacroOutcome>;
|
|
51
|
-
/**
|
|
52
|
+
/** The known command ids, when the host can enumerate them. */
|
|
52
53
|
ids(): readonly string[];
|
|
53
54
|
};
|
|
54
|
-
/**
|
|
55
|
+
/** Inserts text at the caret (or at the end of the document when there is no caret). */
|
|
55
56
|
insertText(text: string): Promise<MacroOutcome>;
|
|
56
|
-
/**
|
|
57
|
+
/** Deletes characters backwards from the caret. */
|
|
57
58
|
deleteBackward(count: number): Promise<MacroOutcome>;
|
|
58
|
-
/**
|
|
59
|
+
/** Deletes characters forwards from the caret. */
|
|
60
|
+
deleteForward(count: number): Promise<MacroOutcome>;
|
|
61
|
+
/** Snapshot of the current selection. Never throws. */
|
|
59
62
|
getSelection(options?: {
|
|
60
63
|
includeText?: boolean;
|
|
61
64
|
}): Promise<SelectionSnapshot>;
|
|
62
|
-
/**
|
|
65
|
+
/** Replaces every occurrence of `query` with `replacement`. Returns how many were replaced. */
|
|
63
66
|
replaceAll(query: string, replacement: string): Promise<{
|
|
64
67
|
ok: boolean;
|
|
65
68
|
replaced: number;
|
|
66
69
|
message?: string;
|
|
67
70
|
}>;
|
|
68
|
-
/**
|
|
71
|
+
/** The full text of the document body. `''` when unavailable. */
|
|
69
72
|
getDocumentText(): Promise<string>;
|
|
70
|
-
/**
|
|
73
|
+
/** Observes every command the engine runs (from any source). Returns a dispose function. */
|
|
71
74
|
onCommand(listener: (id: string, payload: unknown) => void): () => void;
|
|
72
|
-
/**
|
|
75
|
+
/** Observes typing in the document. Returns a dispose function. */
|
|
73
76
|
onTextInput(listener: (event: TextInputEvent) => void): () => void;
|
|
74
77
|
}
|
|
75
|
-
/**
|
|
78
|
+
/** One step of a recorded macro. Fully JSON-serializable. */
|
|
76
79
|
export type MacroStep = {
|
|
77
80
|
type: 'command';
|
|
78
81
|
id: string;
|
|
@@ -89,7 +92,7 @@ export type MacroStep = {
|
|
|
89
92
|
type: 'delete-forward';
|
|
90
93
|
count: number;
|
|
91
94
|
};
|
|
92
|
-
/**
|
|
95
|
+
/** A recorded macro, as persisted and imported/exported. */
|
|
93
96
|
export interface RecordedMacro {
|
|
94
97
|
version: 1;
|
|
95
98
|
id: string;
|
|
@@ -99,21 +102,21 @@ export interface RecordedMacro {
|
|
|
99
102
|
shortcut?: string;
|
|
100
103
|
steps: MacroStep[];
|
|
101
104
|
}
|
|
102
|
-
/**
|
|
105
|
+
/** A written macro — a JavaScript script that runs against the toolkit's API. */
|
|
103
106
|
export interface SavedScript {
|
|
104
107
|
id: string;
|
|
105
108
|
name: string;
|
|
106
109
|
source: string;
|
|
107
110
|
shortcut?: string;
|
|
108
111
|
}
|
|
109
|
-
/**
|
|
112
|
+
/** A text snippet (AutoText building block). */
|
|
110
113
|
export interface Snippet {
|
|
111
114
|
id: string;
|
|
112
115
|
name: string;
|
|
113
|
-
/**
|
|
116
|
+
/** Snippet content. Supports `{{...}}` variables — see `renderSnippet`. */
|
|
114
117
|
text: string;
|
|
115
|
-
/**
|
|
118
|
+
/** Auto-text trigger word: typing the word followed by a space replaces it with the content. */
|
|
116
119
|
trigger?: string;
|
|
117
|
-
/**
|
|
120
|
+
/** Keyboard shortcut, e.g. `Ctrl+Alt+1`. */
|
|
118
121
|
shortcut?: string;
|
|
119
122
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Shared contracts.
|
|
3
3
|
*
|
|
4
|
-
* `MacroHost`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `MacroHost` is the single connection point to the editor: all three
|
|
5
|
+
* capabilities (scripts, recorder, snippets) work against it rather than
|
|
6
|
+
* against SuperDoc directly. That is what makes the toolkit testable with an
|
|
7
|
+
* in-memory double, and what lets another host (a different engine version,
|
|
8
|
+
* a different editor) plug in with one implementation of this interface.
|
|
8
9
|
*/
|
|
9
10
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superdoc-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Macro toolkit for SuperDoc-based editors: sandboxed scripted macros, a Word-style macro recorder, and snippets with auto-text expansion.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|