@lofcz/embedpdf-plugin-actions 3.0.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +192 -0
- package/dist/contract/host.cjs +7 -0
- package/dist/contract/host.cjs.map +1 -0
- package/dist/contract/host.d.cts +7 -0
- package/dist/contract/host.d.ts +7 -0
- package/dist/contract/host.js +7 -0
- package/dist/contract/host.js.map +1 -0
- package/dist/contract-C1K3H4RP.cjs +89 -0
- package/dist/contract-C1K3H4RP.cjs.map +1 -0
- package/dist/contract-Cl-f3d61.d.ts +50 -0
- package/dist/contract-DAem-EST.js +78 -0
- package/dist/contract-DAem-EST.js.map +1 -0
- package/dist/contract-KaJQWh67.d.cts +50 -0
- package/dist/contract.cjs +7 -0
- package/dist/contract.d.cts +3 -0
- package/dist/contract.d.ts +3 -0
- package/dist/contract.js +3 -0
- package/dist/index.cjs +1385 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1380 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +3 -0
- package/dist/internal.d.cts +3 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +2 -0
- package/dist/types-BXJLAKXi.cjs +52 -0
- package/dist/types-BXJLAKXi.cjs.map +1 -0
- package/dist/types-DRQReAuO.d.cts +447 -0
- package/dist/types-DRQReAuO.d.ts +447 -0
- package/dist/types-DgBMy5nE.js +35 -0
- package/dist/types-DgBMy5nE.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
import { n as eventOf, r as originOf, t as ActionsToken } from "./types-DgBMy5nE.js";
|
|
2
|
+
import { n as createHoverPump, t as submitEntriesToUrlEncoded } from "./contract-DAem-EST.js";
|
|
3
|
+
import { DocumentsToken, createEventHook, createSerialQueue, definePlugin } from "@embedpdf/core";
|
|
4
|
+
import { createScriptHost, javaScriptProgramFromActionTree, resolveScriptIdentity, scriptFieldsFromSnapshot, seedFrom } from "@embedpdf/core-acrojs";
|
|
5
|
+
//#region src/capability.ts
|
|
6
|
+
const ALLOW_ALL = {
|
|
7
|
+
user: "allow",
|
|
8
|
+
hover: "allow",
|
|
9
|
+
lifecycle: "allow"
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_POLICY = {
|
|
12
|
+
goto: ALLOW_ALL,
|
|
13
|
+
named: ALLOW_ALL,
|
|
14
|
+
hide: ALLOW_ALL,
|
|
15
|
+
"reset-form": ALLOW_ALL,
|
|
16
|
+
javascript: ALLOW_ALL,
|
|
17
|
+
uri: {
|
|
18
|
+
user: "adapter",
|
|
19
|
+
hover: "report",
|
|
20
|
+
lifecycle: "report"
|
|
21
|
+
},
|
|
22
|
+
print: {
|
|
23
|
+
user: "adapter",
|
|
24
|
+
hover: "block",
|
|
25
|
+
lifecycle: "block"
|
|
26
|
+
},
|
|
27
|
+
"submit-form": {
|
|
28
|
+
user: "adapter",
|
|
29
|
+
hover: "block",
|
|
30
|
+
lifecycle: "block"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Never executable, not configurable — diagnostics only. */
|
|
34
|
+
const NEVER_TYPES = /* @__PURE__ */ new Set([
|
|
35
|
+
"launch",
|
|
36
|
+
"goto-remote",
|
|
37
|
+
"goto-embedded",
|
|
38
|
+
"sound",
|
|
39
|
+
"movie",
|
|
40
|
+
"import-data"
|
|
41
|
+
]);
|
|
42
|
+
/** Recognized types with no Phase-1 interpreter. */
|
|
43
|
+
const UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
44
|
+
"rendition",
|
|
45
|
+
"thread",
|
|
46
|
+
"set-ocg-state",
|
|
47
|
+
"transition",
|
|
48
|
+
"goto-3d-view",
|
|
49
|
+
"unknown"
|
|
50
|
+
]);
|
|
51
|
+
/** Document-lifetime node types: engine mutations, committed in walk order. */
|
|
52
|
+
const DOCUMENT_TYPES = /* @__PURE__ */ new Set(["reset-form", "javascript"]);
|
|
53
|
+
const sameRef = (left, right) => {
|
|
54
|
+
if (left.kind === "objectNumber" && right.kind === "objectNumber") return left.annotObjectNumber === right.annotObjectNumber;
|
|
55
|
+
if (left.kind === "nm" && right.kind === "nm") return left.pageObjectNumber === right.pageObjectNumber && left.nm === right.nm;
|
|
56
|
+
if (left.kind === "index" && right.kind === "index") return left.pageObjectNumber === right.pageObjectNumber && left.index === right.index;
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
const isPrintVerb = (node) => node.type === "named" && node.name === "Print";
|
|
60
|
+
function createActionsCapability(ctx, config = {}) {
|
|
61
|
+
const policy = {
|
|
62
|
+
...DEFAULT_POLICY,
|
|
63
|
+
...config.policy
|
|
64
|
+
};
|
|
65
|
+
const enqueue = createSerialQueue();
|
|
66
|
+
const executors = /* @__PURE__ */ new Map();
|
|
67
|
+
let annotCommitSink = null;
|
|
68
|
+
let formCommitSink = null;
|
|
69
|
+
let uiAdapter = null;
|
|
70
|
+
/** Sink 1 of the submit chain (consent = installation). */
|
|
71
|
+
let submitHandler = null;
|
|
72
|
+
/** The form plugin's dataset resolver (D7's one door). */
|
|
73
|
+
let submitResolver = null;
|
|
74
|
+
/** D3's document-print latch: while a print event wrapper is active, a
|
|
75
|
+
* nested print request is SUPPRESSED (`reentrant-print`) — the adapter
|
|
76
|
+
* opens exactly one dialog per outer request. */
|
|
77
|
+
let docPrintEventActive = false;
|
|
78
|
+
/** D11's deterministic aggregate: JS nodes run in the CURRENT dispatch. */
|
|
79
|
+
let jsNodesThisDispatch = 0;
|
|
80
|
+
const actionHook = createEventHook((error) => globalThis.console?.error("[actions] onAction observer failed:", error));
|
|
81
|
+
const diagnosticHook = createEventHook((error) => globalThis.console?.error("[actions] onDiagnostic observer failed:", error));
|
|
82
|
+
const scriptDiagnosticHook = createEventHook((error) => globalThis.console?.error("[actions] onScriptDiagnostic observer failed:", error));
|
|
83
|
+
const scriptErrorHook = createEventHook((error) => globalThis.console?.error("[actions] onScriptError observer failed:", error));
|
|
84
|
+
ctx.cleanup(() => {
|
|
85
|
+
actionHook.dispose();
|
|
86
|
+
diagnosticHook.dispose();
|
|
87
|
+
scriptDiagnosticHook.dispose();
|
|
88
|
+
scriptErrorHook.dispose();
|
|
89
|
+
});
|
|
90
|
+
/** Policy lookup — `null` means "recognized, no interpreter" (inert). */
|
|
91
|
+
const decisionFor = (node, origin) => {
|
|
92
|
+
if (NEVER_TYPES.has(node.type)) return "never";
|
|
93
|
+
if (UNSUPPORTED_TYPES.has(node.type)) return "unsupported";
|
|
94
|
+
if (isPrintVerb(node)) return policy.print[origin];
|
|
95
|
+
const row = policy[node.type];
|
|
96
|
+
return row ? row[origin] : "unsupported";
|
|
97
|
+
};
|
|
98
|
+
const allowsPrint = () => ctx.tryGet(DocumentsToken)?.allows("doc.print", ctx.documentId ?? void 0) ?? true;
|
|
99
|
+
let actionsSnapshotPromise = null;
|
|
100
|
+
const readDocumentActions = () => {
|
|
101
|
+
if (!actionsSnapshotPromise) {
|
|
102
|
+
const doc = ctx.doc;
|
|
103
|
+
const memo = (doc?.actions ? Promise.resolve(doc.actions.read()) : Promise.resolve(null)).catch((error) => {
|
|
104
|
+
if (actionsSnapshotPromise === memo) actionsSnapshotPromise = null;
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
actionsSnapshotPromise = memo;
|
|
108
|
+
}
|
|
109
|
+
return actionsSnapshotPromise;
|
|
110
|
+
};
|
|
111
|
+
/** The Table-200 key for each verb-shaped document trigger event. */
|
|
112
|
+
const DOC_EVENT_TREES = {
|
|
113
|
+
"will-save": "willSave",
|
|
114
|
+
"did-save": "didSave",
|
|
115
|
+
"will-print": "willPrint",
|
|
116
|
+
"did-print": "didPrint",
|
|
117
|
+
"will-close": "willClose"
|
|
118
|
+
};
|
|
119
|
+
const nowMs = () => js?.now?.() ?? Date.now();
|
|
120
|
+
const intentOfPayload = (payload) => ({
|
|
121
|
+
url: payload.url,
|
|
122
|
+
fields: payload.fields,
|
|
123
|
+
exclude: payload.flags.exclude,
|
|
124
|
+
includeNoValueFields: payload.flags.includeNoValueFields,
|
|
125
|
+
format: payload.flags.format,
|
|
126
|
+
method: payload.flags.method,
|
|
127
|
+
flagsRaw: payload.flags.raw,
|
|
128
|
+
...payload.charSet === void 0 ? {} : { charSet: payload.charSet }
|
|
129
|
+
});
|
|
130
|
+
const toFormSubmissionRequest = (request) => ({
|
|
131
|
+
entries: request.entries,
|
|
132
|
+
intent: {
|
|
133
|
+
url: request.url,
|
|
134
|
+
format: request.format,
|
|
135
|
+
method: request.method,
|
|
136
|
+
flagsRaw: request.flagsRaw,
|
|
137
|
+
...request.charSet === void 0 ? {} : { charSet: request.charSet }
|
|
138
|
+
},
|
|
139
|
+
origin: request.origin,
|
|
140
|
+
clientTimeMs: nowMs()
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* The ONE submit door — both sources land here after policy. Sink order:
|
|
144
|
+
* embedder handler (explicit beats ambient; detached-promise contract) →
|
|
145
|
+
* the document's home (`doc.forms.submit`, engine-asserted, AWAITED — a
|
|
146
|
+
* real op with a real receipt) → blocked with `no-submit-sink`. Never a
|
|
147
|
+
* network call from this plugin.
|
|
148
|
+
*/
|
|
149
|
+
const performSubmit = async (intent, actionCtx, diagnose) => {
|
|
150
|
+
const decision = policy["submit-form"][actionCtx.origin];
|
|
151
|
+
if (decision !== "adapter" && decision !== "allow") {
|
|
152
|
+
diagnose({
|
|
153
|
+
code: "blocked",
|
|
154
|
+
message: `submit-form: policy '${decision}' for origin '${actionCtx.origin}'`
|
|
155
|
+
});
|
|
156
|
+
return { status: "blocked" };
|
|
157
|
+
}
|
|
158
|
+
if (!submitResolver) {
|
|
159
|
+
diagnose({
|
|
160
|
+
code: "no-submit-resolver",
|
|
161
|
+
message: "submit-form: no dataset resolver registered (form plugin missing?)"
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
status: "blocked",
|
|
165
|
+
detail: "no dataset resolver"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
let request;
|
|
169
|
+
try {
|
|
170
|
+
request = await submitResolver(intent, actionCtx, diagnose);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
173
|
+
diagnose({
|
|
174
|
+
code: "executor-failed",
|
|
175
|
+
message: `submit-form: dataset resolution: ${detail}`
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
status: "failed",
|
|
179
|
+
detail
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const doc = ctx.doc;
|
|
183
|
+
const homeSubmit = doc && typeof doc.forms.submit === "function" ? () => Promise.resolve(doc.forms.submit(toFormSubmissionRequest(request))) : null;
|
|
184
|
+
if (submitHandler) try {
|
|
185
|
+
const outcome = submitHandler(request, { submitToDocumentHome: homeSubmit });
|
|
186
|
+
if (outcome && typeof outcome.then === "function") outcome.catch((error) => {
|
|
187
|
+
diagnosticHook.emit({
|
|
188
|
+
code: "executor-failed",
|
|
189
|
+
message: `submit handler rejected (detached): ${error instanceof Error ? error.message : String(error)}`
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
return { status: "executed" };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
195
|
+
diagnose({
|
|
196
|
+
code: "executor-failed",
|
|
197
|
+
message: `submit handler threw: ${detail}`
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
status: "failed",
|
|
201
|
+
detail
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (homeSubmit) try {
|
|
205
|
+
await homeSubmit();
|
|
206
|
+
return { status: "executed" };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
209
|
+
diagnose({
|
|
210
|
+
code: "executor-failed",
|
|
211
|
+
message: `document home refused submit: ${detail}`
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
status: "failed",
|
|
215
|
+
detail
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
diagnose({
|
|
219
|
+
code: "no-submit-sink",
|
|
220
|
+
message: "submit-form: no handler installed and the document has no submit-capable home"
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
status: "blocked",
|
|
224
|
+
detail: "no submit sink"
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
const js = config.javascript;
|
|
228
|
+
const scriptHost = js?.enabled && ctx.doc ? createScriptHost({
|
|
229
|
+
sandboxFactory: js.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox())),
|
|
230
|
+
document: () => {
|
|
231
|
+
const meta = ctx.document();
|
|
232
|
+
return {
|
|
233
|
+
id: ctx.documentId ?? "document",
|
|
234
|
+
fileName: js.fileName?.() ?? meta?.name ?? "document.pdf",
|
|
235
|
+
pageCount: meta?.pageCount ?? 0,
|
|
236
|
+
pageNumber: 0
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
identity: () => ctx.doc ? resolveScriptIdentity(ctx.doc, js.identity) : {
|
|
240
|
+
name: "",
|
|
241
|
+
loginName: "",
|
|
242
|
+
corporation: "",
|
|
243
|
+
email: ""
|
|
244
|
+
},
|
|
245
|
+
environment: (sequence) => {
|
|
246
|
+
const nowMs = js.now?.() ?? Date.now();
|
|
247
|
+
return {
|
|
248
|
+
nowMs,
|
|
249
|
+
utcOffsetMinutes: js.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
|
|
250
|
+
randomSeed: js.randomSeed?.() ?? seedFrom(ctx.documentId ?? "document", sequence)
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
bootSources: async () => {
|
|
254
|
+
return ((await readDocumentActions())?.nameTreeScripts ?? []).map(({ action }) => javaScriptProgramFromActionTree(action));
|
|
255
|
+
},
|
|
256
|
+
...js.budget ? { budget: js.budget } : {}
|
|
257
|
+
}) : null;
|
|
258
|
+
if (scriptHost) ctx.cleanup(() => scriptHost.dispose());
|
|
259
|
+
const surfaceScriptResult = (result) => {
|
|
260
|
+
const uiContext = {
|
|
261
|
+
origin: result.origin,
|
|
262
|
+
phase: result.phase
|
|
263
|
+
};
|
|
264
|
+
for (const effect of result.uiEffects) {
|
|
265
|
+
if (effect.kind === "submitForm") {
|
|
266
|
+
performSubmit(intentOfSubmitEffect(effect), {
|
|
267
|
+
origin: result.origin,
|
|
268
|
+
source: { kind: "api" },
|
|
269
|
+
event: { scope: "activate" }
|
|
270
|
+
}, (diagnostic) => diagnosticHook.emit(diagnostic));
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (effect.kind === "print" && !allowsPrint()) {
|
|
274
|
+
scriptDiagnosticHook.emit({
|
|
275
|
+
code: "ui-effect-suppressed",
|
|
276
|
+
message: "script print request withheld: doc.print is not allowed"
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (effect.kind === "print" && docPrintEventActive) {
|
|
281
|
+
scriptDiagnosticHook.emit({
|
|
282
|
+
code: "ui-effect-suppressed",
|
|
283
|
+
message: "script print request during a document print event — suppressed (reentrant)"
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (!uiAdapter) {
|
|
288
|
+
diagnosticHook.emit({
|
|
289
|
+
code: "no-adapter",
|
|
290
|
+
message: `script ${effect.kind}: no UI adapter installed`
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (effect.kind === "alert") uiAdapter.alert?.(effect.message, {
|
|
295
|
+
...uiContext,
|
|
296
|
+
icon: effect.icon,
|
|
297
|
+
...effect.title !== void 0 ? { title: effect.title } : {}
|
|
298
|
+
});
|
|
299
|
+
else if (effect.kind === "gotoPage") uiAdapter.gotoPage?.(effect.page, uiContext);
|
|
300
|
+
else uiAdapter.print(uiContext);
|
|
301
|
+
}
|
|
302
|
+
for (const diagnostic of result.diagnostics) scriptDiagnosticHook.emit(diagnostic);
|
|
303
|
+
if (result.error) scriptErrorHook.emit(result.error);
|
|
304
|
+
};
|
|
305
|
+
/** Script `doc.submitForm(...)` → the one normalized intent. Script field
|
|
306
|
+
* names are include-mode by definition (Acrobat's aFields). */
|
|
307
|
+
const intentOfSubmitEffect = (effect) => {
|
|
308
|
+
const format = effect.format ?? "fdf";
|
|
309
|
+
return {
|
|
310
|
+
url: effect.url,
|
|
311
|
+
fields: effect.fieldNames?.map((name) => ({
|
|
312
|
+
kind: "name",
|
|
313
|
+
name
|
|
314
|
+
})) ?? null,
|
|
315
|
+
exclude: false,
|
|
316
|
+
includeNoValueFields: effect.includeEmpty,
|
|
317
|
+
format,
|
|
318
|
+
method: effect.method ?? "post",
|
|
319
|
+
flagsRaw: 0
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
const engineColorToArray = (color) => color ? [
|
|
323
|
+
"RGB",
|
|
324
|
+
color.r / 255,
|
|
325
|
+
color.g / 255,
|
|
326
|
+
color.b / 255
|
|
327
|
+
] : ["T"];
|
|
328
|
+
const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((page) => page.pageObjectNumber === pon) ?? -1;
|
|
329
|
+
const scriptWorldFor = async (pon) => {
|
|
330
|
+
const doc = ctx.doc;
|
|
331
|
+
const snapshot = await doc.forms.list();
|
|
332
|
+
const pageIndex = Math.max(0, pageIndexOf(pon));
|
|
333
|
+
const { annotations } = await doc.page(pon).annotations.list();
|
|
334
|
+
const annots = annotations.filter((a) => a.subtype !== "link" && !a.subtype.startsWith("widget")).map((a) => {
|
|
335
|
+
const styled = a;
|
|
336
|
+
return {
|
|
337
|
+
ref: a.ref,
|
|
338
|
+
name: a.nm ?? "",
|
|
339
|
+
subtype: a.subtype,
|
|
340
|
+
page: pageIndex,
|
|
341
|
+
rect: [
|
|
342
|
+
a.rect.left,
|
|
343
|
+
a.rect.bottom,
|
|
344
|
+
a.rect.right,
|
|
345
|
+
a.rect.top
|
|
346
|
+
],
|
|
347
|
+
contents: a.contents ?? "",
|
|
348
|
+
author: a.author ?? "",
|
|
349
|
+
subject: a.subject ?? "",
|
|
350
|
+
strokeColor: engineColorToArray(styled.color),
|
|
351
|
+
fillColor: engineColorToArray(styled.interiorColor),
|
|
352
|
+
opacity: styled.opacity ?? 1,
|
|
353
|
+
width: styled.strokeWidth ?? 1,
|
|
354
|
+
borderStyle: styled.borderStyle === "dashed" ? "D" : "S",
|
|
355
|
+
dash: styled.dashArray ?? [],
|
|
356
|
+
hidden: a.flags.hidden,
|
|
357
|
+
print: a.flags.print,
|
|
358
|
+
readOnly: a.flags.readOnly,
|
|
359
|
+
locked: a.flags.locked,
|
|
360
|
+
noView: a.flags.noView,
|
|
361
|
+
toggleNoView: a.flags.toggleNoView,
|
|
362
|
+
opaqueBody: a.subtype === "stamp"
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
snapshot,
|
|
367
|
+
world: {
|
|
368
|
+
fields: scriptFieldsFromSnapshot(snapshot),
|
|
369
|
+
annots,
|
|
370
|
+
annotPages: [pageIndex],
|
|
371
|
+
annotsCoverDocument: (ctx.document()?.pageCount ?? 0) <= 1
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
const ANNOTATION_EVENT_NAMES = {
|
|
376
|
+
cursorEnter: "Mouse Enter",
|
|
377
|
+
cursorExit: "Mouse Exit",
|
|
378
|
+
mouseDown: "Mouse Down",
|
|
379
|
+
mouseUp: "Mouse Up",
|
|
380
|
+
focus: "Focus",
|
|
381
|
+
blur: "Blur"
|
|
382
|
+
};
|
|
383
|
+
const PAGE_EVENT_NAMES = {
|
|
384
|
+
open: "Open",
|
|
385
|
+
close: "Close",
|
|
386
|
+
visible: "Visible",
|
|
387
|
+
invisible: "Invisible"
|
|
388
|
+
};
|
|
389
|
+
const DOC_EVENT_NAMES = {
|
|
390
|
+
open: "Open",
|
|
391
|
+
"will-save": "WillSave",
|
|
392
|
+
"did-save": "DidSave",
|
|
393
|
+
"will-print": "WillPrint",
|
|
394
|
+
"did-print": "DidPrint",
|
|
395
|
+
"will-close": "WillClose"
|
|
396
|
+
};
|
|
397
|
+
const scriptEventFor = (actionCtx, snapshot) => {
|
|
398
|
+
const provenance = actionCtx.event;
|
|
399
|
+
const type = provenance.scope === "page" ? "Page" : provenance.scope === "document" ? "Doc" : actionCtx.source.kind === "widget" ? "Field" : actionCtx.source.kind === "link" ? "Link" : "Annot";
|
|
400
|
+
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";
|
|
401
|
+
const targetRef = actionCtx.source.kind === "widget" ? actionCtx.source.field : void 0;
|
|
402
|
+
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;
|
|
403
|
+
return {
|
|
404
|
+
kind: "widget-activate",
|
|
405
|
+
type,
|
|
406
|
+
name,
|
|
407
|
+
...targetRef ? {
|
|
408
|
+
target: targetRef,
|
|
409
|
+
source: targetRef
|
|
410
|
+
} : {},
|
|
411
|
+
...targetField && targetField.valueEntry.kind === "scalar" ? { value: targetField.valueEntry.value } : {}
|
|
412
|
+
};
|
|
413
|
+
};
|
|
414
|
+
/** Commit one run's document effects through the owner sinks in the
|
|
415
|
+
* DECLARED order (form first, then annot); the first failure skips the
|
|
416
|
+
* rest across BOTH streams. Returns the failure summary, if any. */
|
|
417
|
+
const commitScriptOutput = async (output, diagnose) => {
|
|
418
|
+
let failure = null;
|
|
419
|
+
if (output.formEffects.length > 0) if (!formCommitSink) diagnose({
|
|
420
|
+
code: "no-commit-sink",
|
|
421
|
+
message: `script form effects dropped: no form commit sink registered (${output.formEffects.length})`
|
|
422
|
+
});
|
|
423
|
+
else {
|
|
424
|
+
const bad = (await formCommitSink(output.formEffects)).results.find((entry) => entry.status === "failed" || entry.status === "rejected");
|
|
425
|
+
if (bad) failure = `form effect ${bad.index}: ${bad.error?.message ?? bad.status}`;
|
|
426
|
+
}
|
|
427
|
+
if (output.annotEffects.length > 0) if (failure) diagnose({
|
|
428
|
+
code: "executor-failed",
|
|
429
|
+
message: `script annot effects skipped after form failure (${output.annotEffects.length})`
|
|
430
|
+
});
|
|
431
|
+
else if (!annotCommitSink) diagnose({
|
|
432
|
+
code: "no-commit-sink",
|
|
433
|
+
message: `script annot effects dropped: no annotation commit sink registered (${output.annotEffects.length})`
|
|
434
|
+
});
|
|
435
|
+
else {
|
|
436
|
+
const entries = output.annotEffects.map((effect) => ({
|
|
437
|
+
annotObjectNumber: effect.ref.kind === "objectNumber" ? effect.ref.annotObjectNumber : -1,
|
|
438
|
+
...effect.ref.kind === "objectNumber" ? { pageObjectNumber: effect.ref.pageObjectNumber } : {},
|
|
439
|
+
patch: effect.patch
|
|
440
|
+
}));
|
|
441
|
+
const bad = (await annotCommitSink(entries)).results.find((entry) => entry.status === "failed");
|
|
442
|
+
if (bad) failure = `annotation ${bad.annotObjectNumber}: ${bad.error ?? "failed"}`;
|
|
443
|
+
}
|
|
444
|
+
if (failure) diagnose({
|
|
445
|
+
code: "executor-failed",
|
|
446
|
+
message: `script commit: ${failure}`
|
|
447
|
+
});
|
|
448
|
+
return failure;
|
|
449
|
+
};
|
|
450
|
+
if (scriptHost) {
|
|
451
|
+
const nodeCap = js?.maxScriptNodesPerDispatch ?? 16;
|
|
452
|
+
executors.set("javascript", async (node, actionCtx) => {
|
|
453
|
+
if (node.type !== "javascript") return {
|
|
454
|
+
status: "inert",
|
|
455
|
+
reason: "not a JS node"
|
|
456
|
+
};
|
|
457
|
+
if (!ctx.doc) return {
|
|
458
|
+
status: "inert",
|
|
459
|
+
reason: "no document"
|
|
460
|
+
};
|
|
461
|
+
if (jsNodesThisDispatch >= nodeCap) return {
|
|
462
|
+
status: "inert",
|
|
463
|
+
reason: `dispatch script budget exhausted (${nodeCap} JS nodes)`
|
|
464
|
+
};
|
|
465
|
+
jsNodesThisDispatch += 1;
|
|
466
|
+
const source = actionCtx.source;
|
|
467
|
+
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;
|
|
468
|
+
if (pon === void 0) return {
|
|
469
|
+
status: "inert",
|
|
470
|
+
reason: "no page to anchor the world on"
|
|
471
|
+
};
|
|
472
|
+
const diagnose = (diagnostic) => diagnosticHook.emit(diagnostic);
|
|
473
|
+
const pendingExternal = [];
|
|
474
|
+
const splitExternal = (output, phase) => output.uiEffects.filter((effect) => {
|
|
475
|
+
if (effect.kind === "print" || effect.kind === "submitForm") {
|
|
476
|
+
pendingExternal.push({
|
|
477
|
+
effect,
|
|
478
|
+
phase
|
|
479
|
+
});
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
});
|
|
484
|
+
const outcome = await scriptHost.transaction(async (txn) => {
|
|
485
|
+
let built = await scriptWorldFor(pon);
|
|
486
|
+
const boot = await txn.boot({
|
|
487
|
+
...built.world,
|
|
488
|
+
event: {
|
|
489
|
+
kind: "name-tree-boot",
|
|
490
|
+
type: "Doc",
|
|
491
|
+
name: "Open"
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
if (boot) {
|
|
495
|
+
await commitScriptOutput(boot, diagnose);
|
|
496
|
+
surfaceScriptResult({
|
|
497
|
+
uiEffects: splitExternal(boot, "boot"),
|
|
498
|
+
diagnostics: boot.diagnostics,
|
|
499
|
+
...boot.error ? { error: boot.error } : {},
|
|
500
|
+
origin: actionCtx.origin,
|
|
501
|
+
phase: "boot"
|
|
502
|
+
});
|
|
503
|
+
if (boot.formEffects.length || boot.annotEffects.length) built = await scriptWorldFor(pon);
|
|
504
|
+
}
|
|
505
|
+
const output = await txn.run(node.script, {
|
|
506
|
+
...built.world,
|
|
507
|
+
event: scriptEventFor(actionCtx, built.snapshot)
|
|
508
|
+
});
|
|
509
|
+
surfaceScriptResult({
|
|
510
|
+
uiEffects: splitExternal(output, "user"),
|
|
511
|
+
diagnostics: output.diagnostics,
|
|
512
|
+
...output.error ? { error: output.error } : {},
|
|
513
|
+
origin: actionCtx.origin,
|
|
514
|
+
phase: "user"
|
|
515
|
+
});
|
|
516
|
+
if (output.error) return {
|
|
517
|
+
status: "failed",
|
|
518
|
+
error: output.error.message
|
|
519
|
+
};
|
|
520
|
+
const failure = await commitScriptOutput(output, diagnose);
|
|
521
|
+
return failure ? {
|
|
522
|
+
status: "failed",
|
|
523
|
+
error: failure
|
|
524
|
+
} : { status: "executed" };
|
|
525
|
+
});
|
|
526
|
+
for (const entry of pendingExternal) if (entry.effect.kind === "print") await firePrintThroughAdapter({
|
|
527
|
+
origin: actionCtx.origin,
|
|
528
|
+
phase: entry.phase
|
|
529
|
+
}, diagnose);
|
|
530
|
+
else await performSubmit(intentOfSubmitEffect(entry.effect), actionCtx, diagnose);
|
|
531
|
+
return outcome;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
async function run(tree, actionCtx) {
|
|
535
|
+
const diagnostics = [];
|
|
536
|
+
const diagnose = (diagnostic) => {
|
|
537
|
+
diagnostics.push(diagnostic);
|
|
538
|
+
diagnosticHook.emit(diagnostic);
|
|
539
|
+
};
|
|
540
|
+
if (tree.incomplete) {
|
|
541
|
+
diagnose({
|
|
542
|
+
code: "incomplete-tree",
|
|
543
|
+
message: "refused: the action tree is incomplete"
|
|
544
|
+
});
|
|
545
|
+
return {
|
|
546
|
+
status: "refused",
|
|
547
|
+
nodes: [],
|
|
548
|
+
diagnostics
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (!tree.root) return {
|
|
552
|
+
status: "inert",
|
|
553
|
+
nodes: [],
|
|
554
|
+
diagnostics
|
|
555
|
+
};
|
|
556
|
+
const nodes = [];
|
|
557
|
+
const deferred = [];
|
|
558
|
+
let documentFailed = false;
|
|
559
|
+
const settle = (result, outcome) => {
|
|
560
|
+
if (outcome.status === "executed") result.status = "executed";
|
|
561
|
+
else if (outcome.status === "inert") {
|
|
562
|
+
result.status = "inert";
|
|
563
|
+
result.detail = outcome.reason;
|
|
564
|
+
diagnose({
|
|
565
|
+
code: "executor-inert",
|
|
566
|
+
message: `${result.type}: ${outcome.reason}`
|
|
567
|
+
});
|
|
568
|
+
} else {
|
|
569
|
+
result.status = "failed";
|
|
570
|
+
result.detail = outcome.error;
|
|
571
|
+
diagnose({
|
|
572
|
+
code: "executor-failed",
|
|
573
|
+
message: `${result.type}: ${outcome.error}`
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const interpretHide = async (node) => {
|
|
578
|
+
const doc = ctx.doc;
|
|
579
|
+
const display = node.hide ? "hidden" : "visible";
|
|
580
|
+
const fieldRefs = [];
|
|
581
|
+
const annotEntries = [];
|
|
582
|
+
const names = [];
|
|
583
|
+
const bareObjectNumbers = [];
|
|
584
|
+
for (const target of node.targets) if (target.kind === "objectNumber") bareObjectNumbers.push(target.objectNumber);
|
|
585
|
+
else names.push(target.name);
|
|
586
|
+
if ((names.length || bareObjectNumbers.length) && doc) {
|
|
587
|
+
const snapshot = names.length || bareObjectNumbers.length ? await doc.forms.list() : null;
|
|
588
|
+
for (const name of names) {
|
|
589
|
+
const field = snapshot?.fields.find((candidate) => candidate.name === name);
|
|
590
|
+
if (!field) {
|
|
591
|
+
diagnose({
|
|
592
|
+
code: "unresolved-target",
|
|
593
|
+
message: `hide: no field named '${name}'`
|
|
594
|
+
});
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
fieldRefs.push({
|
|
598
|
+
kind: "objectNumber",
|
|
599
|
+
fieldObjectNumber: field.fieldObjectNumber
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
for (const objectNumber of bareObjectNumbers) {
|
|
603
|
+
const owner = snapshot?.fields.find((field) => field.widgets.some((widget) => widget.annotObjectNumber === objectNumber));
|
|
604
|
+
if (owner) fieldRefs.push({
|
|
605
|
+
kind: "objectNumber",
|
|
606
|
+
fieldObjectNumber: owner.fieldObjectNumber
|
|
607
|
+
});
|
|
608
|
+
else annotEntries.push({
|
|
609
|
+
annotObjectNumber: objectNumber,
|
|
610
|
+
patch: { flags: { hidden: node.hide } }
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
let failedDetail;
|
|
615
|
+
if (fieldRefs.length) {
|
|
616
|
+
if (!formCommitSink) {
|
|
617
|
+
diagnose({
|
|
618
|
+
code: "no-commit-sink",
|
|
619
|
+
message: "hide: no form commit sink registered (form plugin absent)"
|
|
620
|
+
});
|
|
621
|
+
return { status: "no-executor" };
|
|
622
|
+
}
|
|
623
|
+
const bad = (await formCommitSink(fieldRefs.map((ref) => ({
|
|
624
|
+
kind: "setDisplay",
|
|
625
|
+
ref,
|
|
626
|
+
display
|
|
627
|
+
})))).results.find((entry) => entry.status === "failed" || entry.status === "rejected");
|
|
628
|
+
if (bad) {
|
|
629
|
+
failedDetail = bad.error?.message ?? bad.status;
|
|
630
|
+
diagnose({
|
|
631
|
+
code: "executor-failed",
|
|
632
|
+
message: `hide: ${failedDetail}`
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (annotEntries.length && !failedDetail) {
|
|
637
|
+
if (!annotCommitSink) {
|
|
638
|
+
diagnose({
|
|
639
|
+
code: "no-commit-sink",
|
|
640
|
+
message: "hide: no annotation commit sink registered (annotation plugin absent)"
|
|
641
|
+
});
|
|
642
|
+
return { status: "no-executor" };
|
|
643
|
+
}
|
|
644
|
+
const committed = await annotCommitSink(annotEntries);
|
|
645
|
+
const failed = committed.results.filter((entry) => entry.status === "failed");
|
|
646
|
+
for (const failure of failed) diagnose({
|
|
647
|
+
code: "executor-failed",
|
|
648
|
+
message: `hide: annotation ${failure.annotObjectNumber}: ${failure.error ?? "failed"}`
|
|
649
|
+
});
|
|
650
|
+
if (failed.length === committed.results.length && failed.length > 0) failedDetail = failed[0]?.error;
|
|
651
|
+
}
|
|
652
|
+
if (failedDetail) return {
|
|
653
|
+
status: "failed",
|
|
654
|
+
detail: failedDetail
|
|
655
|
+
};
|
|
656
|
+
return { status: "executed" };
|
|
657
|
+
};
|
|
658
|
+
const interpret = async (node, path) => {
|
|
659
|
+
const result = {
|
|
660
|
+
path,
|
|
661
|
+
type: node.type,
|
|
662
|
+
status: "blocked"
|
|
663
|
+
};
|
|
664
|
+
nodes.push(result);
|
|
665
|
+
const decision = decisionFor(node, actionCtx.origin);
|
|
666
|
+
if (decision === "never" || decision === "block" || decision === "report") {
|
|
667
|
+
result.status = "blocked";
|
|
668
|
+
diagnose({
|
|
669
|
+
code: "blocked",
|
|
670
|
+
message: `${node.type}: ${decision === "never" ? "never executable" : `policy '${decision}' for origin '${actionCtx.origin}'`}`
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (decision === "unsupported") {
|
|
675
|
+
result.status = "no-executor";
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (node.type === "hide") {
|
|
679
|
+
if (documentFailed) {
|
|
680
|
+
result.status = "skipped";
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const outcome = await interpretHide(node);
|
|
684
|
+
result.status = outcome.status;
|
|
685
|
+
if (outcome.detail) result.detail = outcome.detail;
|
|
686
|
+
if (outcome.status === "failed") documentFailed = true;
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (DOCUMENT_TYPES.has(node.type)) {
|
|
690
|
+
if (documentFailed) {
|
|
691
|
+
result.status = "skipped";
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const executor = executors.get(node.type);
|
|
695
|
+
if (!executor) {
|
|
696
|
+
result.status = node.type === "javascript" ? "inert" : "no-executor";
|
|
697
|
+
diagnose({
|
|
698
|
+
code: "no-executor",
|
|
699
|
+
message: `${node.type}: no executor registered${node.type === "javascript" ? " (scripting unavailable)" : ""}`
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
settle(result, await executor(node, actionCtx));
|
|
705
|
+
} catch (error) {
|
|
706
|
+
settle(result, {
|
|
707
|
+
status: "failed",
|
|
708
|
+
error: error instanceof Error ? error.message : String(error)
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (result.status === "failed") documentFailed = true;
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (node.type === "submit-form") {
|
|
715
|
+
if (!node.payload) {
|
|
716
|
+
result.status = "inert";
|
|
717
|
+
result.detail = "submit payload unavailable (older runtime extraction)";
|
|
718
|
+
diagnose({
|
|
719
|
+
code: "submit-payload-unavailable",
|
|
720
|
+
message: "submit-form: payload unavailable (older runtime extraction) — node inert"
|
|
721
|
+
});
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const payload = node.payload;
|
|
725
|
+
result.status = "skipped";
|
|
726
|
+
deferred.push({
|
|
727
|
+
result,
|
|
728
|
+
fire: async () => {
|
|
729
|
+
const outcome = await performSubmit(intentOfPayload(payload), actionCtx, diagnose);
|
|
730
|
+
result.status = outcome.status;
|
|
731
|
+
if (outcome.detail) result.detail = outcome.detail;
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (isPrintVerb(node) || node.type === "uri") {
|
|
737
|
+
result.status = "skipped";
|
|
738
|
+
deferred.push({
|
|
739
|
+
result,
|
|
740
|
+
fire: async () => {
|
|
741
|
+
if (node.type === "uri") {
|
|
742
|
+
if (!uiAdapter) {
|
|
743
|
+
result.status = "no-executor";
|
|
744
|
+
diagnose({
|
|
745
|
+
code: "no-adapter",
|
|
746
|
+
message: `${node.type}: no UI adapter installed`
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
uiAdapter.openUri(node.uri, {
|
|
751
|
+
isMap: node.isMap,
|
|
752
|
+
origin: actionCtx.origin
|
|
753
|
+
});
|
|
754
|
+
result.status = "executed";
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const outcome = await firePrintThroughAdapter(void 0, diagnose);
|
|
758
|
+
result.status = outcome.status;
|
|
759
|
+
if (outcome.detail) result.detail = outcome.detail;
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
result.status = "skipped";
|
|
765
|
+
deferred.push({
|
|
766
|
+
result,
|
|
767
|
+
fire: async () => {
|
|
768
|
+
const executor = executors.get(node.type);
|
|
769
|
+
if (!executor) {
|
|
770
|
+
result.status = "no-executor";
|
|
771
|
+
diagnose({
|
|
772
|
+
code: "no-executor",
|
|
773
|
+
message: `${node.type}: no executor registered`
|
|
774
|
+
});
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
settle(result, await executor(node, actionCtx));
|
|
779
|
+
} catch (error) {
|
|
780
|
+
settle(result, {
|
|
781
|
+
status: "failed",
|
|
782
|
+
error: error instanceof Error ? error.message : String(error)
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
};
|
|
788
|
+
const walk = async (node, path) => {
|
|
789
|
+
await interpret(node, path);
|
|
790
|
+
for (let index = 0; index < node.next.length; index++) await walk(node.next[index], [...path, index]);
|
|
791
|
+
};
|
|
792
|
+
await walk(tree.root, []);
|
|
793
|
+
if (!documentFailed) for (const entry of deferred) await entry.fire();
|
|
794
|
+
const anyExecuted = nodes.some((node) => node.status === "executed");
|
|
795
|
+
const anyFailedOrSkipped = nodes.some((node) => node.status === "failed" || node.status === "skipped");
|
|
796
|
+
return {
|
|
797
|
+
status: anyExecuted ? anyFailedOrSkipped ? "partial" : "executed" : anyFailedOrSkipped ? "partial" : "inert",
|
|
798
|
+
nodes,
|
|
799
|
+
diagnostics
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/** run + seq + event — the shared per-tree unit (queued by execute; called
|
|
803
|
+
* inline per step by the queued trigger resolver). */
|
|
804
|
+
const runAndEmit = async (tree, actionCtx) => {
|
|
805
|
+
const result = await run(tree, actionCtx);
|
|
806
|
+
ctx.dispatch({ type: "ACTIONS_DISPATCHED" });
|
|
807
|
+
actionHook.emit({
|
|
808
|
+
ctx: actionCtx,
|
|
809
|
+
tree,
|
|
810
|
+
result
|
|
811
|
+
});
|
|
812
|
+
return result;
|
|
813
|
+
};
|
|
814
|
+
const execute = (tree, actionCtx) => {
|
|
815
|
+
if (actionCtx.origin === "user") noteUserActivity();
|
|
816
|
+
return enqueue(() => {
|
|
817
|
+
jsNodesThisDispatch = 0;
|
|
818
|
+
return runAndEmit(tree, actionCtx);
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
const canExecute = (tree, actionCtx) => {
|
|
822
|
+
if (tree.incomplete || !tree.root) return false;
|
|
823
|
+
const decision = decisionFor(tree.root, actionCtx.origin);
|
|
824
|
+
return decision === "allow" || decision === "adapter";
|
|
825
|
+
};
|
|
826
|
+
const triggerEnabled = (trigger) => {
|
|
827
|
+
switch (trigger.scope) {
|
|
828
|
+
case "activate": return true;
|
|
829
|
+
case "annotation": return config.triggers?.annotation !== false;
|
|
830
|
+
case "page": return config.triggers?.page !== false;
|
|
831
|
+
case "document": return config.triggers?.document !== false;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
const lifecycleCache = /* @__PURE__ */ new Map();
|
|
835
|
+
const lifecycleTreesFor = async (pon) => {
|
|
836
|
+
const hit = lifecycleCache.get(pon);
|
|
837
|
+
if (hit) return hit;
|
|
838
|
+
const doc = ctx.doc;
|
|
839
|
+
if (!doc) return [];
|
|
840
|
+
const { annotations } = await doc.page(pon).annotations.list();
|
|
841
|
+
const bearing = annotations.filter((a) => a.actions && (a.actions.pageOpen?.root || a.actions.pageClose?.root || a.actions.pageVisible?.root || a.actions.pageInvisible?.root)).map((a) => ({
|
|
842
|
+
ref: a.ref,
|
|
843
|
+
actions: a.actions
|
|
844
|
+
}));
|
|
845
|
+
lifecycleCache.set(pon, bearing);
|
|
846
|
+
return bearing;
|
|
847
|
+
};
|
|
848
|
+
{
|
|
849
|
+
const unsubscribe = ctx.doc?.events?.subscribe((event) => {
|
|
850
|
+
if (event.type.startsWith("annotation.") || event.type === "stream.desynced") lifecycleCache.clear();
|
|
851
|
+
});
|
|
852
|
+
if (unsubscribe) ctx.cleanup(unsubscribe);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* ISO order, verified against 32000-2 Table 197 (2026-09-02): PO "shall be
|
|
856
|
+
* executed after the O action … and the OpenAction entry"; PC "shall be
|
|
857
|
+
* executed before the C action". Rootless trees are skipped (a
|
|
858
|
+
* budget-degraded tree with no root has nothing to walk).
|
|
859
|
+
*/
|
|
860
|
+
const planPageSteps = (event, pon, pageActions, lifecycle) => {
|
|
861
|
+
const pageSource = {
|
|
862
|
+
kind: "page",
|
|
863
|
+
pon
|
|
864
|
+
};
|
|
865
|
+
const annotSteps = (key) => lifecycle.filter((a) => a.actions[key]?.root).map((a) => ({
|
|
866
|
+
source: {
|
|
867
|
+
kind: "annotation",
|
|
868
|
+
annotation: a.ref,
|
|
869
|
+
pon
|
|
870
|
+
},
|
|
871
|
+
tree: a.actions[key]
|
|
872
|
+
}));
|
|
873
|
+
switch (event) {
|
|
874
|
+
case "open": return [...pageActions?.open?.root ? [{
|
|
875
|
+
source: pageSource,
|
|
876
|
+
tree: pageActions.open
|
|
877
|
+
}] : [], ...annotSteps("pageOpen")];
|
|
878
|
+
case "close": return [...annotSteps("pageClose"), ...pageActions?.close?.root ? [{
|
|
879
|
+
source: pageSource,
|
|
880
|
+
tree: pageActions.close
|
|
881
|
+
}] : []];
|
|
882
|
+
case "visible": return annotSteps("pageVisible");
|
|
883
|
+
case "invisible": return annotSteps("pageInvisible");
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
/** Aggregate step statuses. A step failure never skips its siblings; this
|
|
887
|
+
* only FOLDS what each step reported. */
|
|
888
|
+
const foldSteps = (steps, diagnostics) => {
|
|
889
|
+
const statuses = steps.map((s) => s.result.status);
|
|
890
|
+
return {
|
|
891
|
+
status: steps.length === 0 ? "inert" : statuses.every((s) => s === "refused") ? "refused" : statuses.every((s) => s === "inert") ? "inert" : statuses.every((s) => s === "executed") ? "executed" : "partial",
|
|
892
|
+
steps,
|
|
893
|
+
diagnostics
|
|
894
|
+
};
|
|
895
|
+
};
|
|
896
|
+
const runSteps = async (steps, origin, event, diagnostics) => {
|
|
897
|
+
const results = [];
|
|
898
|
+
for (const step of steps) {
|
|
899
|
+
const result = await runAndEmit(step.tree, {
|
|
900
|
+
origin,
|
|
901
|
+
source: step.source,
|
|
902
|
+
event
|
|
903
|
+
});
|
|
904
|
+
results.push({
|
|
905
|
+
source: step.source,
|
|
906
|
+
tree: step.tree,
|
|
907
|
+
result
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return foldSteps(results, diagnostics);
|
|
911
|
+
};
|
|
912
|
+
/** Everything a trigger needs — reads included — INSIDE the queued op:
|
|
913
|
+
* submission order is execution order. Never throws. */
|
|
914
|
+
const resolveAndRun = async (trigger) => {
|
|
915
|
+
const diagnostics = [];
|
|
916
|
+
const diagnose = (diagnostic) => {
|
|
917
|
+
diagnostics.push(diagnostic);
|
|
918
|
+
diagnosticHook.emit(diagnostic);
|
|
919
|
+
};
|
|
920
|
+
try {
|
|
921
|
+
const doc = ctx.doc;
|
|
922
|
+
if (!doc) return {
|
|
923
|
+
status: "inert",
|
|
924
|
+
steps: [],
|
|
925
|
+
diagnostics
|
|
926
|
+
};
|
|
927
|
+
if (!triggerEnabled(trigger)) {
|
|
928
|
+
diagnose({
|
|
929
|
+
code: "trigger-disabled",
|
|
930
|
+
message: `${trigger.scope}: trigger family disabled by config`
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
status: "inert",
|
|
934
|
+
steps: [],
|
|
935
|
+
diagnostics
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const origin = originOf(trigger);
|
|
939
|
+
switch (trigger.scope) {
|
|
940
|
+
case "activate":
|
|
941
|
+
case "annotation": {
|
|
942
|
+
const event = trigger.scope === "activate" ? "activate" : trigger.event;
|
|
943
|
+
const { annotations } = await doc.page(trigger.pon).annotations.list();
|
|
944
|
+
const annotation = annotations.find((candidate) => sameRef(candidate.ref, trigger.ref));
|
|
945
|
+
if (event === "mouseUp" && annotation?.actions?.activate) return {
|
|
946
|
+
status: "inert",
|
|
947
|
+
steps: [],
|
|
948
|
+
diagnostics
|
|
949
|
+
};
|
|
950
|
+
const tree = annotation?.actions?.[event];
|
|
951
|
+
if (!tree?.root && !tree?.incomplete) return {
|
|
952
|
+
status: "inert",
|
|
953
|
+
steps: [],
|
|
954
|
+
diagnostics
|
|
955
|
+
};
|
|
956
|
+
const source = trigger.source ?? {
|
|
957
|
+
kind: "annotation",
|
|
958
|
+
annotation: trigger.ref,
|
|
959
|
+
pon: trigger.pon
|
|
960
|
+
};
|
|
961
|
+
return await runSteps([{
|
|
962
|
+
source,
|
|
963
|
+
tree
|
|
964
|
+
}], origin, eventOf(trigger), diagnostics);
|
|
965
|
+
}
|
|
966
|
+
case "page": {
|
|
967
|
+
const layout = ctx.document()?.pages.find((page) => page.pageObjectNumber === trigger.pon);
|
|
968
|
+
const lifecycle = await lifecycleTreesFor(trigger.pon);
|
|
969
|
+
const steps = planPageSteps(trigger.event, trigger.pon, layout?.actions, lifecycle);
|
|
970
|
+
return await runSteps(steps, origin, eventOf(trigger), diagnostics);
|
|
971
|
+
}
|
|
972
|
+
case "document":
|
|
973
|
+
if (trigger.event !== "open") return await runDocumentEventOp(trigger.event, diagnostics, diagnose);
|
|
974
|
+
if (openFired) {
|
|
975
|
+
diagnose({
|
|
976
|
+
code: "open-sequence-replayed",
|
|
977
|
+
message: "document open sequence already fired for this document"
|
|
978
|
+
});
|
|
979
|
+
return {
|
|
980
|
+
status: "inert",
|
|
981
|
+
steps: [],
|
|
982
|
+
diagnostics
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
openFired = true;
|
|
986
|
+
return await runOpenSequenceOp();
|
|
987
|
+
}
|
|
988
|
+
} catch (error) {
|
|
989
|
+
diagnose({
|
|
990
|
+
code: "trigger-failed",
|
|
991
|
+
message: `trigger resolution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
status: "refused",
|
|
995
|
+
steps: [],
|
|
996
|
+
diagnostics
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const dispatch = (trigger) => {
|
|
1001
|
+
if (originOf(trigger) === "user") noteUserActivity();
|
|
1002
|
+
return enqueue(() => {
|
|
1003
|
+
jsNodesThisDispatch = 0;
|
|
1004
|
+
return resolveAndRun(trigger);
|
|
1005
|
+
});
|
|
1006
|
+
};
|
|
1007
|
+
const CASCADE_CAP = 8;
|
|
1008
|
+
let barrierOpen = false;
|
|
1009
|
+
let bufferedReport = null;
|
|
1010
|
+
let lastEmitted = {
|
|
1011
|
+
currentPon: null,
|
|
1012
|
+
visible: /* @__PURE__ */ new Set()
|
|
1013
|
+
};
|
|
1014
|
+
let cascadeRounds = 0;
|
|
1015
|
+
let openFired = false;
|
|
1016
|
+
let sawUserActivity = false;
|
|
1017
|
+
const emitForReport = (report) => {
|
|
1018
|
+
if (report.cause === "user") cascadeRounds = 0;
|
|
1019
|
+
const nextVisible = new Set(report.visiblePons);
|
|
1020
|
+
const changedCurrent = report.currentPon !== lastEmitted.currentPon;
|
|
1021
|
+
const leaving = [...lastEmitted.visible].filter((pon) => !nextVisible.has(pon));
|
|
1022
|
+
const entering = [...nextVisible].filter((pon) => !lastEmitted.visible.has(pon));
|
|
1023
|
+
if (!changedCurrent && leaving.length === 0 && entering.length === 0) return;
|
|
1024
|
+
const previousCurrent = lastEmitted.currentPon;
|
|
1025
|
+
lastEmitted = {
|
|
1026
|
+
currentPon: report.currentPon,
|
|
1027
|
+
visible: nextVisible
|
|
1028
|
+
};
|
|
1029
|
+
if (report.cause === "programmatic") {
|
|
1030
|
+
cascadeRounds += 1;
|
|
1031
|
+
if (cascadeRounds > CASCADE_CAP) {
|
|
1032
|
+
diagnosticHook.emit({
|
|
1033
|
+
code: "cascade-budget",
|
|
1034
|
+
message: `page-lifecycle emission suppressed: ${cascadeRounds} consecutive programmatic rounds (cap ${CASCADE_CAP})`
|
|
1035
|
+
});
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (changedCurrent && previousCurrent !== null) dispatch({
|
|
1040
|
+
scope: "page",
|
|
1041
|
+
event: "close",
|
|
1042
|
+
pon: previousCurrent
|
|
1043
|
+
});
|
|
1044
|
+
for (const pon of leaving) dispatch({
|
|
1045
|
+
scope: "page",
|
|
1046
|
+
event: "invisible",
|
|
1047
|
+
pon
|
|
1048
|
+
});
|
|
1049
|
+
for (const pon of entering) dispatch({
|
|
1050
|
+
scope: "page",
|
|
1051
|
+
event: "visible",
|
|
1052
|
+
pon
|
|
1053
|
+
});
|
|
1054
|
+
if (changedCurrent && report.currentPon !== null) dispatch({
|
|
1055
|
+
scope: "page",
|
|
1056
|
+
event: "open",
|
|
1057
|
+
pon: report.currentPon
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
const reportPageState = (report) => {
|
|
1061
|
+
if (!report.placed) return;
|
|
1062
|
+
if (report.cause === "user") cascadeRounds = 0;
|
|
1063
|
+
if (!barrierOpen) {
|
|
1064
|
+
bufferedReport = report;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
emitForReport(report);
|
|
1068
|
+
};
|
|
1069
|
+
const releaseBarrier = (fireFallback) => {
|
|
1070
|
+
barrierOpen = true;
|
|
1071
|
+
const report = bufferedReport;
|
|
1072
|
+
bufferedReport = null;
|
|
1073
|
+
try {
|
|
1074
|
+
if (report) emitForReport(report);
|
|
1075
|
+
else if (fireFallback && config.openSequence === "headless") {
|
|
1076
|
+
const first = ctx.document()?.pages[0]?.pageObjectNumber;
|
|
1077
|
+
if (first !== void 0) {
|
|
1078
|
+
lastEmitted = {
|
|
1079
|
+
currentPon: first,
|
|
1080
|
+
visible: lastEmitted.visible
|
|
1081
|
+
};
|
|
1082
|
+
dispatch({
|
|
1083
|
+
scope: "page",
|
|
1084
|
+
event: "open",
|
|
1085
|
+
pon: first
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
diagnosticHook.emit({
|
|
1091
|
+
code: "trigger-failed",
|
|
1092
|
+
message: `barrier release failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
/** The §3.9 sequence body — runs INSIDE the queue (via dispatch or the
|
|
1097
|
+
* latch's own enqueue; callers guarantee openFired was set). */
|
|
1098
|
+
const runOpenSequenceOp = async () => {
|
|
1099
|
+
const diagnostics = [];
|
|
1100
|
+
const steps = [];
|
|
1101
|
+
const lifecycleCtx = {
|
|
1102
|
+
origin: "lifecycle",
|
|
1103
|
+
source: { kind: "document" },
|
|
1104
|
+
event: {
|
|
1105
|
+
scope: "document",
|
|
1106
|
+
name: "open"
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
try {
|
|
1110
|
+
const snapshot = await readDocumentActions();
|
|
1111
|
+
if (snapshot?.openDestination) {
|
|
1112
|
+
const tree = {
|
|
1113
|
+
root: {
|
|
1114
|
+
type: "goto",
|
|
1115
|
+
subtype: "GoTo",
|
|
1116
|
+
destination: snapshot.openDestination,
|
|
1117
|
+
next: []
|
|
1118
|
+
},
|
|
1119
|
+
incomplete: false,
|
|
1120
|
+
warningFlags: 0,
|
|
1121
|
+
warnings: []
|
|
1122
|
+
};
|
|
1123
|
+
steps.push({
|
|
1124
|
+
source: { kind: "document" },
|
|
1125
|
+
tree,
|
|
1126
|
+
result: await runAndEmit(tree, lifecycleCtx)
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
if (snapshot?.openAction?.root || snapshot?.openAction?.incomplete) steps.push({
|
|
1130
|
+
source: { kind: "document" },
|
|
1131
|
+
tree: snapshot.openAction,
|
|
1132
|
+
result: await runAndEmit(snapshot.openAction, lifecycleCtx)
|
|
1133
|
+
});
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
const diagnostic = {
|
|
1136
|
+
code: "trigger-failed",
|
|
1137
|
+
message: `open sequence failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1138
|
+
};
|
|
1139
|
+
diagnostics.push(diagnostic);
|
|
1140
|
+
diagnosticHook.emit(diagnostic);
|
|
1141
|
+
} finally {
|
|
1142
|
+
releaseBarrier(true);
|
|
1143
|
+
}
|
|
1144
|
+
return foldSteps(steps, diagnostics);
|
|
1145
|
+
};
|
|
1146
|
+
/**
|
|
1147
|
+
* D1's open-ordering law: catalog `OpenAction` may never run AFTER a
|
|
1148
|
+
* WillSave/WillPrint/WillClose script. Before the first verb-shaped
|
|
1149
|
+
* document event, an armed-but-unfired open sequence runs inline (we are
|
|
1150
|
+
* already inside the queue; `runOpenSequenceOp` never enqueues). It does
|
|
1151
|
+
* NOT wait for the initial page `/O` — the guarantee is catalog-level.
|
|
1152
|
+
*/
|
|
1153
|
+
const ensureOpenSequenceBeforeDocEvent = async () => {
|
|
1154
|
+
if (openFired || config.openSequence === "off") return;
|
|
1155
|
+
openFired = true;
|
|
1156
|
+
await runOpenSequenceOp();
|
|
1157
|
+
};
|
|
1158
|
+
/**
|
|
1159
|
+
* Run ONE catalog lifecycle tree inside the current queue operation —
|
|
1160
|
+
* shared by the dispatch path, `runDocumentVerb`, and the print wrapper.
|
|
1161
|
+
* Never throws: a read/resolution failure degrades to a diagnostic (a
|
|
1162
|
+
* broken script must not cancel the user's save/print — D2).
|
|
1163
|
+
*/
|
|
1164
|
+
const runDocEventTreeSafe = async (event, diagnose) => {
|
|
1165
|
+
if (config.triggers?.document === false) return null;
|
|
1166
|
+
try {
|
|
1167
|
+
await ensureOpenSequenceBeforeDocEvent();
|
|
1168
|
+
const tree = (await readDocumentActions())?.[DOC_EVENT_TREES[event]];
|
|
1169
|
+
if (!tree?.root && !tree?.incomplete) return null;
|
|
1170
|
+
return {
|
|
1171
|
+
source: { kind: "document" },
|
|
1172
|
+
tree,
|
|
1173
|
+
result: await runAndEmit(tree, {
|
|
1174
|
+
origin: "lifecycle",
|
|
1175
|
+
source: { kind: "document" },
|
|
1176
|
+
event: {
|
|
1177
|
+
scope: "document",
|
|
1178
|
+
name: event
|
|
1179
|
+
}
|
|
1180
|
+
})
|
|
1181
|
+
};
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
diagnose({
|
|
1184
|
+
code: "trigger-failed",
|
|
1185
|
+
message: `${event}: ${error instanceof Error ? error.message : String(error)}`
|
|
1186
|
+
});
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
/** The dispatch-path body for a verb-shaped document event. */
|
|
1191
|
+
const runDocumentEventOp = async (event, diagnostics, diagnose) => {
|
|
1192
|
+
const step = await runDocEventTreeSafe(event, diagnose);
|
|
1193
|
+
return foldSteps(step ? [step] : [], diagnostics);
|
|
1194
|
+
};
|
|
1195
|
+
/**
|
|
1196
|
+
* D3: BOTH adapter print invocations (the Named Print verb; script
|
|
1197
|
+
* `doc.print()` effects from the actions plane) go through here — WP →
|
|
1198
|
+
* `uiAdapter.print` exactly once → DP, latch reset in `finally`. While
|
|
1199
|
+
* the latch is held (including `runDocumentVerb('print')`'s body) a
|
|
1200
|
+
* nested request is suppressed with `reentrant-print`. An adapter throw
|
|
1201
|
+
* skips DP (the latch still resets) — named deviation: DP otherwise
|
|
1202
|
+
* fires when the adapter call RETURNS, since a browser cannot observe
|
|
1203
|
+
* dialog completion.
|
|
1204
|
+
*/
|
|
1205
|
+
const firePrintThroughAdapter = async (uiContext, diagnose) => {
|
|
1206
|
+
if (docPrintEventActive) {
|
|
1207
|
+
diagnose({
|
|
1208
|
+
code: "reentrant-print",
|
|
1209
|
+
message: "print request during a document print event — suppressed (one dialog per request)"
|
|
1210
|
+
});
|
|
1211
|
+
return {
|
|
1212
|
+
status: "blocked",
|
|
1213
|
+
detail: "reentrant print suppressed"
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
if (!allowsPrint()) {
|
|
1217
|
+
diagnose({
|
|
1218
|
+
code: "blocked",
|
|
1219
|
+
message: "print: doc.print is not allowed"
|
|
1220
|
+
});
|
|
1221
|
+
return {
|
|
1222
|
+
status: "blocked",
|
|
1223
|
+
detail: "doc.print is not allowed"
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
if (!uiAdapter) {
|
|
1227
|
+
diagnose({
|
|
1228
|
+
code: "no-adapter",
|
|
1229
|
+
message: "print: no UI adapter installed"
|
|
1230
|
+
});
|
|
1231
|
+
return {
|
|
1232
|
+
status: "no-executor",
|
|
1233
|
+
detail: "no UI adapter installed"
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
docPrintEventActive = true;
|
|
1237
|
+
try {
|
|
1238
|
+
await runDocEventTreeSafe("will-print", diagnose);
|
|
1239
|
+
const adapter = uiAdapter;
|
|
1240
|
+
if (!adapter) return {
|
|
1241
|
+
status: "no-executor",
|
|
1242
|
+
detail: "UI adapter uninstalled mid-print"
|
|
1243
|
+
};
|
|
1244
|
+
if (uiContext) adapter.print(uiContext);
|
|
1245
|
+
else adapter.print();
|
|
1246
|
+
await runDocEventTreeSafe("did-print", diagnose);
|
|
1247
|
+
return { status: "executed" };
|
|
1248
|
+
} finally {
|
|
1249
|
+
docPrintEventActive = false;
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
const maybeFireOpenSequence = () => {
|
|
1253
|
+
if (openFired) return;
|
|
1254
|
+
if (config.openSequence === "off") {
|
|
1255
|
+
openFired = true;
|
|
1256
|
+
releaseBarrier(false);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (!uiAdapter && config.openSequence !== "headless" && !sawUserActivity) return;
|
|
1260
|
+
openFired = true;
|
|
1261
|
+
enqueue(() => runOpenSequenceOp());
|
|
1262
|
+
};
|
|
1263
|
+
const noteUserActivity = () => {
|
|
1264
|
+
cascadeRounds = 0;
|
|
1265
|
+
if (!sawUserActivity) {
|
|
1266
|
+
sawUserActivity = true;
|
|
1267
|
+
maybeFireOpenSequence();
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
maybeFireOpenSequence();
|
|
1271
|
+
return {
|
|
1272
|
+
execute,
|
|
1273
|
+
canExecute,
|
|
1274
|
+
dispatch,
|
|
1275
|
+
canDispatch: (trigger) => triggerEnabled(trigger) && ctx.doc !== null,
|
|
1276
|
+
setUiAdapter: (adapter) => {
|
|
1277
|
+
uiAdapter = adapter;
|
|
1278
|
+
if (adapter) maybeFireOpenSequence();
|
|
1279
|
+
return () => {
|
|
1280
|
+
if (uiAdapter === adapter) uiAdapter = null;
|
|
1281
|
+
};
|
|
1282
|
+
},
|
|
1283
|
+
runDocumentVerb: (verb, operation) => enqueue(async () => {
|
|
1284
|
+
jsNodesThisDispatch = 0;
|
|
1285
|
+
const diagnose = (diagnostic) => diagnosticHook.emit(diagnostic);
|
|
1286
|
+
const before = verb === "save" ? "will-save" : "will-print";
|
|
1287
|
+
const after = verb === "save" ? "did-save" : "did-print";
|
|
1288
|
+
const body = async () => {
|
|
1289
|
+
await runDocEventTreeSafe(before, diagnose);
|
|
1290
|
+
const value = await operation();
|
|
1291
|
+
await runDocEventTreeSafe(after, diagnose);
|
|
1292
|
+
return value;
|
|
1293
|
+
};
|
|
1294
|
+
if (verb !== "print") return body();
|
|
1295
|
+
docPrintEventActive = true;
|
|
1296
|
+
try {
|
|
1297
|
+
return await body();
|
|
1298
|
+
} finally {
|
|
1299
|
+
docPrintEventActive = false;
|
|
1300
|
+
}
|
|
1301
|
+
}),
|
|
1302
|
+
prepareClose: () => dispatch({
|
|
1303
|
+
scope: "document",
|
|
1304
|
+
event: "will-close"
|
|
1305
|
+
}),
|
|
1306
|
+
setSubmitHandler: (handler) => {
|
|
1307
|
+
submitHandler = handler;
|
|
1308
|
+
return () => {
|
|
1309
|
+
if (submitHandler === handler) submitHandler = null;
|
|
1310
|
+
};
|
|
1311
|
+
},
|
|
1312
|
+
onAction: actionHook.on,
|
|
1313
|
+
onDiagnostic: diagnosticHook.on,
|
|
1314
|
+
onScriptDiagnostic: scriptDiagnosticHook.on,
|
|
1315
|
+
onScriptError: scriptErrorHook.on,
|
|
1316
|
+
registerExecutor: (type, executor) => {
|
|
1317
|
+
if (executors.has(type)) diagnosticHook.emit({
|
|
1318
|
+
code: "duplicate-executor",
|
|
1319
|
+
message: `executor for '${type}' replaced (last-wins)`
|
|
1320
|
+
});
|
|
1321
|
+
executors.set(type, executor);
|
|
1322
|
+
return () => {
|
|
1323
|
+
if (executors.get(type) === executor) executors.delete(type);
|
|
1324
|
+
};
|
|
1325
|
+
},
|
|
1326
|
+
registerAnnotCommitSink: (sink) => {
|
|
1327
|
+
annotCommitSink = sink;
|
|
1328
|
+
return () => {
|
|
1329
|
+
if (annotCommitSink === sink) annotCommitSink = null;
|
|
1330
|
+
};
|
|
1331
|
+
},
|
|
1332
|
+
registerFormCommitSink: (sink) => {
|
|
1333
|
+
formCommitSink = sink;
|
|
1334
|
+
return () => {
|
|
1335
|
+
if (formCommitSink === sink) formCommitSink = null;
|
|
1336
|
+
};
|
|
1337
|
+
},
|
|
1338
|
+
...scriptHost ? { scriptTransaction: (body) => scriptHost.transaction(body) } : {},
|
|
1339
|
+
surfaceScriptResult,
|
|
1340
|
+
reportPageState,
|
|
1341
|
+
registerSubmitResolver: (resolver) => {
|
|
1342
|
+
submitResolver = resolver;
|
|
1343
|
+
return () => {
|
|
1344
|
+
if (submitResolver === resolver) submitResolver = null;
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
//#endregion
|
|
1350
|
+
//#region src/reducer.ts
|
|
1351
|
+
const initialActionsState = () => ({ seq: 0 });
|
|
1352
|
+
function actionsReducer(state, action) {
|
|
1353
|
+
switch (action.type) {
|
|
1354
|
+
case "ACTIONS_DISPATCHED": return { seq: state.seq + 1 };
|
|
1355
|
+
default: return state;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/actions.plugin.ts
|
|
1360
|
+
/**
|
|
1361
|
+
* The action engine: the DEPENDENCY ROOT of the action architecture. It
|
|
1362
|
+
* interprets extracted /A and /AA trees; it never detects triggers and never
|
|
1363
|
+
* imports another plugin's token — stage, annotation, link, and form
|
|
1364
|
+
* optionally depend on ActionsToken and register their executors and sinks
|
|
1365
|
+
* at init (the kernel's topological order guarantees this plugin initializes
|
|
1366
|
+
* first). JavaScript is one registered interpreter among many: Hide,
|
|
1367
|
+
* ResetForm, GoTo, and Named work with scripting off.
|
|
1368
|
+
*/
|
|
1369
|
+
const actionsPlugin = (config) => definePlugin({
|
|
1370
|
+
id: "actions",
|
|
1371
|
+
token: ActionsToken,
|
|
1372
|
+
scope: "document",
|
|
1373
|
+
initialState: initialActionsState,
|
|
1374
|
+
reduce: actionsReducer,
|
|
1375
|
+
capability: (ctx) => createActionsCapability(ctx, config)
|
|
1376
|
+
});
|
|
1377
|
+
//#endregion
|
|
1378
|
+
export { ActionsToken, actionsPlugin, createHoverPump, originOf, submitEntriesToUrlEncoded };
|
|
1379
|
+
|
|
1380
|
+
//# sourceMappingURL=index.js.map
|