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