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