@embedpdf/plugin-actions 3.0.0-next.14 → 3.0.0-next.16
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 +13 -1
- package/dist/contract/host.cjs.map +1 -1
- package/dist/contract/host.d.cts +3 -7
- package/dist/contract/host.d.ts +3 -7
- package/dist/contract/host.js +10 -3
- package/dist/contract/host.js.map +1 -1
- package/dist/contract-BTPW8b4L.d.cts +557 -0
- package/dist/contract-Cn-7mBY8.d.ts +557 -0
- package/dist/contract-CwN3NAXU.js +175 -0
- package/dist/contract-CwN3NAXU.js.map +1 -0
- package/dist/contract-_HB_UliT.cjs +210 -0
- package/dist/contract-_HB_UliT.cjs.map +1 -0
- package/dist/contract.cjs +5 -4
- package/dist/contract.d.cts +2 -3
- package/dist/contract.d.ts +2 -3
- package/dist/contract.js +2 -3
- package/dist/host-contract-BbODswDs.d.ts +165 -0
- package/dist/host-contract-D22Gz-qt.d.cts +165 -0
- package/dist/index.cjs +1588 -1072
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +1586 -1072
- package/dist/index.js.map +1 -1
- package/package.json +7 -11
- package/dist/contract-C1K3H4RP.cjs +0 -89
- package/dist/contract-C1K3H4RP.cjs.map +0 -1
- package/dist/contract-COk-w3Fo.d.ts +0 -50
- package/dist/contract-DAem-EST.js +0 -78
- package/dist/contract-DAem-EST.js.map +0 -1
- package/dist/contract-Os5y4NaD.d.cts +0 -50
- package/dist/internal.cjs +0 -3
- package/dist/internal.d.cts +0 -3
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/types-BXJLAKXi.cjs +0 -52
- package/dist/types-BXJLAKXi.cjs.map +0 -1
- package/dist/types-D-DoiESj.d.cts +0 -504
- package/dist/types-D-DoiESj.d.ts +0 -504
- package/dist/types-DgBMy5nE.js +0 -35
- package/dist/types-DgBMy5nE.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,68 +1,291 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const require_contract = require("./contract-_HB_UliT.cjs");
|
|
3
|
+
const require_contract_host = require("./contract/host.cjs");
|
|
4
4
|
let _embedpdf_core = require("@embedpdf/core");
|
|
5
|
+
let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
|
|
5
6
|
let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
|
|
6
|
-
//#region src/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
//#region src/services/annotation-rows.ts
|
|
8
|
+
/**
|
|
9
|
+
* The annotations an action can live on, of both read families: a page's
|
|
10
|
+
* annotations (`doc.annotations`, `doc.annotate.read`) and its widgets, which
|
|
11
|
+
* come with the form (`ActionsForm`, `doc.forms.read`). Each family is read
|
|
12
|
+
* only when the session may read it, so a fill-only session still finds its
|
|
13
|
+
* widgets' actions.
|
|
14
|
+
*/
|
|
15
|
+
/** Every annotation of a page the session may read: its annotations, then its widgets. */
|
|
16
|
+
async function readPageAnnotations(ctx, form, page, read = (task) => task) {
|
|
17
|
+
const [annotations, widgets] = await Promise.all([ctx.allows("doc.annotate.read") ? read(ctx.doc.page(page).annotations.list()).then((list) => list.annotations) : Promise.resolve([]), ctx.allows("doc.forms.read") ? form.read(read).then((snapshot) => snapshot.widgets.filter((widget) => widget.page.objectNumber === page.objectNumber)) : Promise.resolve([])]);
|
|
18
|
+
return [...annotations, ...widgets];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The annotation `ref` names, of either family, or `null` when the session
|
|
22
|
+
* can't see it: the form's widgets first (most actions a viewer runs are a
|
|
23
|
+
* field's), then the page's annotations.
|
|
24
|
+
*/
|
|
25
|
+
async function readAnnotation(ctx, form, ref, read = (task) => task) {
|
|
26
|
+
const key = (0, _embedpdf_engine_core_runtime.annotationKey)(ref);
|
|
27
|
+
const matches = (candidate) => (0, _embedpdf_engine_core_runtime.annotationKey)(candidate.ref) === key;
|
|
28
|
+
if (ctx.allows("doc.forms.read")) {
|
|
29
|
+
const widget = (await form.read(read)).widgets.find(matches);
|
|
30
|
+
if (widget) return widget;
|
|
31
|
+
}
|
|
32
|
+
if (!ctx.allows("doc.annotate.read")) return null;
|
|
33
|
+
return (await read(ctx.doc.page(ref.page).annotations.list())).annotations.find(matches) ?? null;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/dispatch/fold.ts
|
|
37
|
+
const foldSteps = (steps, diagnostics) => {
|
|
38
|
+
const statuses = steps.map((step) => step.result.status);
|
|
10
39
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
status: steps.length === 0 ? "inert" : statuses.every((stepStatus) => stepStatus === "refused") ? "refused" : statuses.every((stepStatus) => stepStatus === "inert") ? "inert" : statuses.every((stepStatus) => stepStatus === "executed") ? "executed" : "partial",
|
|
41
|
+
steps,
|
|
42
|
+
diagnostics
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/dispatch/dispatcher.ts
|
|
47
|
+
/**
|
|
48
|
+
* The dispatcher: the one serialized queue every tree rides. `execute` runs
|
|
49
|
+
* a resolved tree; `dispatch` resolves a trigger inside the queued operation
|
|
50
|
+
* (submission order is execution order) and fans out through the lifecycle
|
|
51
|
+
* areas. `runAndEmit` is the shared per-tree unit: run, then `onExecuted`.
|
|
52
|
+
*
|
|
53
|
+
* A caller's signal skips an operation that hasn't started, stops a
|
|
54
|
+
* trigger's reads, and stops a tree before its next node. `execute` then
|
|
55
|
+
* rejects `operation-cancelled`; `dispatch` never rejects, so it resolves
|
|
56
|
+
* `cancelled`.
|
|
57
|
+
*/
|
|
58
|
+
/** The source a trigger reports for: what its diagnostics carry before a tree is found. */
|
|
59
|
+
const sourceOfTrigger = (trigger) => {
|
|
60
|
+
switch (trigger.scope) {
|
|
61
|
+
case "activate":
|
|
62
|
+
case "annotation": return trigger.source ?? {
|
|
63
|
+
kind: "annotation",
|
|
64
|
+
annotation: trigger.ref,
|
|
65
|
+
page: trigger.page
|
|
66
|
+
};
|
|
67
|
+
case "page": return {
|
|
68
|
+
kind: "page",
|
|
69
|
+
page: trigger.page
|
|
70
|
+
};
|
|
71
|
+
case "document": return { kind: "document" };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const cancelled = (operation, signal) => new _embedpdf_core.PluginError("operation-cancelled", "actions", `${operation} was cancelled`, { cause: signal?.reason });
|
|
75
|
+
function createDispatcher(ctx, services, { run }, { triggerEnabled, lifecycleAnnotationsOf, planPageSteps }, openSequence, { runDocumentEventOp }) {
|
|
76
|
+
const { executed, reportDiagnostic } = services.events;
|
|
77
|
+
const { decisionFor } = services.policy;
|
|
78
|
+
const { allowsPrint } = services.authority;
|
|
79
|
+
const { enqueue, budget } = services.queue;
|
|
80
|
+
const { readDocumentActions, DOC_EVENT_TREES } = services.catalog;
|
|
81
|
+
const { form } = services;
|
|
82
|
+
const { noteUserActivity } = openSequence;
|
|
83
|
+
/** Run one tree and announce it: the shared per-tree unit (queued by
|
|
84
|
+
* `execute`; called inline per step by the queued trigger resolver). */
|
|
85
|
+
const runAndEmit = async (tree, actionContext, signal) => {
|
|
86
|
+
const result = await run(tree, actionContext, signal);
|
|
87
|
+
executed.emit({
|
|
88
|
+
tree,
|
|
89
|
+
result,
|
|
90
|
+
source: actionContext.source
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
const execute = (tree, actionContext, options) => {
|
|
95
|
+
const signal = options?.signal;
|
|
96
|
+
if (actionContext.origin === "user") noteUserActivity();
|
|
97
|
+
return enqueue(async () => {
|
|
98
|
+
budget.scriptNodes = 0;
|
|
99
|
+
const result = await runAndEmit(tree, actionContext, signal);
|
|
100
|
+
if (result.status === "cancelled") throw cancelled("actions.execute", signal);
|
|
101
|
+
return result;
|
|
102
|
+
}, options);
|
|
103
|
+
};
|
|
104
|
+
const canExecute = (tree, actionContext) => {
|
|
105
|
+
if (tree.incomplete || !tree.root) return false;
|
|
106
|
+
const decision = decisionFor(tree.root, actionContext.origin);
|
|
107
|
+
return decision === "allow" || decision === "adapter";
|
|
108
|
+
};
|
|
109
|
+
const runSteps = async (steps, origin, event, diagnostics, signal) => {
|
|
110
|
+
const results = [];
|
|
111
|
+
for (const step of steps) {
|
|
112
|
+
if (signal?.aborted) break;
|
|
113
|
+
const result = await runAndEmit(step.tree, {
|
|
114
|
+
origin,
|
|
115
|
+
source: step.source,
|
|
116
|
+
event
|
|
117
|
+
}, signal);
|
|
118
|
+
results.push({
|
|
119
|
+
source: step.source,
|
|
120
|
+
tree: step.tree,
|
|
121
|
+
result
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (signal?.aborted) return {
|
|
125
|
+
status: "cancelled",
|
|
126
|
+
steps: results,
|
|
127
|
+
diagnostics
|
|
128
|
+
};
|
|
129
|
+
return foldSteps(results, diagnostics);
|
|
130
|
+
};
|
|
131
|
+
/** Everything a trigger needs, reads included, inside the queued operation:
|
|
132
|
+
* submission order is execution order. Never throws. */
|
|
133
|
+
const resolveAndRun = async (trigger, signal) => {
|
|
134
|
+
const diagnostics = [];
|
|
135
|
+
const diagnose = (diagnostic) => {
|
|
136
|
+
diagnostics.push(diagnostic);
|
|
137
|
+
reportDiagnostic(diagnostic, { source: sourceOfTrigger(trigger) });
|
|
138
|
+
};
|
|
139
|
+
/** A read the caller's signal can stop. */
|
|
140
|
+
const read = (task) => ctx.cancellable(signal, task);
|
|
141
|
+
try {
|
|
142
|
+
if (!triggerEnabled(trigger)) {
|
|
143
|
+
diagnose({
|
|
144
|
+
code: "trigger-disabled",
|
|
145
|
+
message: `${trigger.scope}: trigger family disabled by config`
|
|
146
|
+
});
|
|
26
147
|
return {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
148
|
+
status: "inert",
|
|
149
|
+
steps: [],
|
|
150
|
+
diagnostics
|
|
30
151
|
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
152
|
+
}
|
|
153
|
+
const origin = require_contract.triggerOriginOf(trigger);
|
|
154
|
+
switch (trigger.scope) {
|
|
155
|
+
case "activate":
|
|
156
|
+
case "annotation": {
|
|
157
|
+
const event = trigger.scope === "activate" ? "activate" : trigger.event;
|
|
158
|
+
const annotation = await readAnnotation(ctx, form, trigger.ref, read);
|
|
159
|
+
if (event === "mouseUp" && annotation?.actions?.activate) return {
|
|
160
|
+
status: "inert",
|
|
161
|
+
steps: [],
|
|
162
|
+
diagnostics
|
|
163
|
+
};
|
|
164
|
+
const tree = annotation?.actions?.[event];
|
|
165
|
+
if (!tree?.root && !tree?.incomplete) return {
|
|
166
|
+
status: "inert",
|
|
167
|
+
steps: [],
|
|
168
|
+
diagnostics
|
|
169
|
+
};
|
|
170
|
+
const source = sourceOfTrigger(trigger);
|
|
171
|
+
return await runSteps([{
|
|
172
|
+
source,
|
|
173
|
+
tree
|
|
174
|
+
}], origin, require_contract.eventOf(trigger), diagnostics, signal);
|
|
175
|
+
}
|
|
176
|
+
case "page": {
|
|
177
|
+
const lifecycle = await lifecycleAnnotationsOf(trigger.page);
|
|
178
|
+
const pageActions = ctx.getPage(trigger.page)?.actions;
|
|
179
|
+
const steps = planPageSteps(trigger.event, trigger.page, pageActions, lifecycle);
|
|
180
|
+
return await runSteps(steps, origin, require_contract.eventOf(trigger), diagnostics, signal);
|
|
181
|
+
}
|
|
182
|
+
case "document":
|
|
183
|
+
if (trigger.event !== "open") return await runDocumentEventOp(trigger.event, diagnostics, diagnose);
|
|
184
|
+
if (!openSequence.claim()) {
|
|
185
|
+
diagnose({
|
|
186
|
+
code: "open-sequence-replayed",
|
|
187
|
+
message: "document open sequence already fired for this document"
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
status: "inert",
|
|
191
|
+
steps: [],
|
|
192
|
+
diagnostics
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return await openSequence.run();
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if ((0, _embedpdf_core.isPluginError)(error, "operation-cancelled") && signal?.aborted) return {
|
|
199
|
+
status: "cancelled",
|
|
200
|
+
steps: [],
|
|
201
|
+
diagnostics
|
|
202
|
+
};
|
|
203
|
+
diagnose({
|
|
204
|
+
code: "trigger-failed",
|
|
205
|
+
message: `trigger resolution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
status: "refused",
|
|
209
|
+
steps: [],
|
|
210
|
+
diagnostics
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const dispatch = (trigger, options) => {
|
|
215
|
+
const signal = options?.signal;
|
|
216
|
+
if (require_contract.triggerOriginOf(trigger) === "user") noteUserActivity();
|
|
217
|
+
return enqueue(() => {
|
|
218
|
+
budget.scriptNodes = 0;
|
|
219
|
+
return resolveAndRun(trigger, signal);
|
|
220
|
+
}, options).catch(() => ({
|
|
221
|
+
status: "cancelled",
|
|
222
|
+
steps: [],
|
|
223
|
+
diagnostics: []
|
|
224
|
+
}));
|
|
225
|
+
};
|
|
226
|
+
/** The tree `executeNamed` runs, and its context: a click from your code unless `context` says otherwise. */
|
|
227
|
+
const namedTree = (name) => ({
|
|
228
|
+
root: {
|
|
229
|
+
type: "named",
|
|
230
|
+
subtype: "Named",
|
|
231
|
+
name,
|
|
232
|
+
next: []
|
|
233
|
+
},
|
|
234
|
+
incomplete: false,
|
|
235
|
+
warningFlags: 0,
|
|
236
|
+
warnings: []
|
|
237
|
+
});
|
|
238
|
+
const namedContext = (context) => ({
|
|
239
|
+
origin: "user",
|
|
240
|
+
source: { kind: "api" },
|
|
241
|
+
event: { scope: "activate" },
|
|
242
|
+
...context
|
|
243
|
+
});
|
|
244
|
+
const executeNamed = (name, context, options) => execute(namedTree(name), namedContext(context), options);
|
|
245
|
+
const canExecuteNamed = (name, context) => canExecute(namedTree(name), namedContext(context)) && (name !== "Print" || allowsPrint());
|
|
246
|
+
/** The raw tree behind a source, read from the document, with no dispatch
|
|
247
|
+
* rules applied (the /A-shadows-U rule lives in `resolveAndRun`). */
|
|
248
|
+
const readActionTree = async (source) => {
|
|
249
|
+
switch (source.kind) {
|
|
250
|
+
case "annotation": return (await readAnnotation(ctx, form, source.annotation))?.actions?.[source.event ?? "activate"] ?? null;
|
|
251
|
+
case "field": {
|
|
252
|
+
const { fields } = await form.read();
|
|
253
|
+
const target = source.field;
|
|
254
|
+
return fields.find((candidate) => target.kind === "objectNumber" ? candidate.ref.kind === "objectNumber" && candidate.ref.objectNumber === target.objectNumber : candidate.name === target.name)?.actions?.[source.event] ?? null;
|
|
255
|
+
}
|
|
256
|
+
case "page": return ctx.getPage(source.page)?.actions?.[source.event ?? "open"] ?? null;
|
|
257
|
+
case "outline": {
|
|
258
|
+
const { items } = await ctx.doc.outline.get();
|
|
259
|
+
return items.find((candidate) => candidate.ref.objectNumber === source.item.objectNumber)?.action ?? null;
|
|
260
|
+
}
|
|
261
|
+
case "document": {
|
|
262
|
+
const snapshot = await readDocumentActions();
|
|
263
|
+
if (!snapshot) return null;
|
|
264
|
+
const event = source.event ?? "open";
|
|
265
|
+
return (event === "open" ? snapshot.openAction : snapshot[DOC_EVENT_TREES[event]]) ?? null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
runAndEmit,
|
|
271
|
+
dispatch,
|
|
272
|
+
api: {
|
|
273
|
+
execute,
|
|
274
|
+
canExecute,
|
|
275
|
+
executeNamed,
|
|
276
|
+
canExecuteNamed,
|
|
277
|
+
getActionTree: (source, options) => ctx.cancellable(options?.signal, readActionTree(source)),
|
|
278
|
+
dispatch,
|
|
279
|
+
canDispatch: (trigger) => triggerEnabled(trigger),
|
|
280
|
+
prepareClose: (options) => dispatch({
|
|
281
|
+
scope: "document",
|
|
282
|
+
event: "will-close"
|
|
283
|
+
}, options)
|
|
284
|
+
}
|
|
35
285
|
};
|
|
36
286
|
}
|
|
37
287
|
//#endregion
|
|
38
|
-
//#region src/
|
|
39
|
-
const ALLOW_ALL = {
|
|
40
|
-
user: "allow",
|
|
41
|
-
hover: "allow",
|
|
42
|
-
lifecycle: "allow"
|
|
43
|
-
};
|
|
44
|
-
const DEFAULT_POLICY = {
|
|
45
|
-
goto: ALLOW_ALL,
|
|
46
|
-
named: ALLOW_ALL,
|
|
47
|
-
hide: ALLOW_ALL,
|
|
48
|
-
"reset-form": ALLOW_ALL,
|
|
49
|
-
javascript: ALLOW_ALL,
|
|
50
|
-
uri: {
|
|
51
|
-
user: "adapter",
|
|
52
|
-
hover: "report",
|
|
53
|
-
lifecycle: "report"
|
|
54
|
-
},
|
|
55
|
-
print: {
|
|
56
|
-
user: "adapter",
|
|
57
|
-
hover: "block",
|
|
58
|
-
lifecycle: "block"
|
|
59
|
-
},
|
|
60
|
-
"submit-form": {
|
|
61
|
-
user: "adapter",
|
|
62
|
-
hover: "block",
|
|
63
|
-
lifecycle: "block"
|
|
64
|
-
}
|
|
65
|
-
};
|
|
288
|
+
//#region src/services/policy.ts
|
|
66
289
|
/** Never executable, not configurable — diagnostics only. */
|
|
67
290
|
const NEVER_TYPES = /* @__PURE__ */ new Set([
|
|
68
291
|
"launch",
|
|
@@ -72,7 +295,7 @@ const NEVER_TYPES = /* @__PURE__ */ new Set([
|
|
|
72
295
|
"movie",
|
|
73
296
|
"import-data"
|
|
74
297
|
]);
|
|
75
|
-
/** Recognized types with no
|
|
298
|
+
/** Recognized types with no interpreter: reported, never executed. */
|
|
76
299
|
const UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
77
300
|
"rendition",
|
|
78
301
|
"thread",
|
|
@@ -83,565 +306,146 @@ const UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
|
83
306
|
]);
|
|
84
307
|
/** Document-lifetime node types: engine mutations, committed in walk order. */
|
|
85
308
|
const DOCUMENT_TYPES = /* @__PURE__ */ new Set(["reset-form", "javascript"]);
|
|
86
|
-
const sameRef = (left, right) => {
|
|
87
|
-
if (left.kind === "objectNumber" && right.kind === "objectNumber") return left.annotObjectNumber === right.annotObjectNumber;
|
|
88
|
-
if (left.kind === "nm" && right.kind === "nm") return left.pageObjectNumber === right.pageObjectNumber && left.nm === right.nm;
|
|
89
|
-
if (left.kind === "index" && right.kind === "index") return left.pageObjectNumber === right.pageObjectNumber && left.index === right.index;
|
|
90
|
-
return false;
|
|
91
|
-
};
|
|
92
309
|
const isPrintVerb = (node) => node.type === "named" && node.name === "Print";
|
|
93
|
-
function
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const enqueue = (0, _embedpdf_core.createSerialQueue)();
|
|
99
|
-
const executors = /* @__PURE__ */ new Map();
|
|
100
|
-
let annotCommitSink = null;
|
|
101
|
-
let formCommitSink = null;
|
|
102
|
-
let uiAdapter = null;
|
|
103
|
-
/** Sink 1 of the submit chain (consent = installation). */
|
|
104
|
-
let submitHandler = null;
|
|
105
|
-
/** The form plugin's dataset resolver (D7's one door). */
|
|
106
|
-
let submitResolver = null;
|
|
107
|
-
/** D3's document-print latch: while a print event wrapper is active, a
|
|
108
|
-
* nested print request is SUPPRESSED (`reentrant-print`) — the adapter
|
|
109
|
-
* opens exactly one dialog per outer request. */
|
|
110
|
-
let docPrintEventActive = false;
|
|
111
|
-
/** D11's deterministic aggregate: JS nodes run in the CURRENT dispatch. */
|
|
112
|
-
let jsNodesThisDispatch = 0;
|
|
113
|
-
const actionHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onAction observer failed:", error));
|
|
114
|
-
const diagnosticHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onDiagnostic observer failed:", error));
|
|
115
|
-
const scriptDiagnosticHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onScriptDiagnostic observer failed:", error));
|
|
116
|
-
const scriptErrorHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onScriptError observer failed:", error));
|
|
117
|
-
ctx.cleanup(() => {
|
|
118
|
-
actionHook.dispose();
|
|
119
|
-
diagnosticHook.dispose();
|
|
120
|
-
scriptDiagnosticHook.dispose();
|
|
121
|
-
scriptErrorHook.dispose();
|
|
122
|
-
});
|
|
123
|
-
/** Policy lookup — `null` means "recognized, no interpreter" (inert). */
|
|
310
|
+
function createPolicy(ctx) {
|
|
311
|
+
const settings = ctx.settings();
|
|
312
|
+
/** The live policy: read at decision time, never captured. */
|
|
313
|
+
const current = () => settings.get().policy;
|
|
314
|
+
/** The decision for one node and origin; `'never'` and `'unsupported'` are fixed, not configurable. */
|
|
124
315
|
const decisionFor = (node, origin) => {
|
|
125
316
|
if (NEVER_TYPES.has(node.type)) return "never";
|
|
126
317
|
if (UNSUPPORTED_TYPES.has(node.type)) return "unsupported";
|
|
318
|
+
const policy = current();
|
|
127
319
|
if (isPrintVerb(node)) return policy.print[origin];
|
|
128
320
|
const row = policy[node.type];
|
|
129
321
|
return row ? row[origin] : "unsupported";
|
|
130
322
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!actionsSnapshotPromise) {
|
|
135
|
-
const doc = ctx.doc;
|
|
136
|
-
const memo = (doc?.actions ? Promise.resolve(doc.actions.read()) : Promise.resolve(null)).catch((error) => {
|
|
137
|
-
if (actionsSnapshotPromise === memo) actionsSnapshotPromise = null;
|
|
138
|
-
throw error;
|
|
139
|
-
});
|
|
140
|
-
actionsSnapshotPromise = memo;
|
|
141
|
-
}
|
|
142
|
-
return actionsSnapshotPromise;
|
|
323
|
+
return {
|
|
324
|
+
decisionFor,
|
|
325
|
+
current
|
|
143
326
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/submit/intent.ts
|
|
330
|
+
const intentOfPayload = (payload) => ({
|
|
331
|
+
url: payload.url,
|
|
332
|
+
fields: payload.fields,
|
|
333
|
+
exclude: payload.flags.exclude,
|
|
334
|
+
includeNoValueFields: payload.flags.includeNoValueFields,
|
|
335
|
+
format: payload.flags.format,
|
|
336
|
+
method: payload.flags.method,
|
|
337
|
+
flagsRaw: payload.flags.raw,
|
|
338
|
+
...payload.charSet === void 0 ? {} : { charSet: payload.charSet }
|
|
339
|
+
});
|
|
340
|
+
const toFormSubmissionRequest = (request, sentAt) => ({
|
|
341
|
+
entries: request.entries,
|
|
342
|
+
intent: {
|
|
343
|
+
url: request.url,
|
|
344
|
+
format: request.format,
|
|
345
|
+
method: request.method,
|
|
346
|
+
flagsRaw: request.flagsRaw,
|
|
347
|
+
...request.charSet === void 0 ? {} : { charSet: request.charSet }
|
|
348
|
+
},
|
|
349
|
+
origin: request.origin,
|
|
350
|
+
sentAt
|
|
351
|
+
});
|
|
352
|
+
/** Script `doc.submitForm(...)` → the one normalized intent. Script field
|
|
353
|
+
* names are include-mode by definition (Acrobat's aFields). */
|
|
354
|
+
const intentOfSubmitEffect = (effect) => {
|
|
355
|
+
const format = effect.format ?? "fdf";
|
|
356
|
+
return {
|
|
357
|
+
url: effect.url,
|
|
358
|
+
fields: effect.fieldNames?.map((name) => ({
|
|
359
|
+
kind: "name",
|
|
360
|
+
name
|
|
361
|
+
})) ?? null,
|
|
362
|
+
exclude: false,
|
|
363
|
+
includeNoValueFields: effect.includeEmpty,
|
|
364
|
+
format,
|
|
365
|
+
method: effect.method ?? "post",
|
|
366
|
+
flagsRaw: 0
|
|
151
367
|
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
method: request.method,
|
|
169
|
-
flagsRaw: request.flagsRaw,
|
|
170
|
-
...request.charSet === void 0 ? {} : { charSet: request.charSet }
|
|
171
|
-
},
|
|
172
|
-
origin: request.origin,
|
|
173
|
-
clientTimeMs: nowMs()
|
|
174
|
-
});
|
|
175
|
-
/**
|
|
176
|
-
* The ONE submit door — both sources land here after policy. Sink order:
|
|
177
|
-
* embedder handler (explicit beats ambient; detached-promise contract) →
|
|
178
|
-
* the document's home (`doc.forms.submit`, engine-asserted, AWAITED — a
|
|
179
|
-
* real op with a real receipt) → blocked with `no-submit-sink`. Never a
|
|
180
|
-
* network call from this plugin.
|
|
181
|
-
*/
|
|
182
|
-
const performSubmit = async (intent, actionCtx, diagnose) => {
|
|
183
|
-
const decision = policy["submit-form"][actionCtx.origin];
|
|
184
|
-
if (decision !== "adapter" && decision !== "allow") {
|
|
185
|
-
diagnose({
|
|
186
|
-
code: "blocked",
|
|
187
|
-
message: `submit-form: policy '${decision}' for origin '${actionCtx.origin}'`
|
|
188
|
-
});
|
|
189
|
-
return { status: "blocked" };
|
|
190
|
-
}
|
|
191
|
-
if (!submitResolver) {
|
|
192
|
-
diagnose({
|
|
193
|
-
code: "no-submit-resolver",
|
|
194
|
-
message: "submit-form: no dataset resolver registered (form plugin missing?)"
|
|
368
|
+
};
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/dispatch/run.ts
|
|
371
|
+
function createRunner(ctx, services, { performSubmit }, { firePrintThroughAdapter }) {
|
|
372
|
+
const { decisionFor } = services.policy;
|
|
373
|
+
const ports = services.ports.slots;
|
|
374
|
+
const { reportDiagnostic } = services.events;
|
|
375
|
+
const { form } = services;
|
|
376
|
+
async function run(tree, actionContext, signal) {
|
|
377
|
+
const diagnostics = [];
|
|
378
|
+
/** Record a diagnostic in the result and report it, with the action it concerns. */
|
|
379
|
+
const diagnose = (diagnostic, action) => {
|
|
380
|
+
diagnostics.push(diagnostic);
|
|
381
|
+
reportDiagnostic(diagnostic, {
|
|
382
|
+
action,
|
|
383
|
+
source: actionContext.source
|
|
195
384
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
detail: "no dataset resolver"
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
let request;
|
|
202
|
-
try {
|
|
203
|
-
request = await submitResolver(intent, actionCtx, diagnose);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
385
|
+
};
|
|
386
|
+
if (tree.incomplete) {
|
|
206
387
|
diagnose({
|
|
207
|
-
code: "
|
|
208
|
-
message:
|
|
209
|
-
});
|
|
388
|
+
code: "incomplete-tree",
|
|
389
|
+
message: "refused: the action tree is incomplete"
|
|
390
|
+
}, tree.root?.type ?? null);
|
|
210
391
|
return {
|
|
211
|
-
status: "
|
|
212
|
-
|
|
392
|
+
status: "refused",
|
|
393
|
+
nodes: [],
|
|
394
|
+
diagnostics
|
|
213
395
|
};
|
|
214
396
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
397
|
+
if (!tree.root) return {
|
|
398
|
+
status: "inert",
|
|
399
|
+
nodes: [],
|
|
400
|
+
diagnostics
|
|
401
|
+
};
|
|
402
|
+
const nodes = [];
|
|
403
|
+
const deferred = [];
|
|
404
|
+
let documentFailed = false;
|
|
405
|
+
let cancelled = false;
|
|
406
|
+
const settle = (result, outcome) => {
|
|
407
|
+
if (outcome.status === "executed") result.status = "executed";
|
|
408
|
+
else if (outcome.status === "inert") {
|
|
409
|
+
result.status = "inert";
|
|
410
|
+
result.detail = outcome.reason;
|
|
411
|
+
diagnose({
|
|
412
|
+
code: "executor-inert",
|
|
413
|
+
message: `${result.type}: ${outcome.reason}`
|
|
414
|
+
}, result.type);
|
|
415
|
+
} else {
|
|
416
|
+
result.status = "failed";
|
|
417
|
+
result.detail = outcome.error;
|
|
418
|
+
diagnose({
|
|
221
419
|
code: "executor-failed",
|
|
222
|
-
message:
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
return { status: "executed" };
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
228
|
-
diagnose({
|
|
229
|
-
code: "executor-failed",
|
|
230
|
-
message: `submit handler threw: ${detail}`
|
|
231
|
-
});
|
|
232
|
-
return {
|
|
233
|
-
status: "failed",
|
|
234
|
-
detail
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
if (homeSubmit) try {
|
|
238
|
-
await homeSubmit();
|
|
239
|
-
return { status: "executed" };
|
|
240
|
-
} catch (error) {
|
|
241
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
242
|
-
diagnose({
|
|
243
|
-
code: "executor-failed",
|
|
244
|
-
message: `document home refused submit: ${detail}`
|
|
245
|
-
});
|
|
246
|
-
return {
|
|
247
|
-
status: "failed",
|
|
248
|
-
detail
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
diagnose({
|
|
252
|
-
code: "no-submit-sink",
|
|
253
|
-
message: "submit-form: no handler installed and the document has no submit-capable home"
|
|
254
|
-
});
|
|
255
|
-
return {
|
|
256
|
-
status: "blocked",
|
|
257
|
-
detail: "no submit sink"
|
|
258
|
-
};
|
|
259
|
-
};
|
|
260
|
-
const js = config.javascript;
|
|
261
|
-
const realms = js?.enabled && ctx.doc ? createScriptRealmFactory(js, ctx.doc) : null;
|
|
262
|
-
const scriptHost = realms && ctx.doc ? realms.realmFor({
|
|
263
|
-
doc: ctx.doc,
|
|
264
|
-
document: () => ctx.document(),
|
|
265
|
-
bootSources: async () => {
|
|
266
|
-
return ((await readDocumentActions())?.nameTreeScripts ?? []).map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action));
|
|
267
|
-
}
|
|
268
|
-
}) : null;
|
|
269
|
-
if (scriptHost) ctx.cleanup(() => scriptHost.dispose());
|
|
270
|
-
const surfaceScriptResult = (result) => {
|
|
271
|
-
const uiContext = {
|
|
272
|
-
origin: result.origin,
|
|
273
|
-
phase: result.phase
|
|
274
|
-
};
|
|
275
|
-
for (const effect of result.uiEffects) {
|
|
276
|
-
if (result.realm === "detached" && effect.kind !== "alert") {
|
|
277
|
-
scriptDiagnosticHook.emit({
|
|
278
|
-
code: "ui-effect-suppressed",
|
|
279
|
-
message: `script ${effect.kind} request from a detached realm suppressed: no document surface`
|
|
280
|
-
});
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
if (effect.kind === "submitForm") {
|
|
284
|
-
performSubmit(intentOfSubmitEffect(effect), {
|
|
285
|
-
origin: result.origin,
|
|
286
|
-
source: { kind: "api" },
|
|
287
|
-
event: { scope: "activate" }
|
|
288
|
-
}, (diagnostic) => diagnosticHook.emit(diagnostic));
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
if (effect.kind === "print" && !allowsPrint()) {
|
|
292
|
-
scriptDiagnosticHook.emit({
|
|
293
|
-
code: "ui-effect-suppressed",
|
|
294
|
-
message: "script print request withheld: doc.print is not allowed"
|
|
295
|
-
});
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (effect.kind === "print" && docPrintEventActive) {
|
|
299
|
-
scriptDiagnosticHook.emit({
|
|
300
|
-
code: "ui-effect-suppressed",
|
|
301
|
-
message: "script print request during a document print event — suppressed (reentrant)"
|
|
302
|
-
});
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (!uiAdapter) {
|
|
306
|
-
diagnosticHook.emit({
|
|
307
|
-
code: "no-adapter",
|
|
308
|
-
message: `script ${effect.kind}: no UI adapter installed`
|
|
309
|
-
});
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (effect.kind === "alert") uiAdapter.alert?.(effect.message, {
|
|
313
|
-
...uiContext,
|
|
314
|
-
icon: effect.icon,
|
|
315
|
-
...effect.title !== void 0 ? { title: effect.title } : {}
|
|
316
|
-
});
|
|
317
|
-
else if (effect.kind === "gotoPage") uiAdapter.gotoPage?.(effect.page, uiContext);
|
|
318
|
-
else uiAdapter.print(uiContext);
|
|
319
|
-
}
|
|
320
|
-
for (const diagnostic of result.diagnostics) scriptDiagnosticHook.emit(diagnostic);
|
|
321
|
-
if (result.error) scriptErrorHook.emit(result.error);
|
|
322
|
-
};
|
|
323
|
-
/** A K/V/C/F commit surfaces as up to two results: the boot phase (only
|
|
324
|
-
* when it produced effects) and the user phase (always — it carries the
|
|
325
|
-
* diagnostics and the error). */
|
|
326
|
-
const surfaceScriptCommit = (commit, context) => {
|
|
327
|
-
for (const phase of ["boot", "user"]) {
|
|
328
|
-
const uiEffects = commit.uiEffects.filter((effect) => effect.phase === phase);
|
|
329
|
-
if (uiEffects.length === 0 && phase === "boot") continue;
|
|
330
|
-
surfaceScriptResult({
|
|
331
|
-
uiEffects,
|
|
332
|
-
diagnostics: phase === "user" ? commit.diagnostics : [],
|
|
333
|
-
...phase === "user" && commit.error ? { error: commit.error } : {},
|
|
334
|
-
origin: context.origin,
|
|
335
|
-
phase,
|
|
336
|
-
realm: context.realm
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
/** Script `doc.submitForm(...)` → the one normalized intent. Script field
|
|
341
|
-
* names are include-mode by definition (Acrobat's aFields). */
|
|
342
|
-
const intentOfSubmitEffect = (effect) => {
|
|
343
|
-
const format = effect.format ?? "fdf";
|
|
344
|
-
return {
|
|
345
|
-
url: effect.url,
|
|
346
|
-
fields: effect.fieldNames?.map((name) => ({
|
|
347
|
-
kind: "name",
|
|
348
|
-
name
|
|
349
|
-
})) ?? null,
|
|
350
|
-
exclude: false,
|
|
351
|
-
includeNoValueFields: effect.includeEmpty,
|
|
352
|
-
format,
|
|
353
|
-
method: effect.method ?? "post",
|
|
354
|
-
flagsRaw: 0
|
|
355
|
-
};
|
|
356
|
-
};
|
|
357
|
-
const engineColorToArray = (color) => color ? [
|
|
358
|
-
"RGB",
|
|
359
|
-
color.r / 255,
|
|
360
|
-
color.g / 255,
|
|
361
|
-
color.b / 255
|
|
362
|
-
] : ["T"];
|
|
363
|
-
const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((page) => page.pageObjectNumber === pon) ?? -1;
|
|
364
|
-
const scriptWorldFor = async (pon) => {
|
|
365
|
-
const doc = ctx.doc;
|
|
366
|
-
const snapshot = await doc.forms.list();
|
|
367
|
-
const pageIndex = Math.max(0, pageIndexOf(pon));
|
|
368
|
-
const { annotations } = await doc.page(pon).annotations.list();
|
|
369
|
-
const annots = annotations.filter((a) => a.subtype !== "link" && !a.subtype.startsWith("widget")).map((a) => {
|
|
370
|
-
const styled = a;
|
|
371
|
-
return {
|
|
372
|
-
ref: a.ref,
|
|
373
|
-
name: a.nm ?? "",
|
|
374
|
-
subtype: a.subtype,
|
|
375
|
-
page: pageIndex,
|
|
376
|
-
rect: [
|
|
377
|
-
a.rect.left,
|
|
378
|
-
a.rect.bottom,
|
|
379
|
-
a.rect.right,
|
|
380
|
-
a.rect.top
|
|
381
|
-
],
|
|
382
|
-
contents: a.contents ?? "",
|
|
383
|
-
author: a.author ?? "",
|
|
384
|
-
subject: a.subject ?? "",
|
|
385
|
-
strokeColor: engineColorToArray(styled.color),
|
|
386
|
-
fillColor: engineColorToArray(styled.interiorColor),
|
|
387
|
-
opacity: styled.opacity ?? 1,
|
|
388
|
-
width: styled.strokeWidth ?? 1,
|
|
389
|
-
borderStyle: styled.borderStyle === "dashed" ? "D" : "S",
|
|
390
|
-
dash: styled.dashArray ?? [],
|
|
391
|
-
hidden: a.flags.hidden,
|
|
392
|
-
print: a.flags.print,
|
|
393
|
-
readOnly: a.flags.readOnly,
|
|
394
|
-
locked: a.flags.locked,
|
|
395
|
-
noView: a.flags.noView,
|
|
396
|
-
toggleNoView: a.flags.toggleNoView,
|
|
397
|
-
opaqueBody: a.subtype === "stamp"
|
|
398
|
-
};
|
|
399
|
-
});
|
|
400
|
-
return {
|
|
401
|
-
snapshot,
|
|
402
|
-
world: {
|
|
403
|
-
fields: (0, _embedpdf_core_acrojs.scriptFieldsFromSnapshot)(snapshot),
|
|
404
|
-
annots,
|
|
405
|
-
annotPages: [pageIndex],
|
|
406
|
-
annotsCoverDocument: (ctx.document()?.pageCount ?? 0) <= 1
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
};
|
|
410
|
-
const ANNOTATION_EVENT_NAMES = {
|
|
411
|
-
cursorEnter: "Mouse Enter",
|
|
412
|
-
cursorExit: "Mouse Exit",
|
|
413
|
-
mouseDown: "Mouse Down",
|
|
414
|
-
mouseUp: "Mouse Up",
|
|
415
|
-
focus: "Focus",
|
|
416
|
-
blur: "Blur"
|
|
417
|
-
};
|
|
418
|
-
const PAGE_EVENT_NAMES = {
|
|
419
|
-
open: "Open",
|
|
420
|
-
close: "Close",
|
|
421
|
-
visible: "Visible",
|
|
422
|
-
invisible: "Invisible"
|
|
423
|
-
};
|
|
424
|
-
const DOC_EVENT_NAMES = {
|
|
425
|
-
open: "Open",
|
|
426
|
-
"will-save": "WillSave",
|
|
427
|
-
"did-save": "DidSave",
|
|
428
|
-
"will-print": "WillPrint",
|
|
429
|
-
"did-print": "DidPrint",
|
|
430
|
-
"will-close": "WillClose"
|
|
431
|
-
};
|
|
432
|
-
const scriptEventFor = (actionCtx, snapshot) => {
|
|
433
|
-
const provenance = actionCtx.event;
|
|
434
|
-
const type = provenance.scope === "page" ? "Page" : provenance.scope === "document" ? "Doc" : actionCtx.source.kind === "widget" ? "Field" : actionCtx.source.kind === "link" ? "Link" : "Annot";
|
|
435
|
-
const name = provenance.scope === "activate" ? "Mouse Up" : provenance.scope === "annotation" ? ANNOTATION_EVENT_NAMES[provenance.name] ?? "Mouse Up" : provenance.scope === "page" ? PAGE_EVENT_NAMES[provenance.name] ?? "Open" : DOC_EVENT_NAMES[provenance.name] ?? "Open";
|
|
436
|
-
const targetRef = actionCtx.source.kind === "widget" ? actionCtx.source.field : void 0;
|
|
437
|
-
const targetField = targetRef ? snapshot.fields.find((field) => targetRef.kind === "objectNumber" && field.ref.kind === "objectNumber" ? field.ref.fieldObjectNumber === targetRef.fieldObjectNumber : targetRef.kind === "fqn" && field.name === targetRef.name) : void 0;
|
|
438
|
-
return {
|
|
439
|
-
kind: "widget-activate",
|
|
440
|
-
type,
|
|
441
|
-
name,
|
|
442
|
-
...targetRef ? {
|
|
443
|
-
target: targetRef,
|
|
444
|
-
source: targetRef
|
|
445
|
-
} : {},
|
|
446
|
-
...targetField && targetField.valueEntry.kind === "scalar" ? { value: targetField.valueEntry.value } : {}
|
|
447
|
-
};
|
|
448
|
-
};
|
|
449
|
-
/** Commit one run's document effects through the owner sinks in the
|
|
450
|
-
* DECLARED order (form first, then annot); the first failure skips the
|
|
451
|
-
* rest across BOTH streams. Returns the failure summary, if any. */
|
|
452
|
-
const commitScriptOutput = async (output, diagnose) => {
|
|
453
|
-
let failure = null;
|
|
454
|
-
if (output.formEffects.length > 0) if (!formCommitSink) diagnose({
|
|
455
|
-
code: "no-commit-sink",
|
|
456
|
-
message: `script form effects dropped: no form commit sink registered (${output.formEffects.length})`
|
|
457
|
-
});
|
|
458
|
-
else {
|
|
459
|
-
const bad = (await formCommitSink(output.formEffects)).results.find((entry) => entry.status === "failed" || entry.status === "rejected");
|
|
460
|
-
if (bad) failure = `form effect ${bad.index}: ${bad.error?.message ?? bad.status}`;
|
|
461
|
-
}
|
|
462
|
-
if (output.annotEffects.length > 0) if (failure) diagnose({
|
|
463
|
-
code: "executor-failed",
|
|
464
|
-
message: `script annot effects skipped after form failure (${output.annotEffects.length})`
|
|
465
|
-
});
|
|
466
|
-
else if (!annotCommitSink) diagnose({
|
|
467
|
-
code: "no-commit-sink",
|
|
468
|
-
message: `script annot effects dropped: no annotation commit sink registered (${output.annotEffects.length})`
|
|
469
|
-
});
|
|
470
|
-
else {
|
|
471
|
-
const entries = output.annotEffects.map((effect) => ({
|
|
472
|
-
annotObjectNumber: effect.ref.kind === "objectNumber" ? effect.ref.annotObjectNumber : -1,
|
|
473
|
-
...effect.ref.kind === "objectNumber" ? { pageObjectNumber: effect.ref.pageObjectNumber } : {},
|
|
474
|
-
patch: effect.patch
|
|
475
|
-
}));
|
|
476
|
-
const bad = (await annotCommitSink(entries)).results.find((entry) => entry.status === "failed");
|
|
477
|
-
if (bad) failure = `annotation ${bad.annotObjectNumber}: ${bad.error ?? "failed"}`;
|
|
478
|
-
}
|
|
479
|
-
if (failure) diagnose({
|
|
480
|
-
code: "executor-failed",
|
|
481
|
-
message: `script commit: ${failure}`
|
|
482
|
-
});
|
|
483
|
-
return failure;
|
|
484
|
-
};
|
|
485
|
-
if (scriptHost) {
|
|
486
|
-
const nodeCap = js?.maxScriptNodesPerDispatch ?? 16;
|
|
487
|
-
executors.set("javascript", async (node, actionCtx) => {
|
|
488
|
-
if (node.type !== "javascript") return {
|
|
489
|
-
status: "inert",
|
|
490
|
-
reason: "not a JS node"
|
|
491
|
-
};
|
|
492
|
-
if (!ctx.doc) return {
|
|
493
|
-
status: "inert",
|
|
494
|
-
reason: "no document"
|
|
495
|
-
};
|
|
496
|
-
if (jsNodesThisDispatch >= nodeCap) return {
|
|
497
|
-
status: "inert",
|
|
498
|
-
reason: `dispatch script budget exhausted (${nodeCap} JS nodes)`
|
|
499
|
-
};
|
|
500
|
-
jsNodesThisDispatch += 1;
|
|
501
|
-
const source = actionCtx.source;
|
|
502
|
-
const pon = source.kind === "widget" ? source.pon : source.kind === "link" || source.kind === "annotation" ? source.pon ?? ctx.document()?.pages[0]?.pageObjectNumber : source.kind === "page" ? source.pon : ctx.document()?.pages[0]?.pageObjectNumber;
|
|
503
|
-
if (pon === void 0) return {
|
|
504
|
-
status: "inert",
|
|
505
|
-
reason: "no page to anchor the world on"
|
|
506
|
-
};
|
|
507
|
-
const diagnose = (diagnostic) => diagnosticHook.emit(diagnostic);
|
|
508
|
-
const pendingExternal = [];
|
|
509
|
-
const splitExternal = (output, phase) => output.uiEffects.filter((effect) => {
|
|
510
|
-
if (effect.kind === "print" || effect.kind === "submitForm") {
|
|
511
|
-
pendingExternal.push({
|
|
512
|
-
effect,
|
|
513
|
-
phase
|
|
514
|
-
});
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
return true;
|
|
518
|
-
});
|
|
519
|
-
const outcome = await scriptHost.transaction(async (txn) => {
|
|
520
|
-
let built = await scriptWorldFor(pon);
|
|
521
|
-
const boot = await txn.boot({
|
|
522
|
-
...built.world,
|
|
523
|
-
event: {
|
|
524
|
-
kind: "name-tree-boot",
|
|
525
|
-
type: "Doc",
|
|
526
|
-
name: "Open"
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
if (boot) {
|
|
530
|
-
await commitScriptOutput(boot, diagnose);
|
|
531
|
-
surfaceScriptResult({
|
|
532
|
-
uiEffects: splitExternal(boot, "boot"),
|
|
533
|
-
diagnostics: boot.diagnostics,
|
|
534
|
-
...boot.error ? { error: boot.error } : {},
|
|
535
|
-
origin: actionCtx.origin,
|
|
536
|
-
phase: "boot",
|
|
537
|
-
realm: "document"
|
|
538
|
-
});
|
|
539
|
-
if (boot.formEffects.length || boot.annotEffects.length) built = await scriptWorldFor(pon);
|
|
540
|
-
}
|
|
541
|
-
const output = await txn.run(node.script, {
|
|
542
|
-
...built.world,
|
|
543
|
-
event: scriptEventFor(actionCtx, built.snapshot)
|
|
544
|
-
});
|
|
545
|
-
surfaceScriptResult({
|
|
546
|
-
uiEffects: splitExternal(output, "user"),
|
|
547
|
-
diagnostics: output.diagnostics,
|
|
548
|
-
...output.error ? { error: output.error } : {},
|
|
549
|
-
origin: actionCtx.origin,
|
|
550
|
-
phase: "user",
|
|
551
|
-
realm: "document"
|
|
552
|
-
});
|
|
553
|
-
if (output.error) return {
|
|
554
|
-
status: "failed",
|
|
555
|
-
error: output.error.message
|
|
556
|
-
};
|
|
557
|
-
const failure = await commitScriptOutput(output, diagnose);
|
|
558
|
-
return failure ? {
|
|
559
|
-
status: "failed",
|
|
560
|
-
error: failure
|
|
561
|
-
} : { status: "executed" };
|
|
562
|
-
});
|
|
563
|
-
for (const entry of pendingExternal) if (entry.effect.kind === "print") await firePrintThroughAdapter({
|
|
564
|
-
origin: actionCtx.origin,
|
|
565
|
-
phase: entry.phase
|
|
566
|
-
}, diagnose);
|
|
567
|
-
else await performSubmit(intentOfSubmitEffect(entry.effect), actionCtx, diagnose);
|
|
568
|
-
return outcome;
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
async function run(tree, actionCtx) {
|
|
572
|
-
const diagnostics = [];
|
|
573
|
-
const diagnose = (diagnostic) => {
|
|
574
|
-
diagnostics.push(diagnostic);
|
|
575
|
-
diagnosticHook.emit(diagnostic);
|
|
576
|
-
};
|
|
577
|
-
if (tree.incomplete) {
|
|
578
|
-
diagnose({
|
|
579
|
-
code: "incomplete-tree",
|
|
580
|
-
message: "refused: the action tree is incomplete"
|
|
581
|
-
});
|
|
582
|
-
return {
|
|
583
|
-
status: "refused",
|
|
584
|
-
nodes: [],
|
|
585
|
-
diagnostics
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
if (!tree.root) return {
|
|
589
|
-
status: "inert",
|
|
590
|
-
nodes: [],
|
|
591
|
-
diagnostics
|
|
592
|
-
};
|
|
593
|
-
const nodes = [];
|
|
594
|
-
const deferred = [];
|
|
595
|
-
let documentFailed = false;
|
|
596
|
-
const settle = (result, outcome) => {
|
|
597
|
-
if (outcome.status === "executed") result.status = "executed";
|
|
598
|
-
else if (outcome.status === "inert") {
|
|
599
|
-
result.status = "inert";
|
|
600
|
-
result.detail = outcome.reason;
|
|
601
|
-
diagnose({
|
|
602
|
-
code: "executor-inert",
|
|
603
|
-
message: `${result.type}: ${outcome.reason}`
|
|
604
|
-
});
|
|
605
|
-
} else {
|
|
606
|
-
result.status = "failed";
|
|
607
|
-
result.detail = outcome.error;
|
|
608
|
-
diagnose({
|
|
609
|
-
code: "executor-failed",
|
|
610
|
-
message: `${result.type}: ${outcome.error}`
|
|
611
|
-
});
|
|
612
|
-
}
|
|
420
|
+
message: `${result.type}: ${outcome.error}`
|
|
421
|
+
}, result.type);
|
|
422
|
+
}
|
|
613
423
|
};
|
|
614
424
|
const interpretHide = async (node) => {
|
|
615
|
-
const doc = ctx.doc;
|
|
616
425
|
const display = node.hide ? "hidden" : "visible";
|
|
426
|
+
const diagnoseHide = (diagnostic) => diagnose(diagnostic, "hide");
|
|
617
427
|
const fieldRefs = [];
|
|
618
428
|
const annotEntries = [];
|
|
619
429
|
const names = [];
|
|
620
430
|
const bareObjectNumbers = [];
|
|
621
431
|
for (const target of node.targets) if (target.kind === "objectNumber") bareObjectNumbers.push(target.objectNumber);
|
|
622
432
|
else names.push(target.name);
|
|
623
|
-
if (
|
|
624
|
-
const snapshot =
|
|
433
|
+
if (names.length || bareObjectNumbers.length) {
|
|
434
|
+
const snapshot = await form.read();
|
|
625
435
|
for (const name of names) {
|
|
626
|
-
const field = snapshot
|
|
436
|
+
const field = snapshot.fields.find((candidate) => candidate.name === name);
|
|
627
437
|
if (!field) {
|
|
628
|
-
|
|
438
|
+
diagnoseHide({
|
|
629
439
|
code: "unresolved-target",
|
|
630
440
|
message: `hide: no field named '${name}'`
|
|
631
441
|
});
|
|
632
442
|
continue;
|
|
633
443
|
}
|
|
634
|
-
fieldRefs.push(
|
|
635
|
-
kind: "objectNumber",
|
|
636
|
-
fieldObjectNumber: field.fieldObjectNumber
|
|
637
|
-
});
|
|
444
|
+
fieldRefs.push(field.ref);
|
|
638
445
|
}
|
|
639
446
|
for (const objectNumber of bareObjectNumbers) {
|
|
640
|
-
const owner = snapshot
|
|
641
|
-
if (owner) fieldRefs.push(
|
|
642
|
-
kind: "objectNumber",
|
|
643
|
-
fieldObjectNumber: owner.fieldObjectNumber
|
|
644
|
-
});
|
|
447
|
+
const owner = snapshot.fields.find((field) => field.widgets.some((widget) => widget.objectNumber === objectNumber));
|
|
448
|
+
if (owner) fieldRefs.push(owner.ref);
|
|
645
449
|
else annotEntries.push({
|
|
646
450
|
annotObjectNumber: objectNumber,
|
|
647
451
|
patch: { flags: { hidden: node.hide } }
|
|
@@ -650,37 +454,37 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
650
454
|
}
|
|
651
455
|
let failedDetail;
|
|
652
456
|
if (fieldRefs.length) {
|
|
653
|
-
if (!formCommitSink) {
|
|
654
|
-
|
|
457
|
+
if (!ports.formCommitSink) {
|
|
458
|
+
diagnoseHide({
|
|
655
459
|
code: "no-commit-sink",
|
|
656
460
|
message: "hide: no form commit sink registered (form plugin absent)"
|
|
657
461
|
});
|
|
658
462
|
return { status: "no-executor" };
|
|
659
463
|
}
|
|
660
|
-
const
|
|
464
|
+
const result = await ports.formCommitSink(fieldRefs.map((ref) => ({
|
|
661
465
|
kind: "setDisplay",
|
|
662
466
|
ref,
|
|
663
467
|
display
|
|
664
|
-
})))
|
|
665
|
-
if (
|
|
666
|
-
failedDetail =
|
|
667
|
-
|
|
468
|
+
})));
|
|
469
|
+
if (result.status === "failed") {
|
|
470
|
+
failedDetail = result.error ?? "failed";
|
|
471
|
+
diagnoseHide({
|
|
668
472
|
code: "executor-failed",
|
|
669
473
|
message: `hide: ${failedDetail}`
|
|
670
474
|
});
|
|
671
475
|
}
|
|
672
476
|
}
|
|
673
477
|
if (annotEntries.length && !failedDetail) {
|
|
674
|
-
if (!annotCommitSink) {
|
|
675
|
-
|
|
478
|
+
if (!ports.annotCommitSink) {
|
|
479
|
+
diagnoseHide({
|
|
676
480
|
code: "no-commit-sink",
|
|
677
481
|
message: "hide: no annotation commit sink registered (annotation plugin absent)"
|
|
678
482
|
});
|
|
679
483
|
return { status: "no-executor" };
|
|
680
484
|
}
|
|
681
|
-
const committed = await annotCommitSink(annotEntries);
|
|
485
|
+
const committed = await ports.annotCommitSink(annotEntries);
|
|
682
486
|
const failed = committed.results.filter((entry) => entry.status === "failed");
|
|
683
|
-
for (const failure of failed)
|
|
487
|
+
for (const failure of failed) diagnoseHide({
|
|
684
488
|
code: "executor-failed",
|
|
685
489
|
message: `hide: annotation ${failure.annotObjectNumber}: ${failure.error ?? "failed"}`
|
|
686
490
|
});
|
|
@@ -699,12 +503,13 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
699
503
|
status: "blocked"
|
|
700
504
|
};
|
|
701
505
|
nodes.push(result);
|
|
702
|
-
const decision = decisionFor(node,
|
|
506
|
+
const decision = decisionFor(node, actionContext.origin);
|
|
507
|
+
const diagnoseNode = (diagnostic) => diagnose(diagnostic, node.type);
|
|
703
508
|
if (decision === "never" || decision === "block" || decision === "report") {
|
|
704
509
|
result.status = "blocked";
|
|
705
|
-
|
|
510
|
+
if (decision !== "block") diagnoseNode({
|
|
706
511
|
code: "blocked",
|
|
707
|
-
message: `${node.type}: ${decision === "never" ? "never executable" : `policy '
|
|
512
|
+
message: `${node.type}: ${decision === "never" ? "never executable" : `policy 'report' for origin '${actionContext.origin}'`}`
|
|
708
513
|
});
|
|
709
514
|
return;
|
|
710
515
|
}
|
|
@@ -728,17 +533,17 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
728
533
|
result.status = "skipped";
|
|
729
534
|
return;
|
|
730
535
|
}
|
|
731
|
-
const executor = executors.get(node.type);
|
|
536
|
+
const executor = ports.executors.get(node.type);
|
|
732
537
|
if (!executor) {
|
|
733
538
|
result.status = node.type === "javascript" ? "inert" : "no-executor";
|
|
734
|
-
|
|
539
|
+
diagnoseNode({
|
|
735
540
|
code: "no-executor",
|
|
736
541
|
message: `${node.type}: no executor registered${node.type === "javascript" ? " (scripting unavailable)" : ""}`
|
|
737
542
|
});
|
|
738
543
|
return;
|
|
739
544
|
}
|
|
740
545
|
try {
|
|
741
|
-
settle(result, await executor(node,
|
|
546
|
+
settle(result, await executor(node, actionContext));
|
|
742
547
|
} catch (error) {
|
|
743
548
|
settle(result, {
|
|
744
549
|
status: "failed",
|
|
@@ -752,7 +557,7 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
752
557
|
if (!node.payload) {
|
|
753
558
|
result.status = "inert";
|
|
754
559
|
result.detail = "submit payload unavailable (older runtime extraction)";
|
|
755
|
-
|
|
560
|
+
diagnoseNode({
|
|
756
561
|
code: "submit-payload-unavailable",
|
|
757
562
|
message: "submit-form: payload unavailable (older runtime extraction) — node inert"
|
|
758
563
|
});
|
|
@@ -763,7 +568,7 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
763
568
|
deferred.push({
|
|
764
569
|
result,
|
|
765
570
|
fire: async () => {
|
|
766
|
-
const outcome = await performSubmit(intentOfPayload(payload),
|
|
571
|
+
const outcome = await performSubmit(intentOfPayload(payload), actionContext, diagnoseNode);
|
|
767
572
|
result.status = outcome.status;
|
|
768
573
|
if (outcome.detail) result.detail = outcome.detail;
|
|
769
574
|
}
|
|
@@ -776,22 +581,22 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
776
581
|
result,
|
|
777
582
|
fire: async () => {
|
|
778
583
|
if (node.type === "uri") {
|
|
779
|
-
if (!uiAdapter) {
|
|
584
|
+
if (!ports.uiAdapter) {
|
|
780
585
|
result.status = "no-executor";
|
|
781
|
-
|
|
586
|
+
diagnoseNode({
|
|
782
587
|
code: "no-adapter",
|
|
783
588
|
message: `${node.type}: no UI adapter installed`
|
|
784
589
|
});
|
|
785
590
|
return;
|
|
786
591
|
}
|
|
787
|
-
uiAdapter.openUri(node.uri, {
|
|
592
|
+
ports.uiAdapter.openUri(node.uri, {
|
|
788
593
|
isMap: node.isMap,
|
|
789
|
-
origin:
|
|
594
|
+
origin: actionContext.origin
|
|
790
595
|
});
|
|
791
596
|
result.status = "executed";
|
|
792
597
|
return;
|
|
793
598
|
}
|
|
794
|
-
const outcome = await firePrintThroughAdapter(void 0,
|
|
599
|
+
const outcome = await firePrintThroughAdapter(void 0, diagnoseNode);
|
|
795
600
|
result.status = outcome.status;
|
|
796
601
|
if (outcome.detail) result.detail = outcome.detail;
|
|
797
602
|
}
|
|
@@ -802,17 +607,17 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
802
607
|
deferred.push({
|
|
803
608
|
result,
|
|
804
609
|
fire: async () => {
|
|
805
|
-
const executor = executors.get(node.type);
|
|
610
|
+
const executor = ports.executors.get(node.type);
|
|
806
611
|
if (!executor) {
|
|
807
612
|
result.status = "no-executor";
|
|
808
|
-
|
|
613
|
+
diagnoseNode({
|
|
809
614
|
code: "no-executor",
|
|
810
615
|
message: `${node.type}: no executor registered`
|
|
811
616
|
});
|
|
812
617
|
return;
|
|
813
618
|
}
|
|
814
619
|
try {
|
|
815
|
-
settle(result, await executor(node,
|
|
620
|
+
settle(result, await executor(node, actionContext));
|
|
816
621
|
} catch (error) {
|
|
817
622
|
settle(result, {
|
|
818
623
|
status: "failed",
|
|
@@ -823,11 +628,20 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
823
628
|
});
|
|
824
629
|
};
|
|
825
630
|
const walk = async (node, path) => {
|
|
631
|
+
if (signal?.aborted) {
|
|
632
|
+
cancelled = true;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
826
635
|
await interpret(node, path);
|
|
827
|
-
for (let index = 0; index < node.next.length; index++) await walk(node.next[index], [...path, index]);
|
|
636
|
+
for (let index = 0; index < node.next.length && !cancelled; index++) await walk(node.next[index], [...path, index]);
|
|
828
637
|
};
|
|
829
638
|
await walk(tree.root, []);
|
|
830
|
-
if (!documentFailed) for (const entry of deferred) await entry.fire();
|
|
639
|
+
if (!documentFailed && !cancelled) for (const entry of deferred) await entry.fire();
|
|
640
|
+
if (cancelled) return {
|
|
641
|
+
status: "cancelled",
|
|
642
|
+
nodes,
|
|
643
|
+
diagnostics
|
|
644
|
+
};
|
|
831
645
|
const anyExecuted = nodes.some((node) => node.status === "executed");
|
|
832
646
|
const anyFailedOrSkipped = nodes.some((node) => node.status === "failed" || node.status === "skipped");
|
|
833
647
|
return {
|
|
@@ -836,239 +650,426 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
836
650
|
diagnostics
|
|
837
651
|
};
|
|
838
652
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
653
|
+
return { run };
|
|
654
|
+
}
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/dispatch/triggers.ts
|
|
657
|
+
/**
|
|
658
|
+
* The pages whose annotations an event changes: those pages, every page
|
|
659
|
+
* (`'all'`: a redaction, a skipped stretch of the stream, new document
|
|
660
|
+
* bytes), or none.
|
|
661
|
+
*/
|
|
662
|
+
const annotationPagesOf = (event) => {
|
|
663
|
+
switch (event.type) {
|
|
664
|
+
case "annotations.created":
|
|
665
|
+
case "annotations.updated":
|
|
666
|
+
case "annotations.deleted":
|
|
667
|
+
case "annotations.reordered":
|
|
668
|
+
case "annotations.flattened":
|
|
669
|
+
case "forms.widgetsReordered": return [event.page];
|
|
670
|
+
case "forms.created":
|
|
671
|
+
case "forms.deleted":
|
|
672
|
+
case "forms.widgetAdded":
|
|
673
|
+
case "forms.widgetRemoved":
|
|
674
|
+
case "forms.widgetDeleted":
|
|
675
|
+
case "forms.widgetRestored":
|
|
676
|
+
case "forms.widgetUpdated":
|
|
677
|
+
case "forms.restored":
|
|
678
|
+
case "forms.repaired": return "all";
|
|
679
|
+
case "pages.flattened": return event.pages;
|
|
680
|
+
case "redaction.applied":
|
|
681
|
+
case "stream.desynced":
|
|
682
|
+
case "document.versioned": return "all";
|
|
683
|
+
default: return null;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
function createTriggers(ctx, { settings, form }) {
|
|
863
687
|
const triggerEnabled = (trigger) => {
|
|
688
|
+
const { triggers } = settings.get();
|
|
864
689
|
switch (trigger.scope) {
|
|
865
690
|
case "activate": return true;
|
|
866
|
-
case "annotation": return
|
|
867
|
-
case "page": return
|
|
868
|
-
case "document": return
|
|
691
|
+
case "annotation": return triggers.annotation;
|
|
692
|
+
case "page": return triggers.page;
|
|
693
|
+
case "document": return triggers.document;
|
|
869
694
|
}
|
|
870
695
|
};
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
return bearing;
|
|
884
|
-
};
|
|
885
|
-
{
|
|
886
|
-
const unsubscribe = ctx.doc?.events?.subscribe((event) => {
|
|
887
|
-
if (event.type.startsWith("annotation.") || event.type === "stream.desynced") lifecycleCache.clear();
|
|
696
|
+
const lifecycleReads = /* @__PURE__ */ new Map();
|
|
697
|
+
const lifecycleAnnotationsOf = (page) => {
|
|
698
|
+
const key = page.objectNumber;
|
|
699
|
+
const cached = lifecycleReads.get(key);
|
|
700
|
+
if (cached) return cached;
|
|
701
|
+
const read = readPageAnnotations(ctx, form, page).then((annotations) => annotations.filter((annotation) => annotation.actions && (annotation.actions.pageOpen?.root || annotation.actions.pageClose?.root || annotation.actions.pageVisible?.root || annotation.actions.pageInvisible?.root)).map((annotation) => ({
|
|
702
|
+
ref: annotation.ref,
|
|
703
|
+
actions: annotation.actions
|
|
704
|
+
})));
|
|
705
|
+
lifecycleReads.set(key, read);
|
|
706
|
+
read.catch(() => {
|
|
707
|
+
if (lifecycleReads.get(key) === read) lifecycleReads.delete(key);
|
|
888
708
|
});
|
|
889
|
-
|
|
890
|
-
}
|
|
709
|
+
return read;
|
|
710
|
+
};
|
|
711
|
+
/** Forget the pages a confirmed document event changed. */
|
|
712
|
+
const invalidate = (event) => {
|
|
713
|
+
const pages = annotationPagesOf(event);
|
|
714
|
+
if (pages === "all") lifecycleReads.clear();
|
|
715
|
+
else if (pages) for (const page of pages) lifecycleReads.delete(page.objectNumber);
|
|
716
|
+
};
|
|
891
717
|
/**
|
|
892
|
-
* ISO
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
718
|
+
* ISO 32000-2 Table 197 order: /PO "shall be executed after the O action …
|
|
719
|
+
* and the OpenAction entry"; /PC "shall be executed before the C action".
|
|
720
|
+
* Rootless trees are skipped (a budget-degraded tree with no root has
|
|
721
|
+
* nothing to walk).
|
|
896
722
|
*/
|
|
897
|
-
const planPageSteps = (event,
|
|
723
|
+
const planPageSteps = (event, page, pageActions, lifecycle) => {
|
|
898
724
|
const pageSource = {
|
|
899
725
|
kind: "page",
|
|
900
|
-
|
|
726
|
+
page
|
|
901
727
|
};
|
|
902
|
-
const
|
|
728
|
+
const annotationSteps = (key) => lifecycle.filter((annotation) => annotation.actions[key]?.root).map((annotation) => ({
|
|
903
729
|
source: {
|
|
904
730
|
kind: "annotation",
|
|
905
|
-
annotation:
|
|
906
|
-
|
|
731
|
+
annotation: annotation.ref,
|
|
732
|
+
page
|
|
907
733
|
},
|
|
908
|
-
tree:
|
|
734
|
+
tree: annotation.actions[key]
|
|
909
735
|
}));
|
|
910
736
|
switch (event) {
|
|
911
737
|
case "open": return [...pageActions?.open?.root ? [{
|
|
912
738
|
source: pageSource,
|
|
913
739
|
tree: pageActions.open
|
|
914
|
-
}] : [], ...
|
|
915
|
-
case "close": return [...
|
|
740
|
+
}] : [], ...annotationSteps("pageOpen")];
|
|
741
|
+
case "close": return [...annotationSteps("pageClose"), ...pageActions?.close?.root ? [{
|
|
916
742
|
source: pageSource,
|
|
917
743
|
tree: pageActions.close
|
|
918
744
|
}] : []];
|
|
919
|
-
case "visible": return
|
|
920
|
-
case "invisible": return
|
|
745
|
+
case "visible": return annotationSteps("pageVisible");
|
|
746
|
+
case "invisible": return annotationSteps("pageInvisible");
|
|
921
747
|
}
|
|
922
748
|
};
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
status: steps.length === 0 ? "inert" : statuses.every((s) => s === "refused") ? "refused" : statuses.every((s) => s === "inert") ? "inert" : statuses.every((s) => s === "executed") ? "executed" : "partial",
|
|
929
|
-
steps,
|
|
930
|
-
diagnostics
|
|
931
|
-
};
|
|
932
|
-
};
|
|
933
|
-
const runSteps = async (steps, origin, event, diagnostics) => {
|
|
934
|
-
const results = [];
|
|
935
|
-
for (const step of steps) {
|
|
936
|
-
const result = await runAndEmit(step.tree, {
|
|
937
|
-
origin,
|
|
938
|
-
source: step.source,
|
|
939
|
-
event
|
|
940
|
-
});
|
|
941
|
-
results.push({
|
|
942
|
-
source: step.source,
|
|
943
|
-
tree: step.tree,
|
|
944
|
-
result
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
return foldSteps(results, diagnostics);
|
|
749
|
+
return {
|
|
750
|
+
triggerEnabled,
|
|
751
|
+
lifecycleAnnotationsOf,
|
|
752
|
+
invalidate,
|
|
753
|
+
planPageSteps
|
|
948
754
|
};
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
755
|
+
}
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/lifecycle/document-events.ts
|
|
758
|
+
/**
|
|
759
|
+
* The verb-shaped document events (/WC, /WS, /DS, /WP, /DP) and the print
|
|
760
|
+
* door: one catalog lifecycle tree per event, run inside the current queue
|
|
761
|
+
* operation and never throwing (a broken script must not cancel the user's
|
|
762
|
+
* save or print), and a print request sent to the kernel's print, or, without
|
|
763
|
+
* a printer, to the adapter, wrapped /WP → print → /DP under the print latch.
|
|
764
|
+
*/
|
|
765
|
+
function createDocumentEvents(services, { ensureOpenSequenceBeforeDocEvent }, { runAndEmit }) {
|
|
766
|
+
const { reportDiagnostic } = services.events;
|
|
767
|
+
const { readDocumentActions, DOC_EVENT_TREES } = services.catalog;
|
|
768
|
+
const { enqueue, budget } = services.queue;
|
|
769
|
+
const ports = services.ports.slots;
|
|
770
|
+
const { allowsPrint } = services.authority;
|
|
771
|
+
const { documentPrint } = services;
|
|
772
|
+
const printLatch = services.printLatch;
|
|
773
|
+
/**
|
|
774
|
+
* Run one catalog lifecycle tree inside the current queue operation, shared
|
|
775
|
+
* by the dispatch path, `runDocumentVerb` and the print wrapper. Never
|
|
776
|
+
* throws: a read or resolution failure degrades to a diagnostic, because a
|
|
777
|
+
* broken script must not cancel the user's save or print.
|
|
778
|
+
*/
|
|
779
|
+
const runDocEventTreeSafe = async (event, diagnose) => {
|
|
780
|
+
if (!services.settings.get().triggers.document) return null;
|
|
957
781
|
try {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
782
|
+
await ensureOpenSequenceBeforeDocEvent();
|
|
783
|
+
const tree = (await readDocumentActions())?.[DOC_EVENT_TREES[event]];
|
|
784
|
+
if (!tree?.root && !tree?.incomplete) return null;
|
|
785
|
+
return {
|
|
786
|
+
source: { kind: "document" },
|
|
787
|
+
tree,
|
|
788
|
+
result: await runAndEmit(tree, {
|
|
789
|
+
origin: "lifecycle",
|
|
790
|
+
source: { kind: "document" },
|
|
791
|
+
event: {
|
|
792
|
+
scope: "document",
|
|
793
|
+
name: event
|
|
794
|
+
}
|
|
795
|
+
})
|
|
963
796
|
};
|
|
964
|
-
|
|
797
|
+
} catch (error) {
|
|
798
|
+
diagnose({
|
|
799
|
+
code: "trigger-failed",
|
|
800
|
+
message: `${event}: ${error instanceof Error ? error.message : String(error)}`
|
|
801
|
+
});
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
/** The dispatch-path body for a verb-shaped document event. */
|
|
806
|
+
const runDocumentEventOp = async (event, diagnostics, diagnose) => {
|
|
807
|
+
const step = await runDocEventTreeSafe(event, diagnose);
|
|
808
|
+
return foldSteps(step ? [step] : [], diagnostics);
|
|
809
|
+
};
|
|
810
|
+
/**
|
|
811
|
+
* Both print paths (the Named Print verb, and script `doc.print()` effects
|
|
812
|
+
* from the actions plane) go through here. While the latch is held
|
|
813
|
+
* (including `runDocumentVerb('print')`'s body) a nested request is
|
|
814
|
+
* suppressed with `reentrant-print`.
|
|
815
|
+
*
|
|
816
|
+
* With the kernel's print, a user's request starts `documents.print()` and
|
|
817
|
+
* returns: the kernel runs /WP and /DP around the dialog through this
|
|
818
|
+
* plugin's `aroundPrint` wrap, a `runDocumentVerb('print')` that waits for
|
|
819
|
+
* the current queue operation, so it is never awaited here. A hover or
|
|
820
|
+
* lifecycle request never opens the dialog.
|
|
821
|
+
*
|
|
822
|
+
* Without a printer: /WP → `ports.uiAdapter.print` exactly once → /DP,
|
|
823
|
+
* with the latch reset in `finally`. An adapter throw skips /DP (the latch
|
|
824
|
+
* still resets). /DP fires when the adapter call returns, since a browser
|
|
825
|
+
* cannot observe dialog completion.
|
|
826
|
+
*/
|
|
827
|
+
const firePrintThroughAdapter = async (uiContext, diagnose) => {
|
|
828
|
+
if (printLatch.active) {
|
|
829
|
+
diagnose({
|
|
830
|
+
code: "reentrant-print",
|
|
831
|
+
message: "print request during a document print event — suppressed (one dialog per request)"
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
status: "blocked",
|
|
835
|
+
detail: "reentrant print suppressed"
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
if (!allowsPrint()) {
|
|
839
|
+
diagnose({
|
|
840
|
+
code: "blocked",
|
|
841
|
+
message: "print: doc.print is not allowed"
|
|
842
|
+
});
|
|
843
|
+
return {
|
|
844
|
+
status: "blocked",
|
|
845
|
+
detail: "doc.print is not allowed"
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
if (documentPrint.available()) {
|
|
849
|
+
if (uiContext && uiContext.origin !== "user") {
|
|
965
850
|
diagnose({
|
|
966
|
-
code: "
|
|
967
|
-
message:
|
|
851
|
+
code: "blocked",
|
|
852
|
+
message: `print: a ${uiContext.origin} request never prints`
|
|
968
853
|
});
|
|
969
854
|
return {
|
|
970
|
-
status: "
|
|
971
|
-
|
|
972
|
-
diagnostics
|
|
855
|
+
status: "blocked",
|
|
856
|
+
detail: `${uiContext.origin} print request`
|
|
973
857
|
};
|
|
974
858
|
}
|
|
975
|
-
|
|
976
|
-
switch (trigger.scope) {
|
|
977
|
-
case "activate":
|
|
978
|
-
case "annotation": {
|
|
979
|
-
const event = trigger.scope === "activate" ? "activate" : trigger.event;
|
|
980
|
-
const { annotations } = await doc.page(trigger.pon).annotations.list();
|
|
981
|
-
const annotation = annotations.find((candidate) => sameRef(candidate.ref, trigger.ref));
|
|
982
|
-
if (event === "mouseUp" && annotation?.actions?.activate) return {
|
|
983
|
-
status: "inert",
|
|
984
|
-
steps: [],
|
|
985
|
-
diagnostics
|
|
986
|
-
};
|
|
987
|
-
const tree = annotation?.actions?.[event];
|
|
988
|
-
if (!tree?.root && !tree?.incomplete) return {
|
|
989
|
-
status: "inert",
|
|
990
|
-
steps: [],
|
|
991
|
-
diagnostics
|
|
992
|
-
};
|
|
993
|
-
const source = trigger.source ?? {
|
|
994
|
-
kind: "annotation",
|
|
995
|
-
annotation: trigger.ref,
|
|
996
|
-
pon: trigger.pon
|
|
997
|
-
};
|
|
998
|
-
return await runSteps([{
|
|
999
|
-
source,
|
|
1000
|
-
tree
|
|
1001
|
-
}], origin, require_types.eventOf(trigger), diagnostics);
|
|
1002
|
-
}
|
|
1003
|
-
case "page": {
|
|
1004
|
-
const layout = ctx.document()?.pages.find((page) => page.pageObjectNumber === trigger.pon);
|
|
1005
|
-
const lifecycle = await lifecycleTreesFor(trigger.pon);
|
|
1006
|
-
const steps = planPageSteps(trigger.event, trigger.pon, layout?.actions, lifecycle);
|
|
1007
|
-
return await runSteps(steps, origin, require_types.eventOf(trigger), diagnostics);
|
|
1008
|
-
}
|
|
1009
|
-
case "document":
|
|
1010
|
-
if (trigger.event !== "open") return await runDocumentEventOp(trigger.event, diagnostics, diagnose);
|
|
1011
|
-
if (openFired) {
|
|
1012
|
-
diagnose({
|
|
1013
|
-
code: "open-sequence-replayed",
|
|
1014
|
-
message: "document open sequence already fired for this document"
|
|
1015
|
-
});
|
|
1016
|
-
return {
|
|
1017
|
-
status: "inert",
|
|
1018
|
-
steps: [],
|
|
1019
|
-
diagnostics
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
openFired = true;
|
|
1023
|
-
return await runOpenSequenceOp();
|
|
1024
|
-
}
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
diagnose({
|
|
859
|
+
documentPrint.start().catch((error) => diagnose({
|
|
1027
860
|
code: "trigger-failed",
|
|
1028
|
-
message: `
|
|
861
|
+
message: `print: ${error instanceof Error ? error.message : String(error)}`
|
|
862
|
+
}));
|
|
863
|
+
return { status: "executed" };
|
|
864
|
+
}
|
|
865
|
+
if (!ports.uiAdapter) {
|
|
866
|
+
diagnose({
|
|
867
|
+
code: "no-adapter",
|
|
868
|
+
message: "print: no UI adapter installed"
|
|
1029
869
|
});
|
|
1030
870
|
return {
|
|
1031
|
-
status: "
|
|
1032
|
-
|
|
1033
|
-
diagnostics
|
|
871
|
+
status: "no-executor",
|
|
872
|
+
detail: "no UI adapter installed"
|
|
1034
873
|
};
|
|
1035
874
|
}
|
|
875
|
+
printLatch.active = true;
|
|
876
|
+
try {
|
|
877
|
+
await runDocEventTreeSafe("will-print", diagnose);
|
|
878
|
+
const adapter = ports.uiAdapter;
|
|
879
|
+
if (!adapter) return {
|
|
880
|
+
status: "no-executor",
|
|
881
|
+
detail: "UI adapter uninstalled mid-print"
|
|
882
|
+
};
|
|
883
|
+
if (uiContext) adapter.print(uiContext);
|
|
884
|
+
else adapter.print();
|
|
885
|
+
await runDocEventTreeSafe("did-print", diagnose);
|
|
886
|
+
return { status: "executed" };
|
|
887
|
+
} finally {
|
|
888
|
+
printLatch.active = false;
|
|
889
|
+
}
|
|
1036
890
|
};
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
891
|
+
return {
|
|
892
|
+
runDocumentEventOp,
|
|
893
|
+
firePrintThroughAdapter,
|
|
894
|
+
api: { runDocumentVerb: (verb, operation, options) => enqueue(async () => {
|
|
895
|
+
budget.scriptNodes = 0;
|
|
896
|
+
const diagnose = (diagnostic) => reportDiagnostic(diagnostic, { source: { kind: "document" } });
|
|
897
|
+
const before = verb === "save" ? "will-save" : "will-print";
|
|
898
|
+
const after = verb === "save" ? "did-save" : "did-print";
|
|
899
|
+
const body = async () => {
|
|
900
|
+
await runDocEventTreeSafe(before, diagnose);
|
|
901
|
+
if (options?.signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "actions", `${verb} was cancelled`, { cause: options.signal.reason });
|
|
902
|
+
const value = await operation();
|
|
903
|
+
await runDocEventTreeSafe(after, diagnose);
|
|
904
|
+
return value;
|
|
905
|
+
};
|
|
906
|
+
if (verb !== "print") return body();
|
|
907
|
+
printLatch.active = true;
|
|
908
|
+
try {
|
|
909
|
+
return await body();
|
|
910
|
+
} finally {
|
|
911
|
+
printLatch.active = false;
|
|
912
|
+
}
|
|
913
|
+
}, options) }
|
|
1043
914
|
};
|
|
1044
|
-
|
|
915
|
+
}
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/lifecycle/open-sequence.ts
|
|
918
|
+
function createOpenSequence(services, { releaseBarrier, resetCascade }, { runAndEmit }) {
|
|
919
|
+
const { reportDiagnostic, openSequenceCompleted } = services.events;
|
|
920
|
+
/** The `openSequence` setting, read when it decides: it applies until the sequence has run. */
|
|
921
|
+
const mode = () => services.settings.get().openSequence;
|
|
922
|
+
const { readDocumentActions } = services.catalog;
|
|
923
|
+
const { enqueue } = services.queue;
|
|
924
|
+
const ports = services.ports.slots;
|
|
925
|
+
let openFired = false;
|
|
926
|
+
let sawUserActivity = false;
|
|
927
|
+
/** The sequence body. Runs inside the queue (through dispatch or the
|
|
928
|
+
* latch's own enqueue); callers guarantee `openFired` was set. */
|
|
929
|
+
const runOpenSequenceOp = async () => {
|
|
930
|
+
const diagnostics = [];
|
|
931
|
+
const steps = [];
|
|
932
|
+
const openContext = {
|
|
933
|
+
origin: "lifecycle",
|
|
934
|
+
source: { kind: "document" },
|
|
935
|
+
event: {
|
|
936
|
+
scope: "document",
|
|
937
|
+
name: "open"
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
try {
|
|
941
|
+
const snapshot = await readDocumentActions();
|
|
942
|
+
if (snapshot?.openDestination) {
|
|
943
|
+
const tree = {
|
|
944
|
+
root: {
|
|
945
|
+
type: "goto",
|
|
946
|
+
subtype: "GoTo",
|
|
947
|
+
destination: snapshot.openDestination,
|
|
948
|
+
next: []
|
|
949
|
+
},
|
|
950
|
+
incomplete: false,
|
|
951
|
+
warningFlags: 0,
|
|
952
|
+
warnings: []
|
|
953
|
+
};
|
|
954
|
+
steps.push({
|
|
955
|
+
source: { kind: "document" },
|
|
956
|
+
tree,
|
|
957
|
+
result: await runAndEmit(tree, openContext)
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
if (snapshot?.openAction?.root || snapshot?.openAction?.incomplete) steps.push({
|
|
961
|
+
source: { kind: "document" },
|
|
962
|
+
tree: snapshot.openAction,
|
|
963
|
+
result: await runAndEmit(snapshot.openAction, openContext)
|
|
964
|
+
});
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const diagnostic = {
|
|
967
|
+
code: "trigger-failed",
|
|
968
|
+
message: `open sequence failed: ${error instanceof Error ? error.message : String(error)}`
|
|
969
|
+
};
|
|
970
|
+
diagnostics.push(diagnostic);
|
|
971
|
+
reportDiagnostic(diagnostic, { source: { kind: "document" } });
|
|
972
|
+
} finally {
|
|
973
|
+
releaseBarrier(true);
|
|
974
|
+
}
|
|
975
|
+
const result = foldSteps(steps, diagnostics);
|
|
976
|
+
openSequenceCompleted.emit({ result });
|
|
977
|
+
return result;
|
|
978
|
+
};
|
|
979
|
+
/**
|
|
980
|
+
* The catalog /OpenAction never runs after a /WS, /WP or /WC script: before
|
|
981
|
+
* the first verb-shaped document event, an armed but unfired open sequence
|
|
982
|
+
* runs inline (the caller is already inside the queue; `runOpenSequenceOp`
|
|
983
|
+
* never enqueues). It does not wait for the initial page /O: the guarantee
|
|
984
|
+
* is catalog-level.
|
|
985
|
+
*/
|
|
986
|
+
const ensureOpenSequenceBeforeDocEvent = async () => {
|
|
987
|
+
if (openFired || mode() === "off") return;
|
|
988
|
+
openFired = true;
|
|
989
|
+
await runOpenSequenceOp();
|
|
990
|
+
};
|
|
991
|
+
const maybeFireOpenSequence = () => {
|
|
992
|
+
if (openFired) return;
|
|
993
|
+
if (mode() === "off") {
|
|
994
|
+
openFired = true;
|
|
995
|
+
releaseBarrier(false);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (!ports.uiAdapter && mode() !== "headless" && !sawUserActivity) return;
|
|
999
|
+
openFired = true;
|
|
1000
|
+
enqueue(() => runOpenSequenceOp());
|
|
1001
|
+
};
|
|
1002
|
+
const noteUserActivity = () => {
|
|
1003
|
+
resetCascade();
|
|
1004
|
+
if (!sawUserActivity) {
|
|
1005
|
+
sawUserActivity = true;
|
|
1006
|
+
maybeFireOpenSequence();
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
/** Claim the one-shot open for a dispatched document-open trigger: false
|
|
1010
|
+
* when it already fired (the caller reports a replay). */
|
|
1011
|
+
const claim = () => {
|
|
1012
|
+
if (openFired) return false;
|
|
1013
|
+
openFired = true;
|
|
1014
|
+
return true;
|
|
1015
|
+
};
|
|
1016
|
+
return {
|
|
1017
|
+
run: runOpenSequenceOp,
|
|
1018
|
+
ensureOpenSequenceBeforeDocEvent,
|
|
1019
|
+
noteUserActivity,
|
|
1020
|
+
claim,
|
|
1021
|
+
/** Arm the latch at bringup: fires immediately for 'headless', releases
|
|
1022
|
+
* the barrier for 'off', waits for an adapter or user activity for 'auto'. */
|
|
1023
|
+
arm: maybeFireOpenSequence,
|
|
1024
|
+
api: { setUiAdapter: (adapter) => {
|
|
1025
|
+
ports.uiAdapter = adapter;
|
|
1026
|
+
if (adapter) maybeFireOpenSequence();
|
|
1027
|
+
return () => {
|
|
1028
|
+
if (ports.uiAdapter === adapter) ports.uiAdapter = null;
|
|
1029
|
+
};
|
|
1030
|
+
} }
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/lifecycle/page-lifecycle.ts
|
|
1035
|
+
/**
|
|
1036
|
+
* The lifecycle coordinator's page half: the document-open barrier and the
|
|
1037
|
+
* cascade budget. The stage owns page truth (it reports through
|
|
1038
|
+
* `reportPageState`); this owns when page-lifecycle triggers fire: nothing
|
|
1039
|
+
* emits before the document-open sequence has run (or was declared off or
|
|
1040
|
+
* headless), and emission is a diff against the last-emitted state, so
|
|
1041
|
+
* pre-open motion collapses to one open with no phantom close.
|
|
1042
|
+
*/
|
|
1043
|
+
/** Consecutive programmatic rounds after which page-lifecycle emission is suppressed. */
|
|
1044
|
+
const CASCADE_LIMIT = 8;
|
|
1045
|
+
function createPageLifecycle(ctx, { events, settings }, { dispatch }) {
|
|
1046
|
+
const { reportDiagnostic } = events;
|
|
1045
1047
|
let barrierOpen = false;
|
|
1046
1048
|
let bufferedReport = null;
|
|
1047
1049
|
let lastEmitted = {
|
|
1048
|
-
|
|
1050
|
+
current: null,
|
|
1049
1051
|
visible: /* @__PURE__ */ new Set()
|
|
1050
1052
|
};
|
|
1051
1053
|
let cascadeRounds = 0;
|
|
1052
|
-
let openFired = false;
|
|
1053
|
-
let sawUserActivity = false;
|
|
1054
1054
|
const emitForReport = (report) => {
|
|
1055
1055
|
if (report.cause === "user") cascadeRounds = 0;
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1059
|
-
const
|
|
1056
|
+
const current = report.currentPage === null ? null : report.currentPage.objectNumber;
|
|
1057
|
+
const nextVisible = new Set(report.visiblePages.map((page) => page.objectNumber));
|
|
1058
|
+
const changedCurrent = current !== lastEmitted.current;
|
|
1059
|
+
const leaving = [...lastEmitted.visible].filter((pageObjectNumber) => !nextVisible.has(pageObjectNumber));
|
|
1060
|
+
const entering = [...nextVisible].filter((pageObjectNumber) => !lastEmitted.visible.has(pageObjectNumber));
|
|
1060
1061
|
if (!changedCurrent && leaving.length === 0 && entering.length === 0) return;
|
|
1061
|
-
const previousCurrent = lastEmitted.
|
|
1062
|
+
const previousCurrent = lastEmitted.current;
|
|
1062
1063
|
lastEmitted = {
|
|
1063
|
-
|
|
1064
|
+
current,
|
|
1064
1065
|
visible: nextVisible
|
|
1065
1066
|
};
|
|
1066
1067
|
if (report.cause === "programmatic") {
|
|
1067
1068
|
cascadeRounds += 1;
|
|
1068
|
-
if (cascadeRounds >
|
|
1069
|
-
|
|
1069
|
+
if (cascadeRounds > CASCADE_LIMIT) {
|
|
1070
|
+
reportDiagnostic({
|
|
1070
1071
|
code: "cascade-budget",
|
|
1071
|
-
message: `page-lifecycle emission suppressed: ${cascadeRounds} consecutive programmatic rounds (cap ${
|
|
1072
|
+
message: `page-lifecycle emission suppressed: ${cascadeRounds} consecutive programmatic rounds (cap ${CASCADE_LIMIT})`
|
|
1072
1073
|
});
|
|
1073
1074
|
return;
|
|
1074
1075
|
}
|
|
@@ -1076,22 +1077,22 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
1076
1077
|
if (changedCurrent && previousCurrent !== null) dispatch({
|
|
1077
1078
|
scope: "page",
|
|
1078
1079
|
event: "close",
|
|
1079
|
-
|
|
1080
|
+
page: (0, _embedpdf_core.toPageRef)(previousCurrent)
|
|
1080
1081
|
});
|
|
1081
|
-
for (const
|
|
1082
|
+
for (const pageObjectNumber of leaving) dispatch({
|
|
1082
1083
|
scope: "page",
|
|
1083
1084
|
event: "invisible",
|
|
1084
|
-
|
|
1085
|
+
page: (0, _embedpdf_core.toPageRef)(pageObjectNumber)
|
|
1085
1086
|
});
|
|
1086
|
-
for (const
|
|
1087
|
+
for (const pageObjectNumber of entering) dispatch({
|
|
1087
1088
|
scope: "page",
|
|
1088
1089
|
event: "visible",
|
|
1089
|
-
|
|
1090
|
+
page: (0, _embedpdf_core.toPageRef)(pageObjectNumber)
|
|
1090
1091
|
});
|
|
1091
|
-
if (changedCurrent &&
|
|
1092
|
+
if (changedCurrent && current !== null) dispatch({
|
|
1092
1093
|
scope: "page",
|
|
1093
1094
|
event: "open",
|
|
1094
|
-
|
|
1095
|
+
page: (0, _embedpdf_core.toPageRef)(current)
|
|
1095
1096
|
});
|
|
1096
1097
|
};
|
|
1097
1098
|
const reportPageState = (report) => {
|
|
@@ -1103,333 +1104,848 @@ function createActionsCapability(ctx, config = {}) {
|
|
|
1103
1104
|
}
|
|
1104
1105
|
emitForReport(report);
|
|
1105
1106
|
};
|
|
1106
|
-
const releaseBarrier = (fireFallback) => {
|
|
1107
|
-
barrierOpen = true;
|
|
1108
|
-
const report = bufferedReport;
|
|
1109
|
-
bufferedReport = null;
|
|
1110
|
-
try {
|
|
1111
|
-
if (report) emitForReport(report);
|
|
1112
|
-
else if (fireFallback &&
|
|
1113
|
-
const first = ctx.document()?.pages[0]?.
|
|
1114
|
-
if (first !== void 0) {
|
|
1115
|
-
lastEmitted = {
|
|
1116
|
-
|
|
1117
|
-
visible: lastEmitted.visible
|
|
1118
|
-
};
|
|
1119
|
-
dispatch({
|
|
1120
|
-
scope: "page",
|
|
1121
|
-
event: "open",
|
|
1122
|
-
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
} catch (error) {
|
|
1127
|
-
|
|
1128
|
-
code: "trigger-failed",
|
|
1129
|
-
message: `barrier release failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1107
|
+
const releaseBarrier = (fireFallback) => {
|
|
1108
|
+
barrierOpen = true;
|
|
1109
|
+
const report = bufferedReport;
|
|
1110
|
+
bufferedReport = null;
|
|
1111
|
+
try {
|
|
1112
|
+
if (report) emitForReport(report);
|
|
1113
|
+
else if (fireFallback && settings.get().openSequence === "headless") {
|
|
1114
|
+
const first = ctx.document()?.pages[0]?.ref.objectNumber;
|
|
1115
|
+
if (first !== void 0) {
|
|
1116
|
+
lastEmitted = {
|
|
1117
|
+
current: first,
|
|
1118
|
+
visible: lastEmitted.visible
|
|
1119
|
+
};
|
|
1120
|
+
dispatch({
|
|
1121
|
+
scope: "page",
|
|
1122
|
+
event: "open",
|
|
1123
|
+
page: (0, _embedpdf_core.toPageRef)(first)
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
reportDiagnostic({
|
|
1129
|
+
code: "trigger-failed",
|
|
1130
|
+
message: `barrier release failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
return {
|
|
1135
|
+
releaseBarrier,
|
|
1136
|
+
/** A user gesture resets the programmatic-cascade counter. */
|
|
1137
|
+
resetCascade: () => {
|
|
1138
|
+
cascadeRounds = 0;
|
|
1139
|
+
},
|
|
1140
|
+
api: { reportPageState }
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/scripting/world.ts
|
|
1145
|
+
/**
|
|
1146
|
+
* Script world building (a page-scoped prefetch: the form as the user sees it
|
|
1147
|
+
* plus the annotations of the page the action runs on) and the event bridge between
|
|
1148
|
+
* two standards: the /AA keys are ISO 32000-2 Table 200; the event names
|
|
1149
|
+
* live in the JavaScript API layer (ISO 21757-1, Acrobat).
|
|
1150
|
+
*/
|
|
1151
|
+
function createScriptWorld(ctx, { form }) {
|
|
1152
|
+
const engineColorToArray = (color) => {
|
|
1153
|
+
if (!color) return ["T"];
|
|
1154
|
+
const { r, g, b } = (0, _embedpdf_engine_core_runtime.rgbOf)(color);
|
|
1155
|
+
return [
|
|
1156
|
+
"RGB",
|
|
1157
|
+
r / 255,
|
|
1158
|
+
g / 255,
|
|
1159
|
+
b / 255
|
|
1160
|
+
];
|
|
1161
|
+
};
|
|
1162
|
+
const scriptWorldFor = async (pageObjectNumber) => {
|
|
1163
|
+
const page = (0, _embedpdf_engine_core_runtime.toPageRef)(pageObjectNumber);
|
|
1164
|
+
const snapshot = await form.read();
|
|
1165
|
+
const layout = ctx.getPage(page);
|
|
1166
|
+
const pageIndex = Math.max(0, layout?.index ?? -1);
|
|
1167
|
+
const { annotations } = await ctx.doc.page(page).annotations.list();
|
|
1168
|
+
const annots = annotations.filter((annotation) => annotation.subtype !== "link" && !annotation.subtype.startsWith("widget")).map((annotation) => {
|
|
1169
|
+
const inFile = layout ? (0, _embedpdf_engine_core_runtime.pdfRectOf)(annotation.rect, layout.pdfCropBox) : null;
|
|
1170
|
+
const styled = annotation;
|
|
1171
|
+
return {
|
|
1172
|
+
ref: annotation.ref,
|
|
1173
|
+
name: annotation.nm ?? "",
|
|
1174
|
+
subtype: annotation.subtype,
|
|
1175
|
+
page: pageIndex,
|
|
1176
|
+
rect: inFile ? [
|
|
1177
|
+
inFile.left,
|
|
1178
|
+
inFile.bottom,
|
|
1179
|
+
inFile.right,
|
|
1180
|
+
inFile.top
|
|
1181
|
+
] : [
|
|
1182
|
+
0,
|
|
1183
|
+
0,
|
|
1184
|
+
0,
|
|
1185
|
+
0
|
|
1186
|
+
],
|
|
1187
|
+
contents: annotation.contents ?? "",
|
|
1188
|
+
author: annotation.author ?? "",
|
|
1189
|
+
subject: annotation.subject ?? "",
|
|
1190
|
+
strokeColor: engineColorToArray(styled.color),
|
|
1191
|
+
fillColor: engineColorToArray(styled.interiorColor),
|
|
1192
|
+
opacity: styled.opacity ?? 1,
|
|
1193
|
+
width: styled.strokeWidth ?? 1,
|
|
1194
|
+
borderStyle: styled.borderStyle === "dashed" ? "D" : "S",
|
|
1195
|
+
dash: styled.dashArray ?? [],
|
|
1196
|
+
hidden: annotation.hidden,
|
|
1197
|
+
print: annotation.print,
|
|
1198
|
+
readOnly: annotation.readOnly,
|
|
1199
|
+
locked: annotation.locked,
|
|
1200
|
+
noView: annotation.noView,
|
|
1201
|
+
toggleNoView: annotation.toggleNoView,
|
|
1202
|
+
opaqueBody: annotation.subtype === "stamp"
|
|
1203
|
+
};
|
|
1204
|
+
});
|
|
1205
|
+
return {
|
|
1206
|
+
snapshot,
|
|
1207
|
+
world: {
|
|
1208
|
+
fields: (0, _embedpdf_core_acrojs.scriptFieldsFromSnapshot)(snapshot),
|
|
1209
|
+
annots,
|
|
1210
|
+
annotPages: [pageIndex],
|
|
1211
|
+
annotsCoverDocument: (ctx.document()?.pageCount ?? 0) <= 1
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
};
|
|
1215
|
+
const ANNOTATION_EVENT_NAMES = {
|
|
1216
|
+
cursorEnter: "Mouse Enter",
|
|
1217
|
+
cursorExit: "Mouse Exit",
|
|
1218
|
+
mouseDown: "Mouse Down",
|
|
1219
|
+
mouseUp: "Mouse Up",
|
|
1220
|
+
focus: "Focus",
|
|
1221
|
+
blur: "Blur"
|
|
1222
|
+
};
|
|
1223
|
+
const PAGE_EVENT_NAMES = {
|
|
1224
|
+
open: "Open",
|
|
1225
|
+
close: "Close",
|
|
1226
|
+
visible: "Visible",
|
|
1227
|
+
invisible: "Invisible"
|
|
1228
|
+
};
|
|
1229
|
+
const DOC_EVENT_NAMES = {
|
|
1230
|
+
open: "Open",
|
|
1231
|
+
"will-save": "WillSave",
|
|
1232
|
+
"did-save": "DidSave",
|
|
1233
|
+
"will-print": "WillPrint",
|
|
1234
|
+
"did-print": "DidPrint",
|
|
1235
|
+
"will-close": "WillClose"
|
|
1236
|
+
};
|
|
1237
|
+
const scriptEventFor = (actionContext, snapshot) => {
|
|
1238
|
+
const provenance = actionContext.event;
|
|
1239
|
+
const type = provenance.scope === "page" ? "Page" : provenance.scope === "document" ? "Doc" : actionContext.source.kind === "widget" ? "Field" : actionContext.source.kind === "link" ? "Link" : actionContext.source.kind === "outline" ? "Bookmark" : "Annot";
|
|
1240
|
+
const name = provenance.scope === "activate" ? "Mouse Up" : provenance.scope === "annotation" ? ANNOTATION_EVENT_NAMES[provenance.name] ?? "Mouse Up" : provenance.scope === "page" ? PAGE_EVENT_NAMES[provenance.name] ?? "Open" : DOC_EVENT_NAMES[provenance.name] ?? "Open";
|
|
1241
|
+
const targetRef = actionContext.source.kind === "widget" ? actionContext.source.field : void 0;
|
|
1242
|
+
const targetField = targetRef ? snapshot.fields.find((field) => targetRef.kind === "objectNumber" && field.ref.kind === "objectNumber" ? field.ref.objectNumber === targetRef.objectNumber : targetRef.kind === "fqn" && field.name === targetRef.name) : void 0;
|
|
1243
|
+
return {
|
|
1244
|
+
kind: "widget-activate",
|
|
1245
|
+
type,
|
|
1246
|
+
name,
|
|
1247
|
+
...targetRef ? {
|
|
1248
|
+
target: targetRef,
|
|
1249
|
+
source: targetRef
|
|
1250
|
+
} : {},
|
|
1251
|
+
...targetField && targetField.valueEntry.kind === "scalar" ? { value: targetField.valueEntry.value } : {}
|
|
1252
|
+
};
|
|
1253
|
+
};
|
|
1254
|
+
return {
|
|
1255
|
+
scriptWorldFor,
|
|
1256
|
+
scriptEventFor
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/scripting/executor.ts
|
|
1261
|
+
/** The default cap on JavaScript nodes one dispatch may run. */
|
|
1262
|
+
const DEFAULT_SCRIPT_NODES_PER_DISPATCH = 16;
|
|
1263
|
+
function registerScriptExecutor(ctx, services, environment, { scriptHost }, { performSubmit }, { surfaceScriptResult }, documentEvents) {
|
|
1264
|
+
const { reportDiagnostic } = services.events;
|
|
1265
|
+
const ports = services.ports.slots;
|
|
1266
|
+
const { budget } = services.queue;
|
|
1267
|
+
const { firePrintThroughAdapter } = documentEvents;
|
|
1268
|
+
const { scriptWorldFor, scriptEventFor } = createScriptWorld(ctx, services);
|
|
1269
|
+
/** Commit one run's document effects through the owner sinks in the
|
|
1270
|
+
* declared order (form first, then annotations); the first failure skips
|
|
1271
|
+
* the rest across both streams. Returns the failure summary, if any. */
|
|
1272
|
+
const commitScriptOutput = async (output, diagnose) => {
|
|
1273
|
+
let failure = null;
|
|
1274
|
+
if (output.formEffects.length > 0) if (!ports.formCommitSink) diagnose({
|
|
1275
|
+
code: "no-commit-sink",
|
|
1276
|
+
message: `script form effects dropped: no form commit sink registered (${output.formEffects.length})`
|
|
1277
|
+
});
|
|
1278
|
+
else {
|
|
1279
|
+
const result = await ports.formCommitSink(output.formEffects);
|
|
1280
|
+
if (result.status === "failed") failure = `form effects: ${result.error ?? "failed"}`;
|
|
1281
|
+
}
|
|
1282
|
+
if (output.annotEffects.length > 0) if (failure) diagnose({
|
|
1283
|
+
code: "executor-failed",
|
|
1284
|
+
message: `script annot effects skipped after form failure (${output.annotEffects.length})`
|
|
1285
|
+
});
|
|
1286
|
+
else if (!ports.annotCommitSink) diagnose({
|
|
1287
|
+
code: "no-commit-sink",
|
|
1288
|
+
message: `script annot effects dropped: no annotation commit sink registered (${output.annotEffects.length})`
|
|
1289
|
+
});
|
|
1290
|
+
else {
|
|
1291
|
+
const entries = output.annotEffects.map((effect) => ({
|
|
1292
|
+
annotObjectNumber: effect.ref.kind === "objectNumber" ? effect.ref.objectNumber : -1,
|
|
1293
|
+
...effect.ref.kind === "objectNumber" ? { page: effect.ref.page } : {},
|
|
1294
|
+
patch: effect.patch
|
|
1295
|
+
}));
|
|
1296
|
+
const firstFailure = (await ports.annotCommitSink(entries)).results.find((entry) => entry.status === "failed");
|
|
1297
|
+
if (firstFailure) failure = `annotation ${firstFailure.annotObjectNumber}: ${firstFailure.error ?? "failed"}`;
|
|
1298
|
+
}
|
|
1299
|
+
if (failure) diagnose({
|
|
1300
|
+
code: "executor-failed",
|
|
1301
|
+
message: `script commit: ${failure}`
|
|
1302
|
+
});
|
|
1303
|
+
return failure;
|
|
1304
|
+
};
|
|
1305
|
+
if (!scriptHost) return;
|
|
1306
|
+
const nodeLimit = environment.maxScriptNodesPerDispatch ?? DEFAULT_SCRIPT_NODES_PER_DISPATCH;
|
|
1307
|
+
ports.executors.set("javascript", async (node, actionContext) => {
|
|
1308
|
+
if (node.type !== "javascript") return {
|
|
1309
|
+
status: "inert",
|
|
1310
|
+
reason: "not a JS node"
|
|
1311
|
+
};
|
|
1312
|
+
if (budget.scriptNodes >= nodeLimit) return {
|
|
1313
|
+
status: "inert",
|
|
1314
|
+
reason: `dispatch script budget exhausted (${nodeLimit} JS nodes)`
|
|
1315
|
+
};
|
|
1316
|
+
budget.scriptNodes += 1;
|
|
1317
|
+
const source = actionContext.source;
|
|
1318
|
+
const firstPage = ctx.document()?.pages[0]?.ref.objectNumber;
|
|
1319
|
+
const pageObjectNumber = source.kind === "widget" || source.kind === "page" ? source.page.objectNumber : source.kind === "link" || source.kind === "annotation" ? source.page?.objectNumber ?? firstPage : firstPage;
|
|
1320
|
+
if (pageObjectNumber === void 0) return {
|
|
1321
|
+
status: "inert",
|
|
1322
|
+
reason: "no page to anchor the world on"
|
|
1323
|
+
};
|
|
1324
|
+
const diagnose = (diagnostic) => reportDiagnostic(diagnostic, {
|
|
1325
|
+
action: "javascript",
|
|
1326
|
+
source
|
|
1327
|
+
});
|
|
1328
|
+
const pendingExternal = [];
|
|
1329
|
+
const splitExternal = (output, phase) => output.uiEffects.filter((effect) => {
|
|
1330
|
+
if (effect.kind === "print" || effect.kind === "submitForm") {
|
|
1331
|
+
pendingExternal.push({
|
|
1332
|
+
effect,
|
|
1333
|
+
phase
|
|
1334
|
+
});
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
return true;
|
|
1338
|
+
});
|
|
1339
|
+
const outcome = await scriptHost.transaction(async (transaction) => {
|
|
1340
|
+
let built = await scriptWorldFor(pageObjectNumber);
|
|
1341
|
+
const boot = await transaction.boot({
|
|
1342
|
+
...built.world,
|
|
1343
|
+
event: {
|
|
1344
|
+
kind: "name-tree-boot",
|
|
1345
|
+
type: "Doc",
|
|
1346
|
+
name: "Open"
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
if (boot) {
|
|
1350
|
+
await commitScriptOutput(boot, diagnose);
|
|
1351
|
+
surfaceScriptResult({
|
|
1352
|
+
uiEffects: splitExternal(boot, "boot"),
|
|
1353
|
+
diagnostics: boot.diagnostics,
|
|
1354
|
+
...boot.error ? { error: boot.error } : {},
|
|
1355
|
+
origin: actionContext.origin,
|
|
1356
|
+
phase: "boot",
|
|
1357
|
+
realm: "document",
|
|
1358
|
+
source
|
|
1359
|
+
});
|
|
1360
|
+
if (boot.formEffects.length || boot.annotEffects.length) built = await scriptWorldFor(pageObjectNumber);
|
|
1361
|
+
}
|
|
1362
|
+
const output = await transaction.run(node.script, {
|
|
1363
|
+
...built.world,
|
|
1364
|
+
event: scriptEventFor(actionContext, built.snapshot)
|
|
1365
|
+
});
|
|
1366
|
+
surfaceScriptResult({
|
|
1367
|
+
uiEffects: splitExternal(output, "user"),
|
|
1368
|
+
diagnostics: output.diagnostics,
|
|
1369
|
+
...output.error ? { error: output.error } : {},
|
|
1370
|
+
origin: actionContext.origin,
|
|
1371
|
+
phase: "user",
|
|
1372
|
+
realm: "document",
|
|
1373
|
+
source
|
|
1374
|
+
});
|
|
1375
|
+
if (output.error) return {
|
|
1376
|
+
status: "failed",
|
|
1377
|
+
error: output.error.message
|
|
1378
|
+
};
|
|
1379
|
+
const failure = await commitScriptOutput(output, diagnose);
|
|
1380
|
+
return failure ? {
|
|
1381
|
+
status: "failed",
|
|
1382
|
+
error: failure
|
|
1383
|
+
} : { status: "executed" };
|
|
1384
|
+
});
|
|
1385
|
+
for (const entry of pendingExternal) if (entry.effect.kind === "print") await firePrintThroughAdapter({
|
|
1386
|
+
origin: actionContext.origin,
|
|
1387
|
+
phase: entry.phase
|
|
1388
|
+
}, diagnose);
|
|
1389
|
+
else await performSubmit(intentOfSubmitEffect(entry.effect), actionContext, diagnose);
|
|
1390
|
+
return outcome;
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
//#endregion
|
|
1394
|
+
//#region src/scripting/environment.ts
|
|
1395
|
+
function createScriptRealmFactory(environment, identity, identityDoc) {
|
|
1396
|
+
const sandboxFactory = environment.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox()));
|
|
1397
|
+
const budget = environment.budget ?? _embedpdf_core_acrojs.DEFAULT_SCRIPT_BUDGET;
|
|
1398
|
+
return {
|
|
1399
|
+
budget,
|
|
1400
|
+
realmFor: (target) => (0, _embedpdf_core_acrojs.createScriptHost)({
|
|
1401
|
+
sandboxFactory,
|
|
1402
|
+
document: () => {
|
|
1403
|
+
const documentInfo = target.document();
|
|
1404
|
+
return {
|
|
1405
|
+
id: target.doc.id,
|
|
1406
|
+
fileName: environment.fileName?.() ?? documentInfo?.name ?? "document.pdf",
|
|
1407
|
+
pageCount: documentInfo?.pageCount ?? 0,
|
|
1408
|
+
pageNumber: 0
|
|
1409
|
+
};
|
|
1410
|
+
},
|
|
1411
|
+
identity: () => (0, _embedpdf_core_acrojs.resolveScriptIdentity)(identityDoc, identity()),
|
|
1412
|
+
environment: (sequence) => {
|
|
1413
|
+
const nowMs = environment.now?.() ?? Date.now();
|
|
1414
|
+
return {
|
|
1415
|
+
nowMs,
|
|
1416
|
+
utcOffsetMinutes: environment.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
|
|
1417
|
+
randomSeed: environment.randomSeed?.() ?? (0, _embedpdf_core_acrojs.seedFrom)(target.doc.id, sequence)
|
|
1418
|
+
};
|
|
1419
|
+
},
|
|
1420
|
+
bootSources: target.bootSources,
|
|
1421
|
+
budget
|
|
1422
|
+
})
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
//#endregion
|
|
1426
|
+
//#region src/scripting/realm.ts
|
|
1427
|
+
/**
|
|
1428
|
+
* The one ScriptHost per document. The `javascript.enabled` setting, as it is
|
|
1429
|
+
* when the document opens, decides whether a realm factory exists; the
|
|
1430
|
+
* factory carries the session environment and mints this document's own
|
|
1431
|
+
* realm here, and detached realms on demand (host lens): one environment, any
|
|
1432
|
+
* number of isolated realms.
|
|
1433
|
+
*/
|
|
1434
|
+
function createRealm(ctx, { catalog, settings }, environment) {
|
|
1435
|
+
const { readDocumentActions } = catalog;
|
|
1436
|
+
const realms = settings.get().javascript.enabled ? createScriptRealmFactory(environment, () => settings.get().javascript.identity ?? void 0, ctx.doc) : null;
|
|
1437
|
+
const scriptHost = realms ? realms.realmFor({
|
|
1438
|
+
doc: ctx.doc,
|
|
1439
|
+
document: () => ctx.document(),
|
|
1440
|
+
bootSources: async () => {
|
|
1441
|
+
return ((await readDocumentActions())?.nameTreeScripts ?? []).map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action));
|
|
1442
|
+
}
|
|
1443
|
+
}) : null;
|
|
1444
|
+
if (scriptHost) ctx.cleanup(() => scriptHost.dispose());
|
|
1445
|
+
return {
|
|
1446
|
+
scriptHost,
|
|
1447
|
+
api: {
|
|
1448
|
+
isScriptingEnabled: () => scriptHost !== null,
|
|
1449
|
+
...scriptHost && realms ? {
|
|
1450
|
+
scriptRealm: {
|
|
1451
|
+
transaction: (body) => scriptHost.transaction(body),
|
|
1452
|
+
budget: realms.budget
|
|
1453
|
+
},
|
|
1454
|
+
createDetachedScriptRealm: (target) => {
|
|
1455
|
+
const host = realms.realmFor(target);
|
|
1456
|
+
return {
|
|
1457
|
+
transaction: (body) => host.transaction(body),
|
|
1458
|
+
budget: realms.budget,
|
|
1459
|
+
dispose: () => host.dispose()
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
} : {}
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region src/scripting/surface.ts
|
|
1468
|
+
function createScriptSurface(services, { performSubmit }) {
|
|
1469
|
+
const { reportDiagnostic, scriptDiagnosticReported, scriptFailed } = services.events;
|
|
1470
|
+
const ports = services.ports.slots;
|
|
1471
|
+
const { allowsPrint } = services.authority;
|
|
1472
|
+
const { documentPrint } = services;
|
|
1473
|
+
const printLatch = services.printLatch;
|
|
1474
|
+
const surfaceScriptResult = (result) => {
|
|
1475
|
+
const uiContext = {
|
|
1476
|
+
origin: result.origin,
|
|
1477
|
+
phase: result.phase
|
|
1478
|
+
};
|
|
1479
|
+
const source = result.source ?? null;
|
|
1480
|
+
for (const effect of result.uiEffects) {
|
|
1481
|
+
if (result.realm === "detached" && effect.kind !== "alert") {
|
|
1482
|
+
scriptDiagnosticReported.emit({
|
|
1483
|
+
code: "ui-effect-suppressed",
|
|
1484
|
+
message: `script ${effect.kind} request from a detached realm suppressed: no document surface`
|
|
1485
|
+
});
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (effect.kind === "submitForm") {
|
|
1489
|
+
performSubmit(intentOfSubmitEffect(effect), {
|
|
1490
|
+
origin: result.origin,
|
|
1491
|
+
source: result.source ?? { kind: "api" },
|
|
1492
|
+
event: { scope: "activate" }
|
|
1493
|
+
}, (diagnostic) => reportDiagnostic(diagnostic, {
|
|
1494
|
+
action: "javascript",
|
|
1495
|
+
source
|
|
1496
|
+
}));
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (effect.kind === "print" && !allowsPrint()) {
|
|
1500
|
+
scriptDiagnosticReported.emit({
|
|
1501
|
+
code: "ui-effect-suppressed",
|
|
1502
|
+
message: "script print request withheld: doc.print is not allowed"
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (effect.kind === "print" && printLatch.active) {
|
|
1507
|
+
scriptDiagnosticReported.emit({
|
|
1508
|
+
code: "ui-effect-suppressed",
|
|
1509
|
+
message: "script print request during a document print event — suppressed (reentrant)"
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (effect.kind === "print" && documentPrint.available()) {
|
|
1514
|
+
if (result.origin !== "user") {
|
|
1515
|
+
scriptDiagnosticReported.emit({
|
|
1516
|
+
code: "ui-effect-suppressed",
|
|
1517
|
+
message: `script print request from a ${result.origin} event suppressed`
|
|
1518
|
+
});
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
documentPrint.start().catch((error) => reportDiagnostic({
|
|
1522
|
+
code: "trigger-failed",
|
|
1523
|
+
message: `print: ${error instanceof Error ? error.message : String(error)}`
|
|
1524
|
+
}, {
|
|
1525
|
+
action: "javascript",
|
|
1526
|
+
source
|
|
1527
|
+
}));
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (!ports.uiAdapter) {
|
|
1531
|
+
reportDiagnostic({
|
|
1532
|
+
code: "no-adapter",
|
|
1533
|
+
message: `script ${effect.kind}: no UI adapter installed`
|
|
1534
|
+
}, {
|
|
1535
|
+
action: "javascript",
|
|
1536
|
+
source
|
|
1537
|
+
});
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (effect.kind === "alert") ports.uiAdapter.alert?.(effect.message, {
|
|
1541
|
+
...uiContext,
|
|
1542
|
+
icon: effect.icon,
|
|
1543
|
+
...effect.title !== void 0 ? { title: effect.title } : {}
|
|
1544
|
+
});
|
|
1545
|
+
else if (effect.kind === "gotoPage") ports.uiAdapter.gotoPage?.(effect.page, uiContext);
|
|
1546
|
+
else ports.uiAdapter.print(uiContext);
|
|
1547
|
+
}
|
|
1548
|
+
for (const diagnostic of result.diagnostics) scriptDiagnosticReported.emit(diagnostic);
|
|
1549
|
+
if (result.error) scriptFailed.emit({
|
|
1550
|
+
error: result.error,
|
|
1551
|
+
source
|
|
1552
|
+
});
|
|
1553
|
+
};
|
|
1554
|
+
/** A K/V/C/F commit surfaces as up to two results: the boot phase (only
|
|
1555
|
+
* when it produced effects) and the user phase (always — it carries the
|
|
1556
|
+
* diagnostics and the error). */
|
|
1557
|
+
const surfaceScriptCommit = (commit, context) => {
|
|
1558
|
+
for (const phase of ["boot", "user"]) {
|
|
1559
|
+
const uiEffects = commit.uiEffects.filter((effect) => effect.phase === phase);
|
|
1560
|
+
if (uiEffects.length === 0 && phase === "boot") continue;
|
|
1561
|
+
surfaceScriptResult({
|
|
1562
|
+
uiEffects,
|
|
1563
|
+
diagnostics: phase === "user" ? commit.diagnostics : [],
|
|
1564
|
+
...phase === "user" && commit.error ? { error: commit.error } : {},
|
|
1565
|
+
origin: context.origin,
|
|
1566
|
+
phase,
|
|
1567
|
+
realm: context.realm,
|
|
1568
|
+
...context.source ? { source: context.source } : {}
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
return {
|
|
1573
|
+
surfaceScriptResult,
|
|
1574
|
+
api: {
|
|
1575
|
+
surfaceScriptResult,
|
|
1576
|
+
surfaceScriptCommit
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
//#endregion
|
|
1581
|
+
//#region src/services/authority.ts
|
|
1582
|
+
function createAuthority(ctx) {
|
|
1583
|
+
const allowsPrint = () => ctx.allows("doc.print");
|
|
1584
|
+
return { allowsPrint };
|
|
1585
|
+
}
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region src/services/catalog.ts
|
|
1588
|
+
/** The ISO 32000-2 Table 200 key for each verb-shaped document trigger event. */
|
|
1589
|
+
const DOC_EVENT_TREES = {
|
|
1590
|
+
"will-save": "willSave",
|
|
1591
|
+
"did-save": "didSave",
|
|
1592
|
+
"will-print": "willPrint",
|
|
1593
|
+
"did-print": "didPrint",
|
|
1594
|
+
"will-close": "willClose"
|
|
1595
|
+
};
|
|
1596
|
+
function createCatalog(ctx) {
|
|
1597
|
+
let snapshotRead = null;
|
|
1598
|
+
const readDocumentActions = () => {
|
|
1599
|
+
if (!snapshotRead) {
|
|
1600
|
+
const service = ctx.doc.actions;
|
|
1601
|
+
const memoized = (service ? Promise.resolve(service.get()) : Promise.resolve(null)).catch((error) => {
|
|
1602
|
+
if (snapshotRead === memoized) snapshotRead = null;
|
|
1603
|
+
throw error;
|
|
1604
|
+
});
|
|
1605
|
+
snapshotRead = memoized;
|
|
1606
|
+
}
|
|
1607
|
+
return snapshotRead;
|
|
1608
|
+
};
|
|
1609
|
+
return {
|
|
1610
|
+
readDocumentActions,
|
|
1611
|
+
DOC_EVENT_TREES
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
//#endregion
|
|
1615
|
+
//#region src/services/document-print.ts
|
|
1616
|
+
/**
|
|
1617
|
+
* The kernel's print, where the document's own print requests go (the Named Print verb, a
|
|
1618
|
+
* script's `print()`) when the host gave the kernel a printer. The kernel prints the
|
|
1619
|
+
* document's pages, and runs the document's /WP and /DP around the dialog through the wrap
|
|
1620
|
+
* this plugin registers (`ctx.aroundPrint`). Without a printer, requests fall back to the UI
|
|
1621
|
+
* adapter's `print`.
|
|
1622
|
+
*/
|
|
1623
|
+
function createDocumentPrint(ctx) {
|
|
1624
|
+
const documents = () => ctx.get(_embedpdf_core.DocumentsToken);
|
|
1625
|
+
return {
|
|
1626
|
+
/** Whether the kernel prints this document: `doc.print`, and the host gave a printer. */
|
|
1627
|
+
available: () => documents().canPrint(),
|
|
1628
|
+
/** Print the document through the kernel. */
|
|
1629
|
+
start: () => documents().print()
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
//#endregion
|
|
1633
|
+
//#region src/services/events.ts
|
|
1634
|
+
function createEvents(ctx) {
|
|
1635
|
+
const executed = ctx.events.source();
|
|
1636
|
+
const diagnosticReported = ctx.events.source();
|
|
1637
|
+
return {
|
|
1638
|
+
executed,
|
|
1639
|
+
diagnosticReported,
|
|
1640
|
+
scriptDiagnosticReported: ctx.events.source(),
|
|
1641
|
+
scriptFailed: ctx.events.source(),
|
|
1642
|
+
openSequenceCompleted: ctx.events.source(),
|
|
1643
|
+
/** Fire `onDiagnosticReported` with the action and source it concerns (`null` for none). */
|
|
1644
|
+
reportDiagnostic(diagnostic, place = {}) {
|
|
1645
|
+
diagnosticReported.emit({
|
|
1646
|
+
code: diagnostic.code,
|
|
1647
|
+
action: place.action ?? null,
|
|
1648
|
+
source: place.source ?? null
|
|
1130
1649
|
});
|
|
1131
1650
|
}
|
|
1132
1651
|
};
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1652
|
+
}
|
|
1653
|
+
//#endregion
|
|
1654
|
+
//#region src/services/form.ts
|
|
1655
|
+
function createForm(ctx, ports) {
|
|
1656
|
+
return { read: (read = (task) => task) => {
|
|
1657
|
+
const shown = ports.formReader?.();
|
|
1658
|
+
return shown ? Promise.resolve(shown) : read(ctx.doc.forms.list());
|
|
1659
|
+
} };
|
|
1660
|
+
}
|
|
1661
|
+
//#endregion
|
|
1662
|
+
//#region src/services/ports.ts
|
|
1663
|
+
function createPorts({ reportDiagnostic }) {
|
|
1664
|
+
const ports = {
|
|
1665
|
+
executors: /* @__PURE__ */ new Map(),
|
|
1666
|
+
annotCommitSink: null,
|
|
1667
|
+
formCommitSink: null,
|
|
1668
|
+
formReader: null,
|
|
1669
|
+
uiAdapter: null,
|
|
1670
|
+
submitHandler: null,
|
|
1671
|
+
submitResolver: null
|
|
1672
|
+
};
|
|
1673
|
+
return {
|
|
1674
|
+
slots: ports,
|
|
1675
|
+
api: {
|
|
1676
|
+
registerExecutor: (type, executor) => {
|
|
1677
|
+
if (ports.executors.has(type)) reportDiagnostic({
|
|
1678
|
+
code: "duplicate-executor",
|
|
1679
|
+
message: `executor for '${type}' replaced (last-wins)`
|
|
1680
|
+
}, { action: type });
|
|
1681
|
+
ports.executors.set(type, executor);
|
|
1682
|
+
return () => {
|
|
1683
|
+
if (ports.executors.get(type) === executor) ports.executors.delete(type);
|
|
1684
|
+
};
|
|
1685
|
+
},
|
|
1686
|
+
registerAnnotCommitSink: (sink) => {
|
|
1687
|
+
ports.annotCommitSink = sink;
|
|
1688
|
+
return () => {
|
|
1689
|
+
if (ports.annotCommitSink === sink) ports.annotCommitSink = null;
|
|
1690
|
+
};
|
|
1691
|
+
},
|
|
1692
|
+
registerFormCommitSink: (sink) => {
|
|
1693
|
+
ports.formCommitSink = sink;
|
|
1694
|
+
return () => {
|
|
1695
|
+
if (ports.formCommitSink === sink) ports.formCommitSink = null;
|
|
1696
|
+
};
|
|
1697
|
+
},
|
|
1698
|
+
registerFormReader: (reader) => {
|
|
1699
|
+
ports.formReader = reader;
|
|
1700
|
+
return () => {
|
|
1701
|
+
if (ports.formReader === reader) ports.formReader = null;
|
|
1702
|
+
};
|
|
1703
|
+
},
|
|
1704
|
+
setSubmitHandler: (handler) => {
|
|
1705
|
+
ports.submitHandler = handler;
|
|
1706
|
+
return () => {
|
|
1707
|
+
if (ports.submitHandler === handler) ports.submitHandler = null;
|
|
1708
|
+
};
|
|
1709
|
+
},
|
|
1710
|
+
registerSubmitResolver: (resolver) => {
|
|
1711
|
+
ports.submitResolver = resolver;
|
|
1712
|
+
return () => {
|
|
1713
|
+
if (ports.submitResolver === resolver) ports.submitResolver = null;
|
|
1159
1714
|
};
|
|
1160
|
-
steps.push({
|
|
1161
|
-
source: { kind: "document" },
|
|
1162
|
-
tree,
|
|
1163
|
-
result: await runAndEmit(tree, lifecycleCtx)
|
|
1164
|
-
});
|
|
1165
1715
|
}
|
|
1166
|
-
if (snapshot?.openAction?.root || snapshot?.openAction?.incomplete) steps.push({
|
|
1167
|
-
source: { kind: "document" },
|
|
1168
|
-
tree: snapshot.openAction,
|
|
1169
|
-
result: await runAndEmit(snapshot.openAction, lifecycleCtx)
|
|
1170
|
-
});
|
|
1171
|
-
} catch (error) {
|
|
1172
|
-
const diagnostic = {
|
|
1173
|
-
code: "trigger-failed",
|
|
1174
|
-
message: `open sequence failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1175
|
-
};
|
|
1176
|
-
diagnostics.push(diagnostic);
|
|
1177
|
-
diagnosticHook.emit(diagnostic);
|
|
1178
|
-
} finally {
|
|
1179
|
-
releaseBarrier(true);
|
|
1180
1716
|
}
|
|
1181
|
-
return foldSteps(steps, diagnostics);
|
|
1182
1717
|
};
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1718
|
+
}
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/services/print-latch.ts
|
|
1721
|
+
const createPrintLatch = () => ({ active: false });
|
|
1722
|
+
//#endregion
|
|
1723
|
+
//#region src/services/queue.ts
|
|
1724
|
+
function createQueue(ctx) {
|
|
1725
|
+
return {
|
|
1726
|
+
enqueue: ctx.serialQueue("actions"),
|
|
1727
|
+
budget: { scriptNodes: 0 }
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
//#endregion
|
|
1731
|
+
//#region src/services/index.ts
|
|
1732
|
+
function createServices(ctx) {
|
|
1733
|
+
const events = createEvents(ctx);
|
|
1734
|
+
const ports = createPorts(events);
|
|
1735
|
+
return {
|
|
1736
|
+
settings: ctx.settings(),
|
|
1737
|
+
events,
|
|
1738
|
+
policy: createPolicy(ctx),
|
|
1739
|
+
ports,
|
|
1740
|
+
queue: createQueue(ctx),
|
|
1741
|
+
catalog: createCatalog(ctx),
|
|
1742
|
+
form: createForm(ctx, ports.slots),
|
|
1743
|
+
authority: createAuthority(ctx),
|
|
1744
|
+
documentPrint: createDocumentPrint(ctx),
|
|
1745
|
+
printLatch: createPrintLatch()
|
|
1194
1746
|
};
|
|
1747
|
+
}
|
|
1748
|
+
//#endregion
|
|
1749
|
+
//#region src/submit/perform.ts
|
|
1750
|
+
function createSubmit(ctx, services, environment) {
|
|
1751
|
+
const policy = services.policy;
|
|
1752
|
+
const ports = services.ports.slots;
|
|
1753
|
+
const { reportDiagnostic } = services.events;
|
|
1754
|
+
const nowMs = () => environment.now?.() ?? Date.now();
|
|
1195
1755
|
/**
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1756
|
+
* The one submit door. Sink order: the embedder handler (explicit beats
|
|
1757
|
+
* ambient; its promise is not awaited) → the document's home
|
|
1758
|
+
* (`doc.forms.submit`, engine-asserted and awaited: a real operation with a
|
|
1759
|
+
* real receipt) → blocked with `no-submit-sink`.
|
|
1200
1760
|
*/
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
return {
|
|
1208
|
-
source: { kind: "document" },
|
|
1209
|
-
tree,
|
|
1210
|
-
result: await runAndEmit(tree, {
|
|
1211
|
-
origin: "lifecycle",
|
|
1212
|
-
source: { kind: "document" },
|
|
1213
|
-
event: {
|
|
1214
|
-
scope: "document",
|
|
1215
|
-
name: event
|
|
1216
|
-
}
|
|
1217
|
-
})
|
|
1218
|
-
};
|
|
1219
|
-
} catch (error) {
|
|
1220
|
-
diagnose({
|
|
1221
|
-
code: "trigger-failed",
|
|
1222
|
-
message: `${event}: ${error instanceof Error ? error.message : String(error)}`
|
|
1761
|
+
const performSubmit = async (intent, actionContext, diagnose) => {
|
|
1762
|
+
const decision = policy.current()["submit-form"][actionContext.origin];
|
|
1763
|
+
if (decision !== "adapter" && decision !== "allow") {
|
|
1764
|
+
if (decision === "report") diagnose({
|
|
1765
|
+
code: "blocked",
|
|
1766
|
+
message: `submit-form: policy 'report' for origin '${actionContext.origin}'`
|
|
1223
1767
|
});
|
|
1224
|
-
return
|
|
1768
|
+
return { status: "blocked" };
|
|
1225
1769
|
}
|
|
1226
|
-
|
|
1227
|
-
/** The dispatch-path body for a verb-shaped document event. */
|
|
1228
|
-
const runDocumentEventOp = async (event, diagnostics, diagnose) => {
|
|
1229
|
-
const step = await runDocEventTreeSafe(event, diagnose);
|
|
1230
|
-
return foldSteps(step ? [step] : [], diagnostics);
|
|
1231
|
-
};
|
|
1232
|
-
/**
|
|
1233
|
-
* D3: BOTH adapter print invocations (the Named Print verb; script
|
|
1234
|
-
* `doc.print()` effects from the actions plane) go through here — WP →
|
|
1235
|
-
* `uiAdapter.print` exactly once → DP, latch reset in `finally`. While
|
|
1236
|
-
* the latch is held (including `runDocumentVerb('print')`'s body) a
|
|
1237
|
-
* nested request is suppressed with `reentrant-print`. An adapter throw
|
|
1238
|
-
* skips DP (the latch still resets) — named deviation: DP otherwise
|
|
1239
|
-
* fires when the adapter call RETURNS, since a browser cannot observe
|
|
1240
|
-
* dialog completion.
|
|
1241
|
-
*/
|
|
1242
|
-
const firePrintThroughAdapter = async (uiContext, diagnose) => {
|
|
1243
|
-
if (docPrintEventActive) {
|
|
1770
|
+
if (!ports.submitResolver) {
|
|
1244
1771
|
diagnose({
|
|
1245
|
-
code: "
|
|
1246
|
-
message: "
|
|
1772
|
+
code: "no-submit-resolver",
|
|
1773
|
+
message: "submit-form: no dataset resolver registered (form plugin missing?)"
|
|
1247
1774
|
});
|
|
1248
1775
|
return {
|
|
1249
1776
|
status: "blocked",
|
|
1250
|
-
detail: "
|
|
1777
|
+
detail: "no dataset resolver"
|
|
1251
1778
|
};
|
|
1252
1779
|
}
|
|
1253
|
-
|
|
1780
|
+
let request;
|
|
1781
|
+
try {
|
|
1782
|
+
request = await ports.submitResolver(intent, actionContext, diagnose);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1254
1785
|
diagnose({
|
|
1255
|
-
code: "
|
|
1256
|
-
message:
|
|
1786
|
+
code: "executor-failed",
|
|
1787
|
+
message: `submit-form: dataset resolution: ${detail}`
|
|
1257
1788
|
});
|
|
1258
1789
|
return {
|
|
1259
|
-
status: "
|
|
1260
|
-
detail
|
|
1790
|
+
status: "failed",
|
|
1791
|
+
detail
|
|
1261
1792
|
};
|
|
1262
1793
|
}
|
|
1263
|
-
|
|
1794
|
+
const forms = ctx.doc.forms;
|
|
1795
|
+
const homeSubmit = typeof forms.submit === "function" ? () => Promise.resolve(forms.submit(toFormSubmissionRequest(request, new Date(nowMs()).toISOString()))) : null;
|
|
1796
|
+
if (ports.submitHandler) try {
|
|
1797
|
+
const outcome = ports.submitHandler(request, { submitToDocumentHome: homeSubmit });
|
|
1798
|
+
if (outcome && typeof outcome.then === "function") outcome.catch((error) => {
|
|
1799
|
+
reportDiagnostic({
|
|
1800
|
+
code: "executor-failed",
|
|
1801
|
+
message: `submit handler rejected (detached): ${error instanceof Error ? error.message : String(error)}`
|
|
1802
|
+
}, {
|
|
1803
|
+
action: "submit-form",
|
|
1804
|
+
source: actionContext.source
|
|
1805
|
+
});
|
|
1806
|
+
});
|
|
1807
|
+
return { status: "executed" };
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1264
1810
|
diagnose({
|
|
1265
|
-
code: "
|
|
1266
|
-
message:
|
|
1811
|
+
code: "executor-failed",
|
|
1812
|
+
message: `submit handler threw: ${detail}`
|
|
1267
1813
|
});
|
|
1268
1814
|
return {
|
|
1269
|
-
status: "
|
|
1270
|
-
detail
|
|
1815
|
+
status: "failed",
|
|
1816
|
+
detail
|
|
1271
1817
|
};
|
|
1272
1818
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
await runDocEventTreeSafe("will-print", diagnose);
|
|
1276
|
-
const adapter = uiAdapter;
|
|
1277
|
-
if (!adapter) return {
|
|
1278
|
-
status: "no-executor",
|
|
1279
|
-
detail: "UI adapter uninstalled mid-print"
|
|
1280
|
-
};
|
|
1281
|
-
if (uiContext) adapter.print(uiContext);
|
|
1282
|
-
else adapter.print();
|
|
1283
|
-
await runDocEventTreeSafe("did-print", diagnose);
|
|
1819
|
+
if (homeSubmit) try {
|
|
1820
|
+
await homeSubmit();
|
|
1284
1821
|
return { status: "executed" };
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1824
|
+
diagnose({
|
|
1825
|
+
code: "executor-failed",
|
|
1826
|
+
message: `document home refused submit: ${detail}`
|
|
1827
|
+
});
|
|
1828
|
+
return {
|
|
1829
|
+
status: "failed",
|
|
1830
|
+
detail
|
|
1831
|
+
};
|
|
1295
1832
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1833
|
+
diagnose({
|
|
1834
|
+
code: "no-submit-sink",
|
|
1835
|
+
message: "submit-form: no handler installed and the document has no submit-capable home"
|
|
1836
|
+
});
|
|
1837
|
+
return {
|
|
1838
|
+
status: "blocked",
|
|
1839
|
+
detail: "no submit sink"
|
|
1840
|
+
};
|
|
1299
1841
|
};
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1842
|
+
return { performSubmit };
|
|
1843
|
+
}
|
|
1844
|
+
//#endregion
|
|
1845
|
+
//#region src/controller.ts
|
|
1846
|
+
/**
|
|
1847
|
+
* The actions controller: the composition root. It builds the plugin's
|
|
1848
|
+
* services once, wires each area with the services it declares, and
|
|
1849
|
+
* assembles the host capability from the areas' API slices. No behavior
|
|
1850
|
+
* lives here: every verb and read has a home in `submit/`, `scripting/`,
|
|
1851
|
+
* `dispatch/` or `lifecycle/`. The plugin keeps no session state: its rules
|
|
1852
|
+
* are its settings, and its live data (ports, queue, latches) are resources
|
|
1853
|
+
* of the areas.
|
|
1854
|
+
*/
|
|
1855
|
+
/**
|
|
1856
|
+
* `config` is what the app registered; the settings in it reach the
|
|
1857
|
+
* controller through `ctx.settings()`, and only the script environment is
|
|
1858
|
+
* read from it here.
|
|
1859
|
+
*/
|
|
1860
|
+
function createActionsController(ctx, config = {}) {
|
|
1861
|
+
const services = createServices(ctx);
|
|
1862
|
+
const { events, ports, settings } = services;
|
|
1863
|
+
const core = {
|
|
1864
|
+
dispatch: (trigger) => dispatcher.dispatch(trigger),
|
|
1865
|
+
runAndEmit: (tree, actionContext) => dispatcher.runAndEmit(tree, actionContext)
|
|
1306
1866
|
};
|
|
1307
|
-
|
|
1867
|
+
const environment = config.javascript ?? {};
|
|
1868
|
+
const submit = createSubmit(ctx, services, environment);
|
|
1869
|
+
const realm = createRealm(ctx, services, environment);
|
|
1870
|
+
const surface = createScriptSurface(services, submit);
|
|
1871
|
+
const pageLifecycle = createPageLifecycle(ctx, services, core);
|
|
1872
|
+
const openSequence = createOpenSequence(services, pageLifecycle, core);
|
|
1873
|
+
const documentEvents = createDocumentEvents(services, openSequence, core);
|
|
1874
|
+
const triggers = createTriggers(ctx, services);
|
|
1875
|
+
const dispatcher = createDispatcher(ctx, services, createRunner(ctx, services, submit, documentEvents), triggers, openSequence, documentEvents);
|
|
1876
|
+
registerScriptExecutor(ctx, services, environment, realm, submit, surface, documentEvents);
|
|
1308
1877
|
return {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const after = verb === "save" ? "did-save" : "did-print";
|
|
1325
|
-
const body = async () => {
|
|
1326
|
-
await runDocEventTreeSafe(before, diagnose);
|
|
1327
|
-
const value = await operation();
|
|
1328
|
-
await runDocEventTreeSafe(after, diagnose);
|
|
1329
|
-
return value;
|
|
1330
|
-
};
|
|
1331
|
-
if (verb !== "print") return body();
|
|
1332
|
-
docPrintEventActive = true;
|
|
1333
|
-
try {
|
|
1334
|
-
return await body();
|
|
1335
|
-
} finally {
|
|
1336
|
-
docPrintEventActive = false;
|
|
1337
|
-
}
|
|
1338
|
-
}),
|
|
1339
|
-
prepareClose: () => dispatch({
|
|
1340
|
-
scope: "document",
|
|
1341
|
-
event: "will-close"
|
|
1342
|
-
}),
|
|
1343
|
-
setSubmitHandler: (handler) => {
|
|
1344
|
-
submitHandler = handler;
|
|
1345
|
-
return () => {
|
|
1346
|
-
if (submitHandler === handler) submitHandler = null;
|
|
1347
|
-
};
|
|
1348
|
-
},
|
|
1349
|
-
onAction: actionHook.on,
|
|
1350
|
-
onDiagnostic: diagnosticHook.on,
|
|
1351
|
-
onScriptDiagnostic: scriptDiagnosticHook.on,
|
|
1352
|
-
onScriptError: scriptErrorHook.on,
|
|
1353
|
-
registerExecutor: (type, executor) => {
|
|
1354
|
-
if (executors.has(type)) diagnosticHook.emit({
|
|
1355
|
-
code: "duplicate-executor",
|
|
1356
|
-
message: `executor for '${type}' replaced (last-wins)`
|
|
1357
|
-
});
|
|
1358
|
-
executors.set(type, executor);
|
|
1359
|
-
return () => {
|
|
1360
|
-
if (executors.get(type) === executor) executors.delete(type);
|
|
1361
|
-
};
|
|
1362
|
-
},
|
|
1363
|
-
registerAnnotCommitSink: (sink) => {
|
|
1364
|
-
annotCommitSink = sink;
|
|
1365
|
-
return () => {
|
|
1366
|
-
if (annotCommitSink === sink) annotCommitSink = null;
|
|
1367
|
-
};
|
|
1368
|
-
},
|
|
1369
|
-
registerFormCommitSink: (sink) => {
|
|
1370
|
-
formCommitSink = sink;
|
|
1371
|
-
return () => {
|
|
1372
|
-
if (formCommitSink === sink) formCommitSink = null;
|
|
1373
|
-
};
|
|
1374
|
-
},
|
|
1375
|
-
...scriptHost && realms ? {
|
|
1376
|
-
scriptRealm: {
|
|
1377
|
-
transaction: (body) => scriptHost.transaction(body),
|
|
1378
|
-
budget: realms.budget
|
|
1379
|
-
},
|
|
1380
|
-
createDetachedScriptRealm: (target) => {
|
|
1381
|
-
const host = realms.realmFor(target);
|
|
1382
|
-
return {
|
|
1383
|
-
transaction: (body) => host.transaction(body),
|
|
1384
|
-
budget: realms.budget,
|
|
1385
|
-
dispose: () => host.dispose()
|
|
1386
|
-
};
|
|
1878
|
+
api: (0, _embedpdf_core.composeApi)("actions", [
|
|
1879
|
+
settings.api,
|
|
1880
|
+
ports.api,
|
|
1881
|
+
realm.api,
|
|
1882
|
+
surface.api,
|
|
1883
|
+
pageLifecycle.api,
|
|
1884
|
+
openSequence.api,
|
|
1885
|
+
documentEvents.api,
|
|
1886
|
+
dispatcher.api,
|
|
1887
|
+
{
|
|
1888
|
+
onExecuted: events.executed.on,
|
|
1889
|
+
onDiagnosticReported: events.diagnosticReported.on,
|
|
1890
|
+
onScriptDiagnosticReported: events.scriptDiagnosticReported.on,
|
|
1891
|
+
onScriptFailed: events.scriptFailed.on,
|
|
1892
|
+
onOpenSequenceCompleted: events.openSequenceCompleted.on
|
|
1387
1893
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
return () => {
|
|
1395
|
-
if (submitResolver === resolver) submitResolver = null;
|
|
1396
|
-
};
|
|
1894
|
+
]),
|
|
1895
|
+
connect() {
|
|
1896
|
+
ctx.listen(ctx.doc.events, (event) => triggers.invalidate(event));
|
|
1897
|
+
openSequence.arm();
|
|
1898
|
+
ctx.aroundDownload((read) => documentEvents.api.runDocumentVerb("save", read));
|
|
1899
|
+
ctx.aroundPrint((print) => documentEvents.api.runDocumentVerb("print", print));
|
|
1397
1900
|
}
|
|
1398
1901
|
};
|
|
1399
1902
|
}
|
|
1400
1903
|
//#endregion
|
|
1401
|
-
//#region src/
|
|
1402
|
-
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1904
|
+
//#region src/settings.ts
|
|
1905
|
+
/** `config` without the script environment, which is no setting: what the settings register. */
|
|
1906
|
+
function registeredSettings(config) {
|
|
1907
|
+
if (!config) return void 0;
|
|
1908
|
+
const { javascript } = config;
|
|
1909
|
+
return {
|
|
1910
|
+
policy: config.policy,
|
|
1911
|
+
triggers: config.triggers,
|
|
1912
|
+
openSequence: config.openSequence,
|
|
1913
|
+
javascript: javascript && {
|
|
1914
|
+
enabled: javascript.enabled,
|
|
1915
|
+
identity: javascript.identity
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1408
1918
|
}
|
|
1409
1919
|
//#endregion
|
|
1410
1920
|
//#region src/actions.plugin.ts
|
|
1411
1921
|
/**
|
|
1412
|
-
* The action engine
|
|
1922
|
+
* The action engine, the dependency root of the action architecture. It
|
|
1413
1923
|
* interprets extracted /A and /AA trees; it never detects triggers and never
|
|
1414
|
-
* imports another plugin's token
|
|
1415
|
-
* optionally depend on
|
|
1416
|
-
*
|
|
1924
|
+
* imports another plugin's token: the stage, annotation, link and form
|
|
1925
|
+
* plugins optionally depend on it and register their executors and sinks
|
|
1926
|
+
* when they connect (the kernel's dependency order creates this plugin
|
|
1417
1927
|
* first). JavaScript is one registered interpreter among many: Hide,
|
|
1418
|
-
* ResetForm, GoTo
|
|
1928
|
+
* ResetForm, GoTo and Named work with scripting off. Document-scoped and
|
|
1929
|
+
* stateless; its settings are what the app registered over
|
|
1930
|
+
* {@link ACTIONS_DEFAULTS}.
|
|
1419
1931
|
*/
|
|
1420
1932
|
const actionsPlugin = (config) => (0, _embedpdf_core.definePlugin)({
|
|
1421
1933
|
id: "actions",
|
|
1422
|
-
token:
|
|
1934
|
+
token: require_contract_host.ActionsToken,
|
|
1423
1935
|
scope: "document",
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1936
|
+
settings: {
|
|
1937
|
+
defaults: require_contract.ACTIONS_DEFAULTS,
|
|
1938
|
+
registered: registeredSettings(config)
|
|
1939
|
+
},
|
|
1940
|
+
create: (ctx) => createActionsController(ctx, config)
|
|
1427
1941
|
});
|
|
1428
1942
|
//#endregion
|
|
1429
|
-
exports.
|
|
1943
|
+
exports.ACTIONS_DEFAULTS = require_contract.ACTIONS_DEFAULTS;
|
|
1944
|
+
exports.ActionsToken = require_contract.ActionsToken;
|
|
1430
1945
|
exports.actionsPlugin = actionsPlugin;
|
|
1431
1946
|
exports.createHoverPump = require_contract.createHoverPump;
|
|
1432
|
-
exports.
|
|
1947
|
+
exports.eventOf = require_contract.eventOf;
|
|
1433
1948
|
exports.submitEntriesToUrlEncoded = require_contract.submitEntriesToUrlEncoded;
|
|
1949
|
+
exports.triggerOriginOf = require_contract.triggerOriginOf;
|
|
1434
1950
|
|
|
1435
1951
|
//# sourceMappingURL=index.cjs.map
|