@embedpdf/plugin-form 3.0.0-next.10 → 3.0.0-next.11
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/dist/contract/host.cjs +3 -0
- package/dist/contract/host.d.cts +2 -0
- package/dist/contract/host.d.ts +2 -0
- package/dist/contract/host.js +2 -0
- package/dist/contract.cjs +7 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +8 -0
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +7 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.cjs +401 -557
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -247
- package/dist/index.d.ts +10 -247
- package/dist/index.js +389 -545
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +4 -0
- package/dist/internal.d.cts +3 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +3 -0
- package/dist/scripting.cjs +537 -0
- package/dist/scripting.cjs.map +1 -0
- package/dist/scripting.d.cts +60 -0
- package/dist/scripting.d.ts +60 -0
- package/dist/scripting.js +534 -0
- package/dist/scripting.js.map +1 -0
- package/dist/types-BlXdnNRf.d.ts +277 -0
- package/dist/types-Blr4QQM5.d.cts +277 -0
- package/dist/types-C1uIIYfn.cjs +16 -0
- package/dist/types-C1uIIYfn.cjs.map +1 -0
- package/dist/types-DkQmmcTG.js +12 -0
- package/dist/types-DkQmmcTG.js.map +1 -0
- package/package.json +25 -8
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { r as FormCommitResult, s as FormScriptingOptions } from "./types-Blr4QQM5.cjs";
|
|
2
|
+
import { DocumentMeta } from "@embedpdf/core";
|
|
3
|
+
import { DocumentHandle, FormFieldRef, FormFieldValue, PdfActionTree } from "@embedpdf/engine-core/runtime";
|
|
4
|
+
import { ScriptBudget, ScriptTransaction } from "@embedpdf/core-acrojs";
|
|
5
|
+
//#region src/scripting.d.ts
|
|
6
|
+
type FormScriptingControllerOptions = {
|
|
7
|
+
doc: DocumentHandle;
|
|
8
|
+
document(): DocumentMeta | null;
|
|
9
|
+
} & ({
|
|
10
|
+
/** The shared realm's transaction port ({@link ScriptTransaction}) —
|
|
11
|
+
* the actions plugin's per-document host. The K/V/C/F pipeline runs
|
|
12
|
+
* WHOLLY inside one transaction: snapshot fetch, every pass, and the
|
|
13
|
+
* engine commit — the commit-inside-the-boundary law. */
|
|
14
|
+
transaction<T>(body: (txn: ScriptTransaction) => Promise<T>): Promise<T>;
|
|
15
|
+
budget?: ScriptBudget;
|
|
16
|
+
} | {
|
|
17
|
+
/** Standalone convenience (stamp's detached documents, direct tests):
|
|
18
|
+
* the controller builds and OWNS its own realm via
|
|
19
|
+
* {@link createFormScriptingHost}; `dispose()` releases it. */
|
|
20
|
+
config: FormScriptingOptions;
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* A STANDALONE realm for one document, configured with the legacy
|
|
24
|
+
* `FormScriptingOptions` vocabulary — stamp's detached stamp-asset documents
|
|
25
|
+
* and direct controller tests construct their own host here; viewer
|
|
26
|
+
* documents get theirs from `actionsPlugin({ javascript })`.
|
|
27
|
+
*/
|
|
28
|
+
declare function createFormScriptingHost(options: {
|
|
29
|
+
doc: DocumentHandle;
|
|
30
|
+
document(): DocumentMeta | null;
|
|
31
|
+
config: FormScriptingOptions;
|
|
32
|
+
}): {
|
|
33
|
+
transaction<T>(body: (txn: ScriptTransaction) => Promise<T>): Promise<T>;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
};
|
|
36
|
+
declare class FormScriptingController {
|
|
37
|
+
private readonly options;
|
|
38
|
+
private disposed;
|
|
39
|
+
private readonly transaction;
|
|
40
|
+
private readonly transactionBudget;
|
|
41
|
+
private readonly ownedHost;
|
|
42
|
+
constructor(options: FormScriptingControllerOptions);
|
|
43
|
+
/** A shared realm belongs to its HOST (the actions plugin) — this only
|
|
44
|
+
* fences further transactions; a standalone-owned realm is released. */
|
|
45
|
+
dispose(): void;
|
|
46
|
+
commit(ref: FormFieldRef, proposed: FormFieldValue): Promise<FormCommitResult>;
|
|
47
|
+
/** Execute one originating widget's activation action (`/A`, including `/Next`). */
|
|
48
|
+
activate(ref: FormFieldRef, action: PdfActionTree): Promise<FormCommitResult>;
|
|
49
|
+
/** Run lazy document boot plus the `/CO` chain without a user field change.
|
|
50
|
+
* The anchor field resolves INSIDE the transaction (undefined ref). */
|
|
51
|
+
recalculate(): Promise<FormCommitResult>;
|
|
52
|
+
private transact;
|
|
53
|
+
private transactBody;
|
|
54
|
+
private failed;
|
|
55
|
+
private finishRejected;
|
|
56
|
+
}
|
|
57
|
+
declare function createFormScriptingController(options: FormScriptingControllerOptions): FormScriptingController;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { FormScriptingController, FormScriptingControllerOptions, createFormScriptingController, createFormScriptingHost };
|
|
60
|
+
//# sourceMappingURL=scripting.d.cts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { r as FormCommitResult, s as FormScriptingOptions } from "./types-BlXdnNRf.js";
|
|
2
|
+
import { DocumentMeta } from "@embedpdf/core";
|
|
3
|
+
import { DocumentHandle, FormFieldRef, FormFieldValue, PdfActionTree } from "@embedpdf/engine-core/runtime";
|
|
4
|
+
import { ScriptBudget, ScriptTransaction } from "@embedpdf/core-acrojs";
|
|
5
|
+
//#region src/scripting.d.ts
|
|
6
|
+
type FormScriptingControllerOptions = {
|
|
7
|
+
doc: DocumentHandle;
|
|
8
|
+
document(): DocumentMeta | null;
|
|
9
|
+
} & ({
|
|
10
|
+
/** The shared realm's transaction port ({@link ScriptTransaction}) —
|
|
11
|
+
* the actions plugin's per-document host. The K/V/C/F pipeline runs
|
|
12
|
+
* WHOLLY inside one transaction: snapshot fetch, every pass, and the
|
|
13
|
+
* engine commit — the commit-inside-the-boundary law. */
|
|
14
|
+
transaction<T>(body: (txn: ScriptTransaction) => Promise<T>): Promise<T>;
|
|
15
|
+
budget?: ScriptBudget;
|
|
16
|
+
} | {
|
|
17
|
+
/** Standalone convenience (stamp's detached documents, direct tests):
|
|
18
|
+
* the controller builds and OWNS its own realm via
|
|
19
|
+
* {@link createFormScriptingHost}; `dispose()` releases it. */
|
|
20
|
+
config: FormScriptingOptions;
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* A STANDALONE realm for one document, configured with the legacy
|
|
24
|
+
* `FormScriptingOptions` vocabulary — stamp's detached stamp-asset documents
|
|
25
|
+
* and direct controller tests construct their own host here; viewer
|
|
26
|
+
* documents get theirs from `actionsPlugin({ javascript })`.
|
|
27
|
+
*/
|
|
28
|
+
declare function createFormScriptingHost(options: {
|
|
29
|
+
doc: DocumentHandle;
|
|
30
|
+
document(): DocumentMeta | null;
|
|
31
|
+
config: FormScriptingOptions;
|
|
32
|
+
}): {
|
|
33
|
+
transaction<T>(body: (txn: ScriptTransaction) => Promise<T>): Promise<T>;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
};
|
|
36
|
+
declare class FormScriptingController {
|
|
37
|
+
private readonly options;
|
|
38
|
+
private disposed;
|
|
39
|
+
private readonly transaction;
|
|
40
|
+
private readonly transactionBudget;
|
|
41
|
+
private readonly ownedHost;
|
|
42
|
+
constructor(options: FormScriptingControllerOptions);
|
|
43
|
+
/** A shared realm belongs to its HOST (the actions plugin) — this only
|
|
44
|
+
* fences further transactions; a standalone-owned realm is released. */
|
|
45
|
+
dispose(): void;
|
|
46
|
+
commit(ref: FormFieldRef, proposed: FormFieldValue): Promise<FormCommitResult>;
|
|
47
|
+
/** Execute one originating widget's activation action (`/A`, including `/Next`). */
|
|
48
|
+
activate(ref: FormFieldRef, action: PdfActionTree): Promise<FormCommitResult>;
|
|
49
|
+
/** Run lazy document boot plus the `/CO` chain without a user field change.
|
|
50
|
+
* The anchor field resolves INSIDE the transaction (undefined ref). */
|
|
51
|
+
recalculate(): Promise<FormCommitResult>;
|
|
52
|
+
private transact;
|
|
53
|
+
private transactBody;
|
|
54
|
+
private failed;
|
|
55
|
+
private finishRejected;
|
|
56
|
+
}
|
|
57
|
+
declare function createFormScriptingController(options: FormScriptingControllerOptions): FormScriptingController;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { FormScriptingController, FormScriptingControllerOptions, createFormScriptingController, createFormScriptingHost };
|
|
60
|
+
//# sourceMappingURL=scripting.d.ts.map
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { DEFAULT_SCRIPT_BUDGET, createScriptHost, javaScriptProgramFromActionTree, resolveScriptIdentity, scriptFieldsFromSnapshot, seedFrom } from "@embedpdf/core-acrojs";
|
|
2
|
+
//#region src/scripting.ts
|
|
3
|
+
/**
|
|
4
|
+
* A STANDALONE realm for one document, configured with the legacy
|
|
5
|
+
* `FormScriptingOptions` vocabulary — stamp's detached stamp-asset documents
|
|
6
|
+
* and direct controller tests construct their own host here; viewer
|
|
7
|
+
* documents get theirs from `actionsPlugin({ javascript })`.
|
|
8
|
+
*/
|
|
9
|
+
function createFormScriptingHost(options) {
|
|
10
|
+
const { doc, config } = options;
|
|
11
|
+
const host = createScriptHost({
|
|
12
|
+
sandboxFactory: config.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox())),
|
|
13
|
+
document: () => {
|
|
14
|
+
const meta = options.document();
|
|
15
|
+
return {
|
|
16
|
+
id: doc.id,
|
|
17
|
+
fileName: config.fileName?.() ?? meta?.name ?? doc.id,
|
|
18
|
+
pageCount: meta?.pageCount ?? 0,
|
|
19
|
+
pageNumber: 0
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
identity: () => resolveScriptIdentity(doc, config.identity),
|
|
23
|
+
environment: (sequence) => {
|
|
24
|
+
const nowMs = config.now?.() ?? Date.now();
|
|
25
|
+
return {
|
|
26
|
+
nowMs,
|
|
27
|
+
utcOffsetMinutes: config.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
|
|
28
|
+
randomSeed: config.randomSeed?.() ?? seedFrom(doc.id, sequence)
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
bootSources: async () => {
|
|
32
|
+
return (doc.actions ? await doc.actions.read() : null)?.nameTreeScripts.map(({ action }) => javaScriptProgramFromActionTree(action)) ?? [];
|
|
33
|
+
},
|
|
34
|
+
...config.budget ? { budget: config.budget } : {}
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
transaction: host.transaction.bind(host),
|
|
38
|
+
dispose: () => host.dispose()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const refKey = (ref) => ref.kind === "objectNumber" ? `obj:${ref.fieldObjectNumber}` : `fqn:${ref.name}`;
|
|
42
|
+
const sameRef = (left, right) => refKey(left) === refKey(right);
|
|
43
|
+
const cloneValue = (value) => Array.isArray(value) ? [...value] : value;
|
|
44
|
+
const sameValue = (left, right) => Array.isArray(left) && Array.isArray(right) ? left.length === right.length && left.every((value, index) => value === right[index]) : left === right;
|
|
45
|
+
function cloneFields(fields) {
|
|
46
|
+
return fields.map((field) => ({
|
|
47
|
+
...field,
|
|
48
|
+
ref: { ...field.ref },
|
|
49
|
+
value: cloneValue(field.value),
|
|
50
|
+
defaultValue: cloneValue(field.defaultValue),
|
|
51
|
+
...field.options ? { options: field.options.map((option) => ({ ...option })) } : {}
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function fieldByRef(fields, ref) {
|
|
55
|
+
return fields.find((field) => sameRef(field.ref, ref));
|
|
56
|
+
}
|
|
57
|
+
function snapshotField(snapshot, ref) {
|
|
58
|
+
return snapshot.fields.find((field) => sameRef(field.ref, ref));
|
|
59
|
+
}
|
|
60
|
+
function scriptValueFromFormValue(field, value) {
|
|
61
|
+
if (field.family === "text" && value.type === "text") return value.value;
|
|
62
|
+
if ((field.family === "checkbox" || field.family === "radio") && value.type === "toggle") return value.state ?? "Off";
|
|
63
|
+
if (field.family === "combobox" && value.type === "choice") return value.values[0] ?? "";
|
|
64
|
+
if (field.family === "listbox" && value.type === "choice") return [...value.values];
|
|
65
|
+
throw new Error(`Form value type '${value.type}' does not match field family '${field.family}'`);
|
|
66
|
+
}
|
|
67
|
+
function formValueFromScriptValue(field) {
|
|
68
|
+
switch (field.family) {
|
|
69
|
+
case "text": return {
|
|
70
|
+
type: "text",
|
|
71
|
+
value: String(field.value ?? "")
|
|
72
|
+
};
|
|
73
|
+
case "checkbox":
|
|
74
|
+
case "radio": return {
|
|
75
|
+
type: "toggle",
|
|
76
|
+
state: field.value === null || field.value === void 0 || String(field.value) === "Off" ? null : String(field.value)
|
|
77
|
+
};
|
|
78
|
+
case "combobox": return {
|
|
79
|
+
type: "choice",
|
|
80
|
+
values: Array.isArray(field.value) ? field.value.slice(0, 1) : [String(field.value ?? "")]
|
|
81
|
+
};
|
|
82
|
+
case "listbox": return {
|
|
83
|
+
type: "choice",
|
|
84
|
+
values: Array.isArray(field.value) ? [...field.value] : [String(field.value ?? "")]
|
|
85
|
+
};
|
|
86
|
+
default: return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function applyEffects(overlay, effects) {
|
|
90
|
+
for (const effect of effects) {
|
|
91
|
+
if (effect.kind === "reset") {
|
|
92
|
+
for (const ref of effect.refs) {
|
|
93
|
+
const field = fieldByRef(overlay.fields, ref);
|
|
94
|
+
if (!field) continue;
|
|
95
|
+
field.value = cloneValue(field.defaultValue);
|
|
96
|
+
overlay.resetKeys.add(refKey(ref));
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const field = fieldByRef(overlay.fields, effect.ref);
|
|
101
|
+
if (!field) continue;
|
|
102
|
+
const key = refKey(effect.ref);
|
|
103
|
+
if (effect.kind === "setValue") {
|
|
104
|
+
field.value = scriptValueFromFormValue(field, effect.value);
|
|
105
|
+
overlay.resetKeys.delete(key);
|
|
106
|
+
} else if (effect.kind === "setDisplay") field.display = effect.display;
|
|
107
|
+
else overlay.appearances.set(key, {
|
|
108
|
+
ref: effect.ref,
|
|
109
|
+
text: effect.text
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function canonicalEffects(overlay) {
|
|
114
|
+
const effects = [];
|
|
115
|
+
const resets = [];
|
|
116
|
+
for (const field of overlay.fields) {
|
|
117
|
+
const original = fieldByRef(overlay.original, field.ref);
|
|
118
|
+
if (!original) continue;
|
|
119
|
+
if (!sameValue(original.value, field.value)) {
|
|
120
|
+
const key = refKey(field.ref);
|
|
121
|
+
if (overlay.resetKeys.has(key) && sameValue(field.value, field.defaultValue)) resets.push(field.ref);
|
|
122
|
+
else {
|
|
123
|
+
const value = formValueFromScriptValue(field);
|
|
124
|
+
if (value) effects.push({
|
|
125
|
+
kind: "setValue",
|
|
126
|
+
ref: field.ref,
|
|
127
|
+
value
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (original.display !== field.display) effects.push({
|
|
132
|
+
kind: "setDisplay",
|
|
133
|
+
ref: field.ref,
|
|
134
|
+
display: field.display
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (resets.length > 0) effects.unshift({
|
|
138
|
+
kind: "reset",
|
|
139
|
+
refs: resets
|
|
140
|
+
});
|
|
141
|
+
effects.push(...Array.from(overlay.appearances.values(), ({ ref, text }) => ({
|
|
142
|
+
kind: "setAppearanceText",
|
|
143
|
+
ref,
|
|
144
|
+
text
|
|
145
|
+
})));
|
|
146
|
+
return effects;
|
|
147
|
+
}
|
|
148
|
+
function scriptError(message) {
|
|
149
|
+
return {
|
|
150
|
+
kind: "invalid-output",
|
|
151
|
+
message
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function utf8Length(value) {
|
|
155
|
+
let length = 0;
|
|
156
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
157
|
+
const code = value.charCodeAt(index);
|
|
158
|
+
if (code < 128) length += 1;
|
|
159
|
+
else if (code < 2048) length += 2;
|
|
160
|
+
else if (code >= 55296 && code <= 56319 && index + 1 < value.length) {
|
|
161
|
+
length += 4;
|
|
162
|
+
index += 1;
|
|
163
|
+
} else length += 3;
|
|
164
|
+
}
|
|
165
|
+
return length;
|
|
166
|
+
}
|
|
167
|
+
function pageNumberFor(meta, field) {
|
|
168
|
+
const pon = field.widgets.find((widget) => widget.pageObjectNumber > 0)?.pageObjectNumber;
|
|
169
|
+
if (!meta || pon === void 0) return 0;
|
|
170
|
+
const index = meta.pages.findIndex((page) => page.pageObjectNumber === pon);
|
|
171
|
+
return Math.max(0, index);
|
|
172
|
+
}
|
|
173
|
+
function statusFromEffects(result) {
|
|
174
|
+
if (result.results.some(({ status }) => status === "failed" || status === "skipped")) return "failed";
|
|
175
|
+
if (result.results.some(({ status }) => status === "rejected")) return "failed";
|
|
176
|
+
return result.results.some(({ status }) => status === "applied") ? "applied" : "unchanged";
|
|
177
|
+
}
|
|
178
|
+
var FormScriptingController = class {
|
|
179
|
+
constructor(options) {
|
|
180
|
+
this.options = options;
|
|
181
|
+
this.disposed = false;
|
|
182
|
+
if ("transaction" in options) {
|
|
183
|
+
this.transaction = options.transaction.bind(options);
|
|
184
|
+
this.transactionBudget = options.budget;
|
|
185
|
+
this.ownedHost = null;
|
|
186
|
+
} else {
|
|
187
|
+
const host = createFormScriptingHost({
|
|
188
|
+
doc: options.doc,
|
|
189
|
+
document: () => options.document(),
|
|
190
|
+
config: options.config
|
|
191
|
+
});
|
|
192
|
+
this.transaction = host.transaction;
|
|
193
|
+
this.transactionBudget = options.config.budget;
|
|
194
|
+
this.ownedHost = host;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** A shared realm belongs to its HOST (the actions plugin) — this only
|
|
198
|
+
* fences further transactions; a standalone-owned realm is released. */
|
|
199
|
+
dispose() {
|
|
200
|
+
this.disposed = true;
|
|
201
|
+
this.ownedHost?.dispose();
|
|
202
|
+
}
|
|
203
|
+
async commit(ref, proposed) {
|
|
204
|
+
return this.transact(ref, proposed);
|
|
205
|
+
}
|
|
206
|
+
/** Execute one originating widget's activation action (`/A`, including `/Next`). */
|
|
207
|
+
async activate(ref, action) {
|
|
208
|
+
return this.transact(ref, void 0, action);
|
|
209
|
+
}
|
|
210
|
+
/** Run lazy document boot plus the `/CO` chain without a user field change.
|
|
211
|
+
* The anchor field resolves INSIDE the transaction (undefined ref). */
|
|
212
|
+
async recalculate() {
|
|
213
|
+
return this.transact(void 0);
|
|
214
|
+
}
|
|
215
|
+
async transact(refInput, proposed, activation) {
|
|
216
|
+
if (this.disposed) throw new Error("Form scripting controller is disposed");
|
|
217
|
+
return this.transaction((txn) => this.transactBody(txn, refInput, proposed, activation));
|
|
218
|
+
}
|
|
219
|
+
async transactBody(txn, refInput, proposed, activation) {
|
|
220
|
+
const snapshot = await this.options.doc.forms.list();
|
|
221
|
+
const ref = refInput ?? snapshot.calculationOrder.find((candidate) => candidate !== null && snapshotField(snapshot, candidate) !== void 0) ?? snapshot.fields[0]?.ref;
|
|
222
|
+
if (!ref) return {
|
|
223
|
+
status: "unchanged",
|
|
224
|
+
scripted: true,
|
|
225
|
+
effectsResult: null,
|
|
226
|
+
uiEffects: [],
|
|
227
|
+
diagnostics: []
|
|
228
|
+
};
|
|
229
|
+
const target = snapshotField(snapshot, ref);
|
|
230
|
+
if (!target) return this.failed([], [], scriptError(`Form field '${refKey(ref)}' no longer exists`));
|
|
231
|
+
const original = scriptFieldsFromSnapshot(snapshot);
|
|
232
|
+
const overlay = {
|
|
233
|
+
original,
|
|
234
|
+
fields: cloneFields(original),
|
|
235
|
+
resetKeys: /* @__PURE__ */ new Set(),
|
|
236
|
+
appearances: /* @__PURE__ */ new Map()
|
|
237
|
+
};
|
|
238
|
+
const targetInput = fieldByRef(overlay.fields, ref);
|
|
239
|
+
if (!targetInput) return this.failed([], [], scriptError(`Form field '${refKey(ref)}' has no script view`));
|
|
240
|
+
const hasProposedValue = proposed !== void 0;
|
|
241
|
+
let proposedValue = cloneValue(targetInput.value);
|
|
242
|
+
if (proposed) try {
|
|
243
|
+
proposedValue = scriptValueFromFormValue(targetInput, proposed);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return this.failed([], [], scriptError(error instanceof Error ? error.message : String(error)));
|
|
246
|
+
}
|
|
247
|
+
const uiEffects = [];
|
|
248
|
+
const diagnostics = [];
|
|
249
|
+
const pageNumber = pageNumberFor(this.options.document(), target);
|
|
250
|
+
const budget = this.transactionBudget ?? DEFAULT_SCRIPT_BUDGET;
|
|
251
|
+
let executionMs = 0;
|
|
252
|
+
let aggregateOutputSize = 0;
|
|
253
|
+
const consume = (output, phase) => {
|
|
254
|
+
aggregateOutputSize += utf8Length(JSON.stringify(output));
|
|
255
|
+
uiEffects.push(...output.uiEffects.map((effect) => ({
|
|
256
|
+
...effect,
|
|
257
|
+
phase
|
|
258
|
+
})));
|
|
259
|
+
diagnostics.push(...output.diagnostics);
|
|
260
|
+
if (aggregateOutputSize > budget.maxOutputBytes) return {
|
|
261
|
+
kind: "budget",
|
|
262
|
+
message: `Form script transaction output exceeded ${budget.maxOutputBytes} bytes`
|
|
263
|
+
};
|
|
264
|
+
return output.error ?? null;
|
|
265
|
+
};
|
|
266
|
+
const remainingBudget = () => {
|
|
267
|
+
const remaining = budget.maxExecutionMs - executionMs;
|
|
268
|
+
return remaining <= 0 ? null : {
|
|
269
|
+
...budget,
|
|
270
|
+
maxExecutionMs: remaining
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
const world = (fields, event) => ({
|
|
274
|
+
fields,
|
|
275
|
+
pageNumber,
|
|
276
|
+
event
|
|
277
|
+
});
|
|
278
|
+
const run = async (source, input) => {
|
|
279
|
+
const remaining = remainingBudget();
|
|
280
|
+
if (!remaining) return { error: {
|
|
281
|
+
kind: "budget",
|
|
282
|
+
message: "Form script transaction timed out"
|
|
283
|
+
} };
|
|
284
|
+
let output;
|
|
285
|
+
try {
|
|
286
|
+
const startedAt = Date.now();
|
|
287
|
+
output = await txn.run(source, input, remaining);
|
|
288
|
+
executionMs += Date.now() - startedAt;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return { error: scriptError(error instanceof Error ? error.message : String(error)) };
|
|
291
|
+
}
|
|
292
|
+
const error = consume(output, "user");
|
|
293
|
+
return error ? { error } : { output };
|
|
294
|
+
};
|
|
295
|
+
{
|
|
296
|
+
let boot = null;
|
|
297
|
+
try {
|
|
298
|
+
boot = await txn.boot(world(overlay.fields, { kind: "name-tree-boot" }), budget);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
code: "script-error",
|
|
302
|
+
message: `Document boot script failed (continuing without it): ${error instanceof Error ? error.message : String(error)}`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (boot) {
|
|
306
|
+
const bootError = consume(boot, "boot");
|
|
307
|
+
if (!bootError) applyEffects(overlay, boot.formEffects);
|
|
308
|
+
else diagnostics.push({
|
|
309
|
+
code: "script-error",
|
|
310
|
+
message: `Document boot script failed (continuing without it): ${bootError.message}`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const bootEffects = canonicalEffects(overlay);
|
|
315
|
+
if (activation) {
|
|
316
|
+
let program;
|
|
317
|
+
try {
|
|
318
|
+
program = javaScriptProgramFromActionTree(activation);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
|
|
321
|
+
}
|
|
322
|
+
if (program) {
|
|
323
|
+
const activated = await run(program, world(overlay.fields, {
|
|
324
|
+
kind: "widget-activate",
|
|
325
|
+
target: ref,
|
|
326
|
+
source: ref,
|
|
327
|
+
value: targetInput.value
|
|
328
|
+
}));
|
|
329
|
+
if (activated.error) return this.failed(uiEffects, diagnostics, activated.error);
|
|
330
|
+
applyEffects(overlay, activated.output.formEffects);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!activation && hasProposedValue && target.actions?.keystroke && target.family === "text") {
|
|
334
|
+
let program;
|
|
335
|
+
try {
|
|
336
|
+
program = javaScriptProgramFromActionTree(target.actions.keystroke);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
|
|
339
|
+
}
|
|
340
|
+
if (program) {
|
|
341
|
+
const oldValue = String(targetInput.value ?? "");
|
|
342
|
+
const typing = await run(program, world(overlay.fields, {
|
|
343
|
+
kind: "field-keystroke",
|
|
344
|
+
target: ref,
|
|
345
|
+
source: ref,
|
|
346
|
+
value: oldValue,
|
|
347
|
+
change: String(proposedValue ?? ""),
|
|
348
|
+
selStart: 0,
|
|
349
|
+
selEnd: oldValue.length,
|
|
350
|
+
willCommit: false
|
|
351
|
+
}));
|
|
352
|
+
if (typing.error && typing.error.kind !== "exception") return this.failed(uiEffects, diagnostics, typing.error);
|
|
353
|
+
if (typing.error) diagnostics.push({
|
|
354
|
+
code: "script-error",
|
|
355
|
+
message: `Keystroke script failed (kept the typed value): ${typing.error.message}`
|
|
356
|
+
});
|
|
357
|
+
else {
|
|
358
|
+
if (!typing.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
|
|
359
|
+
applyEffects(overlay, typing.output.formEffects);
|
|
360
|
+
const event = typing.output.event;
|
|
361
|
+
const base = String(event.value ?? "");
|
|
362
|
+
proposedValue = base.slice(0, event.selStart) + event.change + base.slice(Math.max(event.selStart, event.selEnd));
|
|
363
|
+
}
|
|
364
|
+
const commit = await run(program, world(overlay.fields, {
|
|
365
|
+
kind: "field-keystroke",
|
|
366
|
+
target: ref,
|
|
367
|
+
source: ref,
|
|
368
|
+
value: proposedValue,
|
|
369
|
+
change: "",
|
|
370
|
+
selStart: 0,
|
|
371
|
+
selEnd: 0,
|
|
372
|
+
willCommit: true
|
|
373
|
+
}));
|
|
374
|
+
if (commit.error && commit.error.kind !== "exception") return this.failed(uiEffects, diagnostics, commit.error);
|
|
375
|
+
if (commit.error) diagnostics.push({
|
|
376
|
+
code: "script-error",
|
|
377
|
+
message: `Keystroke commit script failed (kept the typed value): ${commit.error.message}`
|
|
378
|
+
});
|
|
379
|
+
else {
|
|
380
|
+
if (!commit.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
|
|
381
|
+
applyEffects(overlay, commit.output.formEffects);
|
|
382
|
+
proposedValue = cloneValue(commit.output.event.value);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!activation && hasProposedValue) {
|
|
387
|
+
targetInput.value = cloneValue(proposedValue);
|
|
388
|
+
overlay.resetKeys.delete(refKey(ref));
|
|
389
|
+
}
|
|
390
|
+
if (!activation && hasProposedValue && target.actions?.validate) {
|
|
391
|
+
let program;
|
|
392
|
+
try {
|
|
393
|
+
program = javaScriptProgramFromActionTree(target.actions.validate);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
|
|
396
|
+
}
|
|
397
|
+
if (program) {
|
|
398
|
+
const validation = await run(program, world(overlay.fields, {
|
|
399
|
+
kind: "field-validate",
|
|
400
|
+
target: ref,
|
|
401
|
+
source: ref,
|
|
402
|
+
value: proposedValue,
|
|
403
|
+
willCommit: true
|
|
404
|
+
}));
|
|
405
|
+
if (validation.error && validation.error.kind !== "exception") return this.failed(uiEffects, diagnostics, validation.error);
|
|
406
|
+
if (validation.error) diagnostics.push({
|
|
407
|
+
code: "script-error",
|
|
408
|
+
message: `Validate script failed (accepted the value): ${validation.error.message}`
|
|
409
|
+
});
|
|
410
|
+
else {
|
|
411
|
+
if (!validation.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
|
|
412
|
+
targetInput.value = cloneValue(validation.output.event.value);
|
|
413
|
+
applyEffects(overlay, validation.output.formEffects);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const formatRefs = !activation && hasProposedValue ? [ref] : [];
|
|
418
|
+
for (const calculationRef of activation ? [] : snapshot.calculationOrder) {
|
|
419
|
+
if (!calculationRef) continue;
|
|
420
|
+
const calculated = snapshotField(snapshot, calculationRef);
|
|
421
|
+
const tree = calculated?.actions?.calculate;
|
|
422
|
+
if (!calculated || !tree) continue;
|
|
423
|
+
let program;
|
|
424
|
+
try {
|
|
425
|
+
program = javaScriptProgramFromActionTree(tree);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
|
|
428
|
+
}
|
|
429
|
+
const calculatedInput = fieldByRef(overlay.fields, calculationRef);
|
|
430
|
+
if (program && calculatedInput) {
|
|
431
|
+
const calculation = await run(program, world(overlay.fields, {
|
|
432
|
+
kind: "field-calculate",
|
|
433
|
+
target: calculationRef,
|
|
434
|
+
...hasProposedValue ? { source: ref } : {},
|
|
435
|
+
value: calculatedInput.value
|
|
436
|
+
}));
|
|
437
|
+
if (calculation.error && calculation.error.kind !== "exception") return this.failed(uiEffects, diagnostics, calculation.error);
|
|
438
|
+
if (calculation.error) diagnostics.push({
|
|
439
|
+
code: "script-error",
|
|
440
|
+
message: `Calculate script failed (field left unchanged): ${calculation.error.message}`
|
|
441
|
+
});
|
|
442
|
+
else applyEffects(overlay, calculation.output.formEffects);
|
|
443
|
+
}
|
|
444
|
+
if (!formatRefs.some((candidate) => sameRef(candidate, calculationRef))) formatRefs.push(calculationRef);
|
|
445
|
+
}
|
|
446
|
+
for (const formatRef of formatRefs) {
|
|
447
|
+
const field = snapshotField(snapshot, formatRef);
|
|
448
|
+
const tree = field?.actions?.format;
|
|
449
|
+
const overlayField = fieldByRef(overlay.fields, formatRef);
|
|
450
|
+
if (!field || !tree || !overlayField) continue;
|
|
451
|
+
let program;
|
|
452
|
+
try {
|
|
453
|
+
program = javaScriptProgramFromActionTree(tree);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
|
|
456
|
+
}
|
|
457
|
+
if (!program) continue;
|
|
458
|
+
const format = await run(program, world(overlay.fields, {
|
|
459
|
+
kind: "field-format",
|
|
460
|
+
target: formatRef,
|
|
461
|
+
...hasProposedValue ? { source: ref } : {},
|
|
462
|
+
value: overlayField.value
|
|
463
|
+
}));
|
|
464
|
+
if (format.error && format.error.kind !== "exception") return this.failed(uiEffects, diagnostics, format.error);
|
|
465
|
+
if (format.error) {
|
|
466
|
+
diagnostics.push({
|
|
467
|
+
code: "script-error",
|
|
468
|
+
message: `Format script failed (kept the raw value): ${format.error.message}`
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
applyEffects(overlay, format.output.formEffects);
|
|
473
|
+
}
|
|
474
|
+
const effects = canonicalEffects(overlay);
|
|
475
|
+
if (effects.length + uiEffects.length > budget.maxEffects) return this.failed(uiEffects, diagnostics, {
|
|
476
|
+
kind: "budget",
|
|
477
|
+
message: `Form script transaction exceeded ${budget.maxEffects} effects`
|
|
478
|
+
});
|
|
479
|
+
if (effects.length === 0) return {
|
|
480
|
+
status: "unchanged",
|
|
481
|
+
scripted: true,
|
|
482
|
+
effectsResult: null,
|
|
483
|
+
uiEffects,
|
|
484
|
+
diagnostics
|
|
485
|
+
};
|
|
486
|
+
if (!this.options.doc.forms.applyEffects) return this.failed(uiEffects, diagnostics, scriptError("This engine does not support batched form effects"));
|
|
487
|
+
const effectsResult = await this.options.doc.forms.applyEffects(effects);
|
|
488
|
+
const status = statusFromEffects(effectsResult);
|
|
489
|
+
return {
|
|
490
|
+
status,
|
|
491
|
+
scripted: true,
|
|
492
|
+
effectsResult,
|
|
493
|
+
uiEffects,
|
|
494
|
+
diagnostics,
|
|
495
|
+
...status === "failed" ? { error: scriptError("One or more native form effects failed") } : {}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
failed(uiEffects, diagnostics, error) {
|
|
499
|
+
return {
|
|
500
|
+
status: "failed",
|
|
501
|
+
scripted: true,
|
|
502
|
+
effectsResult: null,
|
|
503
|
+
uiEffects,
|
|
504
|
+
diagnostics,
|
|
505
|
+
error
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
async finishRejected(bootEffects, uiEffects, diagnostics) {
|
|
509
|
+
if (bootEffects.length === 0) return {
|
|
510
|
+
status: "rejected",
|
|
511
|
+
scripted: true,
|
|
512
|
+
effectsResult: null,
|
|
513
|
+
uiEffects,
|
|
514
|
+
diagnostics
|
|
515
|
+
};
|
|
516
|
+
if (!this.options.doc.forms.applyEffects) return this.failed(uiEffects, diagnostics, scriptError("Batched form effects are unavailable"));
|
|
517
|
+
const effectsResult = await this.options.doc.forms.applyEffects(bootEffects);
|
|
518
|
+
if (statusFromEffects(effectsResult) === "failed") return this.failed(uiEffects, diagnostics, scriptError("A name-tree boot effect failed"));
|
|
519
|
+
return {
|
|
520
|
+
status: "rejected",
|
|
521
|
+
scripted: true,
|
|
522
|
+
effectsResult,
|
|
523
|
+
uiEffects,
|
|
524
|
+
diagnostics
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
function createFormScriptingController(options) {
|
|
529
|
+
return new FormScriptingController(options);
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
export { FormScriptingController, createFormScriptingController, createFormScriptingHost };
|
|
533
|
+
|
|
534
|
+
//# sourceMappingURL=scripting.js.map
|