@embedpdf/plugin-form 3.0.0-next.1 → 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/dist/index.cjs CHANGED
@@ -1,9 +1,105 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_scripting = require("./scripting.cjs");
3
+ const require_types = require("./types-C1uIIYfn.cjs");
4
+ const require_contract = require("./contract.cjs");
2
5
  let _embedpdf_core = require("@embedpdf/core");
3
- let _embedpdf_plugin_annotation = require("@embedpdf/plugin-annotation");
4
- let _embedpdf_plugin_annotation_internal = require("@embedpdf/plugin-annotation/internal");
5
- let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
6
- let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
6
+ let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
7
+ let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
8
+ let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
9
+ let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
10
+ let _embedpdf_plugin_interaction_contract = require("@embedpdf/plugin-interaction/contract");
11
+ let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
12
+ let _embedpdf_plugin_annotation_authoring = require("@embedpdf/plugin-annotation/authoring");
13
+ //#region src/field-selection.ts
14
+ /**
15
+ * ISO field selection, written ONCE for ResetForm (Tables 241/242) and
16
+ * SubmitForm (Tables 239/240): a NAME target selects the field AND its
17
+ * descendants (FQN dot-prefix — `parent` selects `parent.c1`, the rule the
18
+ * old exact-match resolution violated); an objectNumber target selects that
19
+ * field dictionary. Include mode keeps the selection; exclude mode keeps
20
+ * the complement (a name excludes its subtree the same way it includes
21
+ * it). `targets === null` (the key ABSENT) selects everything and the flag
22
+ * is ignored; `[]` + include selects NOTHING while `[]` + exclude selects
23
+ * everything — presence and emptiness are different states.
24
+ *
25
+ * `listed` reports whether a field was ADDRESSED by the target list
26
+ * (directly or through its subtree) — the "explicitly listed" predicate the
27
+ * submit honesty diagnostics key on.
28
+ */
29
+ const resolveFieldSelection = (all, targets, exclude) => {
30
+ if (targets === null) return {
31
+ selected: [...all],
32
+ listed: () => false
33
+ };
34
+ const names = targets.filter((t) => t.kind === "name").map((t) => t.name);
35
+ const objectNumbers = new Set(targets.filter((t) => t.kind === "objectNumber").map((t) => t.objectNumber));
36
+ const listed = (field) => field.ref.kind === "objectNumber" && objectNumbers.has(field.ref.fieldObjectNumber) || names.some((name) => field.name === name || field.name.startsWith(`${name}.`));
37
+ return {
38
+ selected: exclude ? all.filter((f) => !listed(f)) : all.filter(listed),
39
+ listed
40
+ };
41
+ };
42
+ /**
43
+ * Build the resolved submission dataset from a live field list (Tables
44
+ * 239/240 semantics; pure — the capability supplies a FRESH engine read so
45
+ * no model-lag class exists):
46
+ *
47
+ * - the unconditional **NoExport veto** — even an explicit include cannot
48
+ * override it (diagnosed when explicitly listed, so the veto is
49
+ * observable, silent when swept in implicitly);
50
+ * - **push-buttons and signatures** never contribute entries in v1 —
51
+ * ISO's explicit-pushbutton-`/AP`-in-FDF rule is unrepresentable in the
52
+ * current DTO, so an EXPLICITLY listed one is diagnosed
53
+ * (`submit-entry-unsupported`), never silently dropped;
54
+ * - an **unsupported `/V` shape** is always diagnosed (silent omission
55
+ * would be data loss);
56
+ * - a valueless field becomes a NAME-ONLY entry (`value: null`) only under
57
+ * IncludeNoValueFields, else it is skipped (the ISO default);
58
+ * - values are entry-faithful: scalar → string, array (multi-select list
59
+ * box) → string[]; the submitted name is the fully qualified name.
60
+ */
61
+ const buildSubmitEntries = (fields, intent, diagnose) => {
62
+ const { selected, listed } = resolveFieldSelection(fields, intent.fields, intent.exclude);
63
+ const explicitInclude = intent.fields !== null && !intent.exclude;
64
+ const entries = [];
65
+ for (const field of selected) {
66
+ const explicit = explicitInclude && listed(field);
67
+ if (field.family === "pushbutton" || field.family === "signature") {
68
+ if (explicit) diagnose({
69
+ code: "submit-entry-unsupported",
70
+ message: `submit: '${field.name}' (${field.family}) has no representable submission value in v1 — skipped`
71
+ });
72
+ continue;
73
+ }
74
+ if (field.flags.noExport) {
75
+ if (explicit) diagnose({
76
+ code: "submit-entry-unsupported",
77
+ message: `submit: '${field.name}' carries the NoExport flag — the veto is unconditional (ISO 12.7.6.2), skipped`
78
+ });
79
+ continue;
80
+ }
81
+ const entry = field.valueEntry;
82
+ if (entry.kind === "scalar") entries.push({
83
+ name: field.name,
84
+ value: entry.value
85
+ });
86
+ else if (entry.kind === "array") entries.push({
87
+ name: field.name,
88
+ value: entry.values
89
+ });
90
+ else if (entry.kind === "none") {
91
+ if (intent.includeNoValueFields) entries.push({
92
+ name: field.name,
93
+ value: null
94
+ });
95
+ } else diagnose({
96
+ code: "submit-entry-unsupported",
97
+ message: `submit: '${field.name}' has an unsupported /V shape — skipped (would otherwise be silent data loss)`
98
+ });
99
+ }
100
+ return entries;
101
+ };
102
+ //#endregion
7
103
  //#region src/core/model.ts
8
104
  const fieldKeyOf = (field) => field.fieldObjectNumber > 0 ? `obj:${field.fieldObjectNumber}` : `fqn:${field.name}`;
9
105
  const initialModel = () => ({
@@ -217,507 +313,8 @@ function fillItemForWidget(model, annotObjectNumber) {
217
313
  }
218
314
  //#endregion
219
315
  //#region src/mutationQueue.ts
220
- /** Small promise-tail queue: failures never poison later mutations. */
221
- function createSerialMutationQueue() {
222
- let tail = Promise.resolve();
223
- return (operation) => {
224
- const result = tail.then(operation, operation);
225
- tail = result.then(() => void 0, () => void 0);
226
- return result;
227
- };
228
- }
229
- //#endregion
230
- //#region src/scripting.ts
231
- const refKey = (ref) => ref.kind === "objectNumber" ? `obj:${ref.fieldObjectNumber}` : `fqn:${ref.name}`;
232
- const sameRef = (left, right) => refKey(left) === refKey(right);
233
- const cloneValue = (value) => Array.isArray(value) ? [...value] : value;
234
- const sameValue = (left, right) => Array.isArray(left) && Array.isArray(right) ? left.length === right.length && left.every((value, index) => value === right[index]) : left === right;
235
- function cloneFields(fields) {
236
- return fields.map((field) => ({
237
- ...field,
238
- ref: { ...field.ref },
239
- value: cloneValue(field.value),
240
- defaultValue: cloneValue(field.defaultValue),
241
- ...field.options ? { options: field.options.map((option) => ({ ...option })) } : {}
242
- }));
243
- }
244
- function fieldByRef(fields, ref) {
245
- return fields.find((field) => sameRef(field.ref, ref));
246
- }
247
- function snapshotField(snapshot, ref) {
248
- return snapshot.fields.find((field) => sameRef(field.ref, ref));
249
- }
250
- function scriptValueFromFormValue(field, value) {
251
- if (field.family === "text" && value.type === "text") return value.value;
252
- if ((field.family === "checkbox" || field.family === "radio") && value.type === "toggle") return value.state ?? "Off";
253
- if (field.family === "combobox" && value.type === "choice") return value.values[0] ?? "";
254
- if (field.family === "listbox" && value.type === "choice") return [...value.values];
255
- throw new Error(`Form value type '${value.type}' does not match field family '${field.family}'`);
256
- }
257
- function formValueFromScriptValue(field) {
258
- switch (field.family) {
259
- case "text": return {
260
- type: "text",
261
- value: String(field.value ?? "")
262
- };
263
- case "checkbox":
264
- case "radio": return {
265
- type: "toggle",
266
- state: field.value === null || field.value === void 0 || String(field.value) === "Off" ? null : String(field.value)
267
- };
268
- case "combobox": return {
269
- type: "choice",
270
- values: Array.isArray(field.value) ? field.value.slice(0, 1) : [String(field.value ?? "")]
271
- };
272
- case "listbox": return {
273
- type: "choice",
274
- values: Array.isArray(field.value) ? [...field.value] : [String(field.value ?? "")]
275
- };
276
- default: return null;
277
- }
278
- }
279
- function applyEffects(overlay, effects) {
280
- for (const effect of effects) {
281
- if (effect.kind === "reset") {
282
- for (const ref of effect.refs) {
283
- const field = fieldByRef(overlay.fields, ref);
284
- if (!field) continue;
285
- field.value = cloneValue(field.defaultValue);
286
- overlay.resetKeys.add(refKey(ref));
287
- }
288
- continue;
289
- }
290
- const field = fieldByRef(overlay.fields, effect.ref);
291
- if (!field) continue;
292
- const key = refKey(effect.ref);
293
- if (effect.kind === "setValue") {
294
- field.value = scriptValueFromFormValue(field, effect.value);
295
- overlay.resetKeys.delete(key);
296
- } else if (effect.kind === "setDisplay") field.display = effect.display;
297
- else overlay.appearances.set(key, {
298
- ref: effect.ref,
299
- text: effect.text
300
- });
301
- }
302
- }
303
- function canonicalEffects(overlay) {
304
- const effects = [];
305
- const resets = [];
306
- for (const field of overlay.fields) {
307
- const original = fieldByRef(overlay.original, field.ref);
308
- if (!original) continue;
309
- if (!sameValue(original.value, field.value)) {
310
- const key = refKey(field.ref);
311
- if (overlay.resetKeys.has(key) && sameValue(field.value, field.defaultValue)) resets.push(field.ref);
312
- else {
313
- const value = formValueFromScriptValue(field);
314
- if (value) effects.push({
315
- kind: "setValue",
316
- ref: field.ref,
317
- value
318
- });
319
- }
320
- }
321
- if (original.display !== field.display) effects.push({
322
- kind: "setDisplay",
323
- ref: field.ref,
324
- display: field.display
325
- });
326
- }
327
- if (resets.length > 0) effects.unshift({
328
- kind: "reset",
329
- refs: resets
330
- });
331
- effects.push(...Array.from(overlay.appearances.values(), ({ ref, text }) => ({
332
- kind: "setAppearanceText",
333
- ref,
334
- text
335
- })));
336
- return effects;
337
- }
338
- function scriptError(message) {
339
- return {
340
- kind: "invalid-output",
341
- message
342
- };
343
- }
344
- function utf8Length(value) {
345
- let length = 0;
346
- for (let index = 0; index < value.length; index += 1) {
347
- const code = value.charCodeAt(index);
348
- if (code < 128) length += 1;
349
- else if (code < 2048) length += 2;
350
- else if (code >= 55296 && code <= 56319 && index + 1 < value.length) {
351
- length += 4;
352
- index += 1;
353
- } else length += 3;
354
- }
355
- return length;
356
- }
357
- function seedFrom(documentId, sequence) {
358
- let hash = 2166136261;
359
- for (const char of `${documentId}:${sequence}`) {
360
- hash ^= char.charCodeAt(0);
361
- hash = Math.imul(hash, 16777619);
362
- }
363
- return hash >>> 0;
364
- }
365
- function resolvedIdentity(doc, config) {
366
- const claims = doc.security.identity;
367
- const supplied = typeof config.identity === "function" ? config.identity() : config.identity ?? {};
368
- return {
369
- name: supplied.name ?? claims?.display_name ?? claims?.user_id ?? "",
370
- loginName: supplied.loginName ?? claims?.user_id ?? "",
371
- corporation: supplied.corporation ?? claims?.group_id ?? "",
372
- email: supplied.email ?? ""
373
- };
374
- }
375
- function pageNumberFor(meta, field) {
376
- const pon = field.widgets.find((widget) => widget.pageObjectNumber > 0)?.pageObjectNumber;
377
- if (!meta || pon === void 0) return 0;
378
- const index = meta.pages.findIndex((page) => page.pageObjectNumber === pon);
379
- return Math.max(0, index);
380
- }
381
- function statusFromEffects(result) {
382
- if (result.results.some(({ status }) => status === "failed" || status === "skipped")) return "failed";
383
- if (result.results.some(({ status }) => status === "rejected")) return "failed";
384
- return result.results.some(({ status }) => status === "applied") ? "applied" : "unchanged";
385
- }
386
- var FormScriptingController = class {
387
- constructor(options) {
388
- this.options = options;
389
- this.sandboxPromise = null;
390
- this.booted = false;
391
- this.disposed = false;
392
- this.sequence = 0;
393
- }
394
- dispose() {
395
- if (this.disposed) return;
396
- this.disposed = true;
397
- this.sandboxPromise?.then((sandbox) => sandbox.dispose()).catch(() => void 0);
398
- }
399
- async commit(snapshot, ref, proposed) {
400
- return this.transact(snapshot, ref, proposed);
401
- }
402
- /** Execute one originating widget's activation action (`/A`, including `/Next`). */
403
- async activate(snapshot, ref, action) {
404
- return this.transact(snapshot, ref, void 0, action);
405
- }
406
- /** Run lazy document boot plus the `/CO` chain without a user field change. */
407
- async recalculate(snapshot) {
408
- const target = snapshot.calculationOrder.find((ref) => ref !== null && snapshotField(snapshot, ref) !== void 0) ?? snapshot.fields[0]?.ref;
409
- if (!target) return {
410
- status: "unchanged",
411
- scripted: true,
412
- effectsResult: null,
413
- uiEffects: [],
414
- diagnostics: []
415
- };
416
- return this.transact(snapshot, target);
417
- }
418
- async transact(snapshot, ref, proposed, activation) {
419
- if (this.disposed) throw new Error("Form scripting controller is disposed");
420
- const target = snapshotField(snapshot, ref);
421
- if (!target) return this.failed([], [], scriptError(`Form field '${refKey(ref)}' no longer exists`));
422
- const original = (0, _embedpdf_core_acrojs.scriptFieldsFromSnapshot)(snapshot);
423
- const overlay = {
424
- original,
425
- fields: cloneFields(original),
426
- resetKeys: /* @__PURE__ */ new Set(),
427
- appearances: /* @__PURE__ */ new Map()
428
- };
429
- const targetInput = fieldByRef(overlay.fields, ref);
430
- if (!targetInput) return this.failed([], [], scriptError(`Form field '${refKey(ref)}' has no script view`));
431
- const hasProposedValue = proposed !== void 0;
432
- let proposedValue = cloneValue(targetInput.value);
433
- if (proposed) try {
434
- proposedValue = scriptValueFromFormValue(targetInput, proposed);
435
- } catch (error) {
436
- return this.failed([], [], scriptError(error instanceof Error ? error.message : String(error)));
437
- }
438
- const uiEffects = [];
439
- const diagnostics = [];
440
- const meta = this.options.document();
441
- const nowMs = this.options.config.now?.() ?? Date.now();
442
- const inputBase = {
443
- document: {
444
- id: this.options.doc.id,
445
- fileName: this.options.config.fileName?.() ?? meta?.name ?? this.options.doc.id,
446
- pageCount: meta?.pageCount ?? 0,
447
- pageNumber: pageNumberFor(meta, target)
448
- },
449
- identity: resolvedIdentity(this.options.doc, this.options.config),
450
- environment: {
451
- nowMs,
452
- utcOffsetMinutes: this.options.config.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
453
- randomSeed: this.options.config.randomSeed?.() ?? seedFrom(this.options.doc.id, this.sequence++)
454
- }
455
- };
456
- const budget = this.options.config.budget ?? _embedpdf_core_acrojs.DEFAULT_SCRIPT_BUDGET;
457
- let executionMs = 0;
458
- let aggregateOutputSize = 0;
459
- const consume = (output, phase) => {
460
- aggregateOutputSize += utf8Length(JSON.stringify(output));
461
- uiEffects.push(...output.uiEffects.map((effect) => ({
462
- ...effect,
463
- phase
464
- })));
465
- diagnostics.push(...output.diagnostics);
466
- if (aggregateOutputSize > budget.maxOutputBytes) return {
467
- kind: "budget",
468
- message: `Form script transaction output exceeded ${budget.maxOutputBytes} bytes`
469
- };
470
- return output.error ?? null;
471
- };
472
- const remainingBudget = () => {
473
- const remaining = budget.maxExecutionMs - executionMs;
474
- return remaining <= 0 ? null : {
475
- ...budget,
476
- maxExecutionMs: remaining
477
- };
478
- };
479
- const run = async (kind, source, input) => {
480
- const remaining = remainingBudget();
481
- if (!remaining) return { error: {
482
- kind: "budget",
483
- message: "Form script transaction timed out"
484
- } };
485
- let output;
486
- try {
487
- const sandbox = await this.sandbox();
488
- const startedAt = Date.now();
489
- output = kind === "boot" ? sandbox.boot(source, input, remaining) : sandbox.run(source, input, remaining);
490
- executionMs += Date.now() - startedAt;
491
- } catch (error) {
492
- return { error: scriptError(error instanceof Error ? error.message : String(error)) };
493
- }
494
- const error = consume(output, kind === "boot" ? "boot" : "user");
495
- return error ? { error } : { output };
496
- };
497
- if (!this.booted) {
498
- this.booted = true;
499
- let sources = [];
500
- let bootError = null;
501
- try {
502
- sources = (this.options.doc.actions ? await this.options.doc.actions.read() : null)?.nameTreeScripts.map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action)) ?? [];
503
- } catch (error) {
504
- bootError = scriptError(error instanceof Error ? error.message : String(error));
505
- }
506
- if (!bootError && sources.length > 0) {
507
- const boot = await run("boot", sources, {
508
- ...inputBase,
509
- fields: overlay.fields,
510
- event: { kind: "name-tree-boot" }
511
- });
512
- if (boot.error) bootError = boot.error;
513
- else applyEffects(overlay, boot.output.formEffects);
514
- }
515
- if (bootError) diagnostics.push({
516
- code: "script-error",
517
- message: `Document boot script failed (continuing without it): ${bootError.message}`
518
- });
519
- }
520
- const bootEffects = canonicalEffects(overlay);
521
- if (activation) {
522
- let program;
523
- try {
524
- program = (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(activation);
525
- } catch (error) {
526
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
527
- }
528
- if (program) {
529
- const activated = await run("run", program, {
530
- ...inputBase,
531
- fields: overlay.fields,
532
- event: {
533
- kind: "widget-activate",
534
- target: ref,
535
- source: ref,
536
- value: targetInput.value
537
- }
538
- });
539
- if (activated.error) return this.failed(uiEffects, diagnostics, activated.error);
540
- applyEffects(overlay, activated.output.formEffects);
541
- }
542
- }
543
- if (!activation && hasProposedValue && target.actions?.keystroke && target.family === "text") {
544
- let program;
545
- try {
546
- program = (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(target.actions.keystroke);
547
- } catch (error) {
548
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
549
- }
550
- if (program) {
551
- const oldValue = String(targetInput.value ?? "");
552
- const key = await run("run", program, {
553
- ...inputBase,
554
- fields: overlay.fields,
555
- event: {
556
- kind: "field-keystroke",
557
- target: ref,
558
- source: ref,
559
- value: oldValue,
560
- change: String(proposedValue ?? ""),
561
- selStart: 0,
562
- selEnd: oldValue.length,
563
- willCommit: true
564
- }
565
- });
566
- if (key.error) return this.failed(uiEffects, diagnostics, key.error);
567
- if (!key.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
568
- applyEffects(overlay, key.output.formEffects);
569
- const event = key.output.event;
570
- const base = String(event.value ?? "");
571
- proposedValue = base.slice(0, event.selStart) + event.change + base.slice(Math.max(event.selStart, event.selEnd));
572
- }
573
- }
574
- if (!activation && hasProposedValue) {
575
- targetInput.value = cloneValue(proposedValue);
576
- overlay.resetKeys.delete(refKey(ref));
577
- }
578
- if (!activation && hasProposedValue && target.actions?.validate) {
579
- let program;
580
- try {
581
- program = (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(target.actions.validate);
582
- } catch (error) {
583
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
584
- }
585
- if (program) {
586
- const validation = await run("run", program, {
587
- ...inputBase,
588
- fields: overlay.fields,
589
- event: {
590
- kind: "field-validate",
591
- target: ref,
592
- source: ref,
593
- value: proposedValue,
594
- willCommit: true
595
- }
596
- });
597
- if (validation.error) return this.failed(uiEffects, diagnostics, validation.error);
598
- if (!validation.output.event.rc) return this.finishRejected(bootEffects, uiEffects, diagnostics);
599
- targetInput.value = cloneValue(validation.output.event.value);
600
- applyEffects(overlay, validation.output.formEffects);
601
- }
602
- }
603
- const formatRefs = !activation && hasProposedValue ? [ref] : [];
604
- for (const calculationRef of activation ? [] : snapshot.calculationOrder) {
605
- if (!calculationRef) continue;
606
- const calculated = snapshotField(snapshot, calculationRef);
607
- const tree = calculated?.actions?.calculate;
608
- if (!calculated || !tree) continue;
609
- let program;
610
- try {
611
- program = (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(tree);
612
- } catch (error) {
613
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
614
- }
615
- const calculatedInput = fieldByRef(overlay.fields, calculationRef);
616
- if (program && calculatedInput) {
617
- const calculation = await run("run", program, {
618
- ...inputBase,
619
- fields: overlay.fields,
620
- event: {
621
- kind: "field-calculate",
622
- target: calculationRef,
623
- ...hasProposedValue ? { source: ref } : {},
624
- value: calculatedInput.value
625
- }
626
- });
627
- if (calculation.error) return this.failed(uiEffects, diagnostics, calculation.error);
628
- applyEffects(overlay, calculation.output.formEffects);
629
- }
630
- if (!formatRefs.some((candidate) => sameRef(candidate, calculationRef))) formatRefs.push(calculationRef);
631
- }
632
- for (const formatRef of formatRefs) {
633
- const field = snapshotField(snapshot, formatRef);
634
- const tree = field?.actions?.format;
635
- const overlayField = fieldByRef(overlay.fields, formatRef);
636
- if (!field || !tree || !overlayField) continue;
637
- let program;
638
- try {
639
- program = (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(tree);
640
- } catch (error) {
641
- return this.failed(uiEffects, diagnostics, scriptError(error instanceof Error ? error.message : String(error)));
642
- }
643
- if (!program) continue;
644
- const format = await run("run", program, {
645
- ...inputBase,
646
- fields: overlay.fields,
647
- event: {
648
- kind: "field-format",
649
- target: formatRef,
650
- ...hasProposedValue ? { source: ref } : {},
651
- value: overlayField.value
652
- }
653
- });
654
- if (format.error) return this.failed(uiEffects, diagnostics, format.error);
655
- applyEffects(overlay, format.output.formEffects);
656
- }
657
- const effects = canonicalEffects(overlay);
658
- if (effects.length + uiEffects.length > budget.maxEffects) return this.failed(uiEffects, diagnostics, {
659
- kind: "budget",
660
- message: `Form script transaction exceeded ${budget.maxEffects} effects`
661
- });
662
- if (effects.length === 0) return {
663
- status: "unchanged",
664
- scripted: true,
665
- effectsResult: null,
666
- uiEffects,
667
- diagnostics
668
- };
669
- if (!this.options.doc.forms.applyEffects) return this.failed(uiEffects, diagnostics, scriptError("This engine does not support batched form effects"));
670
- const effectsResult = await this.options.doc.forms.applyEffects(effects);
671
- const status = statusFromEffects(effectsResult);
672
- return {
673
- status,
674
- scripted: true,
675
- effectsResult,
676
- uiEffects,
677
- diagnostics,
678
- ...status === "failed" ? { error: scriptError("One or more native form effects failed") } : {}
679
- };
680
- }
681
- async sandbox() {
682
- this.sandboxPromise ?? (this.sandboxPromise = this.options.sandboxFactory().then((sandbox) => {
683
- if (this.disposed) sandbox.dispose();
684
- return sandbox;
685
- }));
686
- return this.sandboxPromise;
687
- }
688
- failed(uiEffects, diagnostics, error) {
689
- return {
690
- status: "failed",
691
- scripted: true,
692
- effectsResult: null,
693
- uiEffects,
694
- diagnostics,
695
- error
696
- };
697
- }
698
- async finishRejected(bootEffects, uiEffects, diagnostics) {
699
- if (bootEffects.length === 0) return {
700
- status: "rejected",
701
- scripted: true,
702
- effectsResult: null,
703
- uiEffects,
704
- diagnostics
705
- };
706
- if (!this.options.doc.forms.applyEffects) return this.failed(uiEffects, diagnostics, scriptError("Batched form effects are unavailable"));
707
- const effectsResult = await this.options.doc.forms.applyEffects(bootEffects);
708
- if (statusFromEffects(effectsResult) === "failed") return this.failed(uiEffects, diagnostics, scriptError("A name-tree boot effect failed"));
709
- return {
710
- status: "rejected",
711
- scripted: true,
712
- effectsResult,
713
- uiEffects,
714
- diagnostics
715
- };
716
- }
717
- };
718
- function createFormScriptingController(options) {
719
- return new FormScriptingController(options);
720
- }
316
+ /** The core serial queue under its historical form-plugin name. */
317
+ const createSerialMutationQueue = _embedpdf_core.createSerialQueue;
721
318
  //#endregion
722
319
  //#region src/capability.ts
723
320
  /** PDF user-space rect (y-up) → content-space box (y-down, crop-relative). */
@@ -757,34 +354,36 @@ function createFormCapability(ctx, options = {}) {
757
354
  };
758
355
  };
759
356
  const enqueueMutation = createSerialMutationQueue();
760
- const scripting = options.scripting?.enabled && ctx.doc ? createFormScriptingController({
357
+ const actionsHost = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
358
+ const scriptPort = actionsHost?.scriptTransaction?.bind(actionsHost) ?? null;
359
+ const scripting = scriptPort && ctx.doc ? require_scripting.createFormScriptingController({
761
360
  doc: ctx.doc,
762
361
  document: () => ctx.document(),
763
- config: options.scripting,
764
- sandboxFactory: options.scripting.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox()))
362
+ transaction: scriptPort
765
363
  }) : null;
766
364
  if (scripting) ctx.cleanup(() => scripting.dispose());
767
- let uiEffectProvider = null;
768
- const surfaceScriptingResult = (result) => {
769
- const observe = (callback) => {
770
- if (!callback) return;
771
- try {
772
- callback();
773
- } catch (error) {
774
- globalThis.console?.error("[form] scripting observer failed:", error);
775
- }
776
- };
777
- for (const effect of result.uiEffects) {
778
- observe(options.scripting?.onUiEffect ? () => options.scripting.onUiEffect(effect) : void 0);
779
- observe(uiEffectProvider ? () => uiEffectProvider(effect) : void 0);
365
+ /** Every script surface (UI effects, diagnostics, errors) flows through
366
+ * the actions plugin's ONE port — origin/phase attached (D9). */
367
+ const surfaceViaActions = (result, origin) => {
368
+ if (!actionsHost) return;
369
+ for (const phase of ["boot", "user"]) {
370
+ const uiEffects = result.uiEffects.filter((effect) => effect.phase === phase);
371
+ if (uiEffects.length === 0 && phase === "boot") continue;
372
+ actionsHost.surfaceScriptResult({
373
+ uiEffects,
374
+ diagnostics: phase === "user" ? result.diagnostics : [],
375
+ ...phase === "user" && result.error ? { error: result.error } : {},
376
+ origin,
377
+ phase
378
+ });
780
379
  }
781
- for (const diagnostic of result.diagnostics) observe(options.scripting?.onDiagnostic ? () => options.scripting.onDiagnostic(diagnostic) : void 0);
782
- if (result.error) observe(options.scripting?.onError ? () => options.scripting.onError(result.error) : void 0);
783
380
  };
381
+ const can = (cap) => ctx.doc?.security.allows(cap) ?? false;
784
382
  let refreshPromise = null;
785
383
  const refresh = async (force = false) => {
786
384
  const doc = ctx.doc;
787
385
  if (!doc) return;
386
+ if (!can("doc.forms.read")) return;
788
387
  if (refreshPromise) {
789
388
  await refreshPromise;
790
389
  if (!force) return;
@@ -792,7 +391,9 @@ function createFormCapability(ctx, options = {}) {
792
391
  refreshPromise = doc.forms.list().then((snapshot) => apply({
793
392
  t: "snapshot",
794
393
  snapshot
795
- })).finally(() => {
394
+ })).catch((err) => {
395
+ console.warn("[form] form snapshot failed to load", err);
396
+ }).finally(() => {
796
397
  refreshPromise = null;
797
398
  });
798
399
  return refreshPromise;
@@ -820,14 +421,20 @@ function createFormCapability(ctx, options = {}) {
820
421
  geomLoading.delete(pon);
821
422
  });
822
423
  };
424
+ const fuseFill = (item, fillable) => item === null || fillable || item.disabled ? item : {
425
+ ...item,
426
+ disabled: true
427
+ };
823
428
  const fillCache = /* @__PURE__ */ new Map();
824
429
  const fillItems$1 = (pon) => {
825
430
  const m = model();
431
+ const fillable = can("doc.forms.fill");
826
432
  const hit = fillCache.get(pon);
827
- if (hit && hit.seq === m.seq) return hit.items;
828
- const items = fillItems(m, pon);
433
+ if (hit && hit.seq === m.seq && hit.fillable === fillable) return hit.items;
434
+ const items = fillItems(m, pon).map((item) => fuseFill(item, fillable));
829
435
  fillCache.set(pon, {
830
436
  seq: m.seq,
437
+ fillable,
831
438
  items
832
439
  });
833
440
  return items;
@@ -835,11 +442,13 @@ function createFormCapability(ctx, options = {}) {
835
442
  const fillItemCache = /* @__PURE__ */ new Map();
836
443
  const fillItem = (annotObjectNumber) => {
837
444
  const m = model();
445
+ const fillable = can("doc.forms.fill");
838
446
  const hit = fillItemCache.get(annotObjectNumber);
839
- if (hit && hit.seq === m.seq) return hit.item;
840
- const item = fillItemForWidget(m, annotObjectNumber);
447
+ if (hit && hit.seq === m.seq && hit.fillable === fillable) return hit.item;
448
+ const item = fuseFill(fillItemForWidget(m, annotObjectNumber), fillable);
841
449
  fillItemCache.set(annotObjectNumber, {
842
450
  seq: m.seq,
451
+ fillable,
843
452
  item
844
453
  });
845
454
  return item;
@@ -848,8 +457,8 @@ function createFormCapability(ctx, options = {}) {
848
457
  const doc = ctx.doc;
849
458
  if (!doc) throw new Error("no document");
850
459
  if (scripting) {
851
- const result = await scripting.commit(await doc.forms.list(), ref, value);
852
- surfaceScriptingResult(result);
460
+ const result = await scripting.commit(ref, value);
461
+ surfaceViaActions(result, "user");
853
462
  if (result.effectsResult !== null) await refresh(true);
854
463
  return result;
855
464
  }
@@ -863,32 +472,34 @@ function createFormCapability(ctx, options = {}) {
863
472
  diagnostics: []
864
473
  };
865
474
  };
866
- const write = (key, value) => enqueueMutation(async () => {
867
- if (!ctx.doc) return;
868
- apply({
869
- t: "writeStart",
870
- key
871
- });
872
- try {
873
- const result = await commitValueNow(refKeyOf(key), value);
874
- if (result.status === "rejected" || result.status === "failed") apply({
875
- t: "writeFailed",
876
- key
877
- });
878
- else if (result.effectsResult === null && result.scripted) apply({
879
- t: "writeFailed",
880
- key
881
- });
882
- } catch (err) {
475
+ const write = (key, value) => {
476
+ if (!can("doc.forms.fill")) return Promise.reject(new _embedpdf_engine_core_runtime.PermissionDenied("doc.forms.fill", "form.fill"));
477
+ return enqueueMutation(async () => {
478
+ if (!ctx.doc) return;
883
479
  apply({
884
- t: "writeFailed",
480
+ t: "writeStart",
885
481
  key
886
482
  });
887
- throw err;
888
- }
889
- });
890
- const can = (cap) => ctx.doc?.security.allows(cap) ?? false;
891
- const annotationHost = ctx.tryGet(_embedpdf_plugin_annotation_internal.AnnotationToken);
483
+ try {
484
+ const result = await commitValueNow(refKeyOf(key), value);
485
+ if (result.status === "rejected" || result.status === "failed") apply({
486
+ t: "writeFailed",
487
+ key
488
+ });
489
+ else if (result.effectsResult === null && result.scripted) apply({
490
+ t: "writeFailed",
491
+ key
492
+ });
493
+ } catch (err) {
494
+ apply({
495
+ t: "writeFailed",
496
+ key
497
+ });
498
+ throw err;
499
+ }
500
+ });
501
+ };
502
+ const annotationHost = ctx.tryGet(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
892
503
  const annotationActivation = async (ref) => {
893
504
  const doc = ctx.doc;
894
505
  if (!doc) return null;
@@ -899,8 +510,7 @@ function createFormCapability(ctx, options = {}) {
899
510
  return annotation?.subtype === "widget" ? annotation.actions?.activate ?? null : null;
900
511
  };
901
512
  const activateWidgetNow = async (key, annotationRef) => {
902
- const doc = ctx.doc;
903
- if (!doc) throw new Error("no document");
513
+ if (!ctx.doc) throw new Error("no document");
904
514
  if (!scripting) return {
905
515
  status: "unchanged",
906
516
  scripted: false,
@@ -916,11 +526,118 @@ function createFormCapability(ctx, options = {}) {
916
526
  uiEffects: [],
917
527
  diagnostics: []
918
528
  };
919
- const result = await scripting.activate(await doc.forms.list(), refKeyOf(key), action);
920
- surfaceScriptingResult(result);
529
+ const result = await scripting.activate(refKeyOf(key), action);
530
+ surfaceViaActions(result, "user");
921
531
  if (result.effectsResult !== null) await refresh(true);
922
532
  return result;
923
533
  };
534
+ const NOT_SCRIPTED = {
535
+ status: "unchanged",
536
+ scripted: false,
537
+ effectsResult: null,
538
+ uiEffects: [],
539
+ diagnostics: []
540
+ };
541
+ /**
542
+ * The shared reset core: one effects batch → refresh → V/C/F
543
+ * recalculation when the transaction port is present. Ridden by BOTH
544
+ * doors — the /ResetForm executor and the public `reset(key)` — so the
545
+ * two can never diverge again (the "reset() asymmetry" fix). `origin` is
546
+ * PRESERVED through recalculation surfacing: a lifecycle/hover ResetForm
547
+ * can no longer launder its alerts into user origin.
548
+ */
549
+ const applyResetBatch = async (refs, origin) => {
550
+ const doc = ctx.doc;
551
+ if (!doc) throw new Error("no document");
552
+ if (!doc.forms.applyEffects) {
553
+ const result = {
554
+ ...NOT_SCRIPTED,
555
+ diagnostics: [{
556
+ code: "unsupported-api",
557
+ message: "this engine has no form-effects batch door"
558
+ }]
559
+ };
560
+ surfaceViaActions(result, origin);
561
+ return result;
562
+ }
563
+ if (refs.length === 0) return NOT_SCRIPTED;
564
+ const effectsResult = await doc.forms.applyEffects([{
565
+ kind: "reset",
566
+ refs
567
+ }]);
568
+ const resetFailed = effectsResult.results.some((entry) => entry.status === "failed" || entry.status === "rejected");
569
+ await refresh(true);
570
+ if (resetFailed) return {
571
+ status: "failed",
572
+ scripted: false,
573
+ effectsResult,
574
+ uiEffects: [],
575
+ diagnostics: []
576
+ };
577
+ let recalc = null;
578
+ if (scripting) {
579
+ recalc = await scripting.recalculate();
580
+ surfaceViaActions(recalc, origin);
581
+ if (recalc.effectsResult !== null) await refresh(true);
582
+ }
583
+ return {
584
+ status: "applied",
585
+ scripted: recalc !== null,
586
+ effectsResult,
587
+ uiEffects: recalc?.uiEffects ?? [],
588
+ diagnostics: recalc?.diagnostics ?? [],
589
+ ...recalc?.error ? { error: recalc.error } : {}
590
+ };
591
+ };
592
+ const resetFormAction = (targets, exclude, origin = "user") => enqueueMutation(async () => {
593
+ const doc = ctx.doc;
594
+ if (!doc) throw new Error("no document");
595
+ const { selected } = resolveFieldSelection((await doc.forms.list()).fields, targets, exclude);
596
+ const resettable = selected.filter((f) => f.family !== "pushbutton" && f.family !== "signature");
597
+ return applyResetBatch(resettable.map((f) => f.ref), origin);
598
+ });
599
+ /**
600
+ * The submit dataset resolver (Phase 4, D7): a FRESH engine read — never
601
+ * the cached model, so no staleness class exists — then the pure ISO
602
+ * builder. Selection/veto/value semantics live in `field-selection.ts`;
603
+ * this door only supplies the live fields and assembles the request.
604
+ */
605
+ const resolveSubmitDataset = async (intent, actionCtx, diagnose) => {
606
+ const doc = ctx.doc;
607
+ if (!doc) throw new Error("no document");
608
+ const snapshot = await doc.forms.list();
609
+ return {
610
+ url: intent.url,
611
+ method: intent.method,
612
+ format: intent.format,
613
+ flagsRaw: intent.flagsRaw,
614
+ ...intent.charSet === void 0 ? {} : { charSet: intent.charSet },
615
+ entries: buildSubmitEntries(snapshot.fields, intent, diagnose),
616
+ origin: actionCtx.origin,
617
+ event: actionCtx.event
618
+ };
619
+ };
620
+ const widgetSource = (key, annotationRef) => ({
621
+ kind: "widget",
622
+ field: refKeyOf(key),
623
+ annotation: annotationRef,
624
+ pon: annotationRef.pageObjectNumber
625
+ });
626
+ /** One shared hover pump (Exit→Enter as one ordered pair; intermediates
627
+ * skipped), created on first use against the resolved actions capability. */
628
+ let widgetPump = null;
629
+ const widgetHoverPump = (actions) => {
630
+ widgetPump ?? (widgetPump = (0, _embedpdf_plugin_actions_contract.createHoverPump)(actions.dispatch));
631
+ return widgetPump;
632
+ };
633
+ const widgetHoverFlags = (annotationRef) => {
634
+ const loaded = annotationHost?.get(annotationRef);
635
+ if (loaded?.subtype !== "widget") return null;
636
+ return {
637
+ enter: Boolean(loaded.actions?.cursorEnter?.root),
638
+ exit: Boolean(loaded.actions?.cursorExit?.root)
639
+ };
640
+ };
924
641
  const nudgeAnnotations = (pons) => {
925
642
  if (!annotationHost) return;
926
643
  for (const pon of new Set(pons)) annotationHost.reloadPage(pon);
@@ -1064,32 +781,137 @@ function createFormCapability(ctx, options = {}) {
1064
781
  type: "choice",
1065
782
  values
1066
783
  }),
1067
- reset: (key) => enqueueMutation(async () => {
1068
- const doc = ctx.doc;
1069
- if (!doc) return;
1070
- apply({
1071
- t: "writeStart",
1072
- key
1073
- });
1074
- try {
1075
- const result = await doc.forms.reset(refKeyOf(key));
784
+ reset: (key) => {
785
+ if (!can("doc.forms.fill")) return Promise.reject(new _embedpdf_engine_core_runtime.PermissionDenied("doc.forms.fill", "form.reset"));
786
+ return enqueueMutation(async () => {
787
+ const doc = ctx.doc;
788
+ if (!doc) return;
1076
789
  apply({
1077
- t: "writeDone",
1078
- key,
1079
- field: result.field
1080
- });
1081
- } catch (err) {
1082
- apply({
1083
- t: "writeFailed",
790
+ t: "writeStart",
1084
791
  key
1085
792
  });
1086
- throw err;
1087
- }
1088
- }),
793
+ try {
794
+ if (doc.forms.applyEffects) {
795
+ const result = await applyResetBatch([refKeyOf(key)], "user");
796
+ if (result.status === "failed") throw new Error(result.effectsResult?.results.find((entry) => entry.status === "failed" || entry.status === "rejected")?.error?.message ?? "reset failed");
797
+ const field = fieldByKey(model(), key);
798
+ if (field) apply({
799
+ t: "writeDone",
800
+ key,
801
+ field
802
+ });
803
+ else apply({
804
+ t: "writeFailed",
805
+ key
806
+ });
807
+ } else {
808
+ const result = await doc.forms.reset(refKeyOf(key));
809
+ apply({
810
+ t: "writeDone",
811
+ key,
812
+ field: result.field
813
+ });
814
+ }
815
+ } catch (err) {
816
+ apply({
817
+ t: "writeFailed",
818
+ key
819
+ });
820
+ throw err;
821
+ }
822
+ });
823
+ },
1089
824
  commitValue: (ref, value) => enqueueMutation(() => commitValueNow(ref, value)),
1090
- activateWidget: (key, annotationRef) => enqueueMutation(() => activateWidgetNow(key, annotationRef)),
1091
- setUiEffectProvider: (provider) => {
1092
- uiEffectProvider = provider;
825
+ activateWidget: async (key, annotationRef) => {
826
+ const actions = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
827
+ if (actions) {
828
+ const result = await actions.dispatch({
829
+ scope: "activate",
830
+ ref: annotationRef,
831
+ pon: annotationRef.pageObjectNumber,
832
+ source: widgetSource(key, annotationRef)
833
+ });
834
+ if (!(result.status === "inert" && result.steps.length === 0 && result.diagnostics.length === 0)) return {
835
+ kind: "dispatched",
836
+ result
837
+ };
838
+ }
839
+ return {
840
+ kind: "form",
841
+ result: await enqueueMutation(() => activateWidgetNow(key, annotationRef))
842
+ };
843
+ },
844
+ notifyWidgetEvent: (key, annotationRef, event) => {
845
+ const actions = ctx.tryGet(_embedpdf_plugin_actions_contract.ActionsToken);
846
+ if (!actions) return;
847
+ const source = widgetSource(key, annotationRef);
848
+ if (event === "cursorEnter" || event === "cursorExit") {
849
+ if (event === "cursorExit") {
850
+ widgetHoverPump(actions).hover(null);
851
+ return;
852
+ }
853
+ const flags = widgetHoverFlags(annotationRef);
854
+ if (flags && !flags.enter && !flags.exit) return;
855
+ widgetHoverPump(actions).hover({
856
+ ref: annotationRef,
857
+ pon: annotationRef.pageObjectNumber,
858
+ source,
859
+ ...flags ? { events: flags } : {}
860
+ });
861
+ return;
862
+ }
863
+ actions.dispatch({
864
+ scope: "annotation",
865
+ event,
866
+ ref: annotationRef,
867
+ pon: annotationRef.pageObjectNumber,
868
+ source
869
+ });
870
+ },
871
+ resetFormAction,
872
+ resolveSubmitDataset,
873
+ commitScriptFormEffects: async (effects) => {
874
+ const doc = ctx.doc;
875
+ if (!doc?.forms.applyEffects) return {
876
+ results: effects.map((_, index) => ({
877
+ index,
878
+ status: "failed",
879
+ fields: [],
880
+ changedWidgets: [],
881
+ error: {
882
+ code: "NotSupported",
883
+ message: "no form-effects batch door"
884
+ }
885
+ })),
886
+ changedWidgets: [],
887
+ meta: null
888
+ };
889
+ let result;
890
+ try {
891
+ result = await doc.forms.applyEffects(effects);
892
+ } catch (error) {
893
+ const message = error instanceof Error ? error.message : String(error);
894
+ return {
895
+ results: effects.map((_, index) => ({
896
+ index,
897
+ status: "failed",
898
+ fields: [],
899
+ changedWidgets: [],
900
+ error: {
901
+ code: "Refused",
902
+ message
903
+ }
904
+ })),
905
+ changedWidgets: [],
906
+ meta: null
907
+ };
908
+ }
909
+ await refresh(true);
910
+ if (annotationHost) {
911
+ const pons = new Set(result.changedWidgets.map((widget) => widget.pageObjectNumber).filter((pon) => pon > 0));
912
+ for (const pon of pons) await annotationHost.reloadPage(pon);
913
+ }
914
+ return result;
1093
915
  },
1094
916
  setValue: (ref, value) => enqueueMutation(async () => {
1095
917
  const doc = ctx.doc;
@@ -1125,8 +947,9 @@ function createFormCapability(ctx, options = {}) {
1125
947
  updateField: (key, patch) => enqueueMutation(() => updateFieldNow(key, patch)),
1126
948
  deleteField: (key) => enqueueMutation(() => deleteFieldNow(key)),
1127
949
  detachWidget: (key, annotObjectNumber) => enqueueMutation(() => detachWidgetNow(key, annotObjectNumber)),
950
+ canRead: () => can("doc.forms.read"),
1128
951
  canFill: () => can("doc.forms.fill"),
1129
- canModify: () => can("doc.forms.modify")
952
+ canDesign: () => can("doc.forms.modify")
1130
953
  };
1131
954
  }
1132
955
  //#endregion
@@ -1160,7 +983,11 @@ function registerFormEffects(ctx) {
1160
983
  }
1161
984
  };
1162
985
  const unsubscribe = doc.events.subscribe((event) => {
1163
- if (!event.type.startsWith("form.")) return;
986
+ if (event.type === "stream.desynced") {
987
+ refresh();
988
+ return;
989
+ }
990
+ if (!event.type.startsWith("form.") || !("origin" in event)) return;
1164
991
  const structural = STRUCTURAL.has(event.type);
1165
992
  if (event.origin.kind !== "remote" && !structural) return;
1166
993
  if (structural) ctx.dispatch({
@@ -1276,7 +1103,7 @@ function createPlaceHandler(form, interaction, annotation) {
1276
1103
  enabledFor: (t) => t.enables.has("form-place"),
1277
1104
  onDown: (s) => {
1278
1105
  if (!s.page || !FORM_TOOL_BY_ID.has(interaction.activeToolId())) return false;
1279
- if (!form.canModify()) return false;
1106
+ if (!form.canDesign()) return false;
1280
1107
  origin = {
1281
1108
  pon: s.page.pon,
1282
1109
  start: s.page.point,
@@ -1286,11 +1113,11 @@ function createPlaceHandler(form, interaction, annotation) {
1286
1113
  },
1287
1114
  onMove: (s) => {
1288
1115
  if (!origin) return;
1289
- const point = (0, _embedpdf_plugin_interaction.samplePointOn)(s, origin.pon);
1116
+ const point = (0, _embedpdf_plugin_interaction_contract.samplePointOn)(s, origin.pon);
1290
1117
  if (!point) return;
1291
1118
  origin.last = point;
1292
1119
  const box = rectFrom(origin.start, point);
1293
- if (annotation) if (Math.max(box.width, box.height) >= _embedpdf_plugin_annotation.MIN_DRAG) annotation.setPlacementPreview(interaction.activeToolId(), origin.pon, box);
1120
+ if (annotation) if (Math.max(box.width, box.height) >= _embedpdf_plugin_annotation_authoring.MIN_DRAG) annotation.setPlacementPreview(interaction.activeToolId(), origin.pon, box);
1294
1121
  else annotation.clearPlacementPreview();
1295
1122
  },
1296
1123
  onUp: (s) => {
@@ -1301,16 +1128,16 @@ function createPlaceHandler(form, interaction, annotation) {
1301
1128
  const toolId = interaction.activeToolId();
1302
1129
  const tool = FORM_TOOL_BY_ID.get(toolId);
1303
1130
  if (!tool) return;
1304
- const end = (0, _embedpdf_plugin_interaction.samplePointOn)(s, o.pon) ?? o.last;
1131
+ const end = (0, _embedpdf_plugin_interaction_contract.samplePointOn)(s, o.pon) ?? o.last;
1305
1132
  const dragged = rectFrom(o.start, end);
1306
- const isClick = dragged.width < _embedpdf_plugin_annotation.MIN_DRAG && dragged.height < _embedpdf_plugin_annotation.MIN_DRAG;
1133
+ const isClick = dragged.width < _embedpdf_plugin_annotation_authoring.MIN_DRAG && dragged.height < _embedpdf_plugin_annotation_authoring.MIN_DRAG;
1307
1134
  const pageBox = form.pageBox(o.pon);
1308
1135
  const box = isClick ? boxOfClick(o.start, tool.clickCreate, pageBox) : dragged;
1309
1136
  form.placeField({
1310
1137
  family: tool.family,
1311
1138
  pageObjectNumber: o.pon,
1312
1139
  box,
1313
- appearance: (0, _embedpdf_plugin_annotation.widgetAppearanceFromProps)(annotation ? annotation.currentDefaults(toolId) : tool.defaults)
1140
+ appearance: (0, _embedpdf_plugin_annotation_authoring.widgetAppearanceFromProps)(annotation ? annotation.currentDefaults(toolId) : tool.defaults)
1314
1141
  }).then((placed) => {
1315
1142
  if (!annotation || interaction.activeToolId() !== toolId) return;
1316
1143
  const widget = placed.widget;
@@ -1329,7 +1156,7 @@ function createPlaceHandler(form, interaction, annotation) {
1329
1156
  /** The click-create box through the SHARED placement layer (fields are boxes;
1330
1157
  * the policy anchor + page clamp are exactly the annotation click's). */
1331
1158
  function boxOfClick(point, policy, pageBox) {
1332
- const placement = (0, _embedpdf_plugin_annotation.resolveClickPlacement)(point, policy, { pageBox: pageBox ?? void 0 });
1159
+ const placement = (0, _embedpdf_plugin_annotation_authoring.resolveClickPlacement)(point, policy, { pageBox: pageBox ?? void 0 });
1333
1160
  return placement.kind === "box" ? placement.rect : {
1334
1161
  x: point.x,
1335
1162
  y: point.y,
@@ -1343,9 +1170,6 @@ const initialFormState = () => ({ model: initialModel() });
1343
1170
  /** Dumb store: the pure core computes the next model; the shell dispatches it. */
1344
1171
  const formReducer = (state, action) => action.type === "SET_MODEL" ? { model: action.model } : state;
1345
1172
  //#endregion
1346
- //#region src/types.ts
1347
- const FormToken = (0, _embedpdf_core.createCapabilityToken)("form");
1348
- //#endregion
1349
1173
  //#region src/form.plugin.ts
1350
1174
  /**
1351
1175
  * The form plugin: the FIELD plane. Document-scoped; requires the
@@ -1360,17 +1184,17 @@ const FormToken = (0, _embedpdf_core.createCapabilityToken)("form");
1360
1184
  */
1361
1185
  const formPlugin = (options = {}) => (0, _embedpdf_core.definePlugin)({
1362
1186
  id: "form",
1363
- token: FormToken,
1187
+ token: require_types.FormToken,
1364
1188
  scope: "document",
1365
- requires: [_embedpdf_plugin_interaction.InteractionToken],
1366
- optional: [_embedpdf_plugin_annotation.AnnotationToken],
1189
+ requires: [_embedpdf_plugin_interaction_contract.InteractionToken],
1190
+ optional: [_embedpdf_plugin_annotation_contract.AnnotationToken, _embedpdf_plugin_actions_contract.ActionsToken],
1367
1191
  initialState: initialFormState,
1368
1192
  reduce: formReducer,
1369
1193
  capability: (ctx) => createFormCapability(ctx, options),
1370
1194
  effects: registerFormEffects,
1371
1195
  init: (ctx) => {
1372
- const interaction = ctx.get(_embedpdf_plugin_interaction.InteractionToken);
1373
- const form = ctx.get(FormToken);
1196
+ const interaction = ctx.get(_embedpdf_plugin_interaction_contract.InteractionToken);
1197
+ const form = ctx.get(require_types.FormToken);
1374
1198
  interaction.registerTool({
1375
1199
  id: "form-fill",
1376
1200
  cursor: "default",
@@ -1381,7 +1205,7 @@ const formPlugin = (options = {}) => (0, _embedpdf_core.definePlugin)({
1381
1205
  cursor: "default",
1382
1206
  enables: /* @__PURE__ */ new Set(["annotation-edit", "annotation-marquee"])
1383
1207
  });
1384
- const annotation = ctx.tryGet(_embedpdf_plugin_annotation_internal.AnnotationToken);
1208
+ const annotation = ctx.tryGet(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
1385
1209
  for (const t of FORM_TOOLS) if (annotation) annotation.registerTool({
1386
1210
  id: t.id,
1387
1211
  subtype: t.visualKind,
@@ -1401,14 +1225,57 @@ const formPlugin = (options = {}) => (0, _embedpdf_core.definePlugin)({
1401
1225
  matches: (a) => a.subtype.startsWith("widget"),
1402
1226
  engaged: () => interaction.activeTool()?.enables.has("form-fill") ?? false
1403
1227
  });
1228
+ const actions = ctx.tryGet(_embedpdf_plugin_actions_contract_host.ActionsToken);
1229
+ if (actions) {
1230
+ ctx.cleanup(actions.registerFormCommitSink((effects) => {
1231
+ const formHost = ctx.tryGet(require_types.FormToken);
1232
+ if (!formHost) return Promise.resolve({
1233
+ results: effects.map((_, index) => ({
1234
+ index,
1235
+ status: "failed",
1236
+ fields: [],
1237
+ changedWidgets: []
1238
+ })),
1239
+ changedWidgets: [],
1240
+ meta: null
1241
+ });
1242
+ return formHost.commitScriptFormEffects(effects);
1243
+ }));
1244
+ ctx.cleanup(actions.registerSubmitResolver((intent, actionCtx, diagnose) => {
1245
+ const formHost = ctx.tryGet(require_types.FormToken);
1246
+ if (!formHost) throw new Error("form plugin unavailable");
1247
+ return formHost.resolveSubmitDataset(intent, actionCtx, diagnose);
1248
+ }));
1249
+ ctx.cleanup(actions.registerExecutor("reset-form", async (node, actionCtx) => {
1250
+ if (node.type !== "reset-form") return {
1251
+ status: "inert",
1252
+ reason: "not a reset-form node"
1253
+ };
1254
+ const formHost = ctx.tryGet(require_types.FormToken);
1255
+ if (!formHost) return {
1256
+ status: "inert",
1257
+ reason: "form plugin unavailable"
1258
+ };
1259
+ const result = await formHost.resetFormAction(node.fields, node.exclude, actionCtx.origin);
1260
+ if (result.status === "failed") return {
1261
+ status: "failed",
1262
+ error: result.error?.message ?? "reset failed"
1263
+ };
1264
+ if (result.effectsResult === null) return {
1265
+ status: "inert",
1266
+ reason: result.diagnostics[0]?.message ?? "reset-form resolved no fields"
1267
+ };
1268
+ return { status: "executed" };
1269
+ }));
1270
+ }
1404
1271
  }
1405
1272
  });
1406
1273
  //#endregion
1407
1274
  exports.FORM_TOOLS = FORM_TOOLS;
1408
1275
  exports.FORM_TOOL_BY_ID = FORM_TOOL_BY_ID;
1409
- exports.FormScriptingController = FormScriptingController;
1410
- exports.FormToken = FormToken;
1411
- exports.createFormScriptingController = createFormScriptingController;
1276
+ exports.FormScriptingController = require_scripting.FormScriptingController;
1277
+ exports.FormToken = require_contract.FormToken;
1278
+ exports.createFormScriptingController = require_scripting.createFormScriptingController;
1412
1279
  exports.createSerialMutationQueue = createSerialMutationQueue;
1413
1280
  exports.fieldKeyOf = fieldKeyOf;
1414
1281
  exports.formPlugin = formPlugin;