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