@embedpdf/plugin-redaction 3.0.0-next.13 → 3.0.0-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,163 +1,392 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_contract = require("./contract-CyLm5eeQ.cjs");
2
+ const require_contract = require("./contract-Ib0iOWF3.cjs");
3
3
  let _embedpdf_core = require("@embedpdf/core");
4
4
  let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
5
- let _embedpdf_plugin_interaction_contract = require("@embedpdf/plugin-interaction/contract");
5
+ let _embedpdf_plugin_search_contract = require("@embedpdf/plugin-search/contract");
6
6
  let _embedpdf_plugin_selection_contract = require("@embedpdf/plugin-selection/contract");
7
- //#region src/capability.ts
8
- /** Applying destroys content — its own granted power, narrower than annotate.
9
- * The engine's apply asserts all three (LocalDocumentRedactionService; the
10
- * cloud route guards match): the redact grant itself, the page-content
7
+ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
8
+ let _embedpdf_engine_core = require("@embedpdf/engine-core");
9
+ //#region src/read/pending.ts
10
+ /**
11
+ * The pending view: `redact` annotations projected as marks, per page and
12
+ * memoized on the annotation plane's own reference-stable lists, plus the
13
+ * client-side collateral estimate.
14
+ */
15
+ /** The regions a redact mark targets: its quads' boxes, else its `rect`. */
16
+ const regionsOf = (dto) => dto.quadPoints.length === 0 ? [dto.rect] : dto.quadPoints.map(_embedpdf_engine_core.pageQuadBounds);
17
+ const EMPTY = [];
18
+ /** A `redact` annotation as a mark; `pageIndex` is its page's display index. */
19
+ const markOf = (record, pageIndex) => ({
20
+ ref: record.ref,
21
+ page: record.ref.page,
22
+ pageIndex,
23
+ kind: record.quadPoints.length > 0 ? "text" : "area",
24
+ bounds: record.rect,
25
+ overlayText: record.overlayText ?? null,
26
+ repeat: record.repeat ?? false
27
+ });
28
+ function createPendingReads(ctx, { store, siblings }) {
29
+ const { pages, pageIndexOf } = store;
30
+ const { annotation } = siblings;
31
+ /** One page's marks, the same array while the plane's list and the page's position hold. */
32
+ const marksOn = (0, _embedpdf_core.memoByKey)((pageObjectNumber) => {
33
+ const page = (0, _embedpdf_core.toPageRef)(pageObjectNumber);
34
+ return [annotation.list({ pages: [page] }), pageIndexOf(page)];
35
+ }, (_pageObjectNumber, records, pageIndex) => {
36
+ const marks = records.flatMap((record) => record.subtype === "redact" ? [markOf(record, pageIndex)] : []);
37
+ return marks.length ? marks : EMPTY;
38
+ });
39
+ /** The whole document's marks, the same array while no page's marks changed. */
40
+ const allMarks = (0, _embedpdf_core.memo)(() => pages().map((page) => marksOn(page.objectNumber)), (...perPage) => perPage.some((marks) => marks.length) ? perPage.flat() : EMPTY);
41
+ /** One page's marks (a ref or an index; none for a page that isn't there), or every mark. */
42
+ const listPending = (filter) => {
43
+ if (filter?.page === void 0) return allMarks();
44
+ const layout = ctx.getPage(filter.page);
45
+ return layout ? marksOn(layout.ref.objectNumber) : EMPTY;
46
+ };
47
+ /** A record the annotation plugin made or changed, as the mark it is. */
48
+ const markFor = (record) => record.subtype === "redact" ? markOf(record, pageIndexOf(record.ref.page)) : null;
49
+ const getPending = (ref) => {
50
+ const key = (0, _embedpdf_core.annotationKey)(ref);
51
+ return listPending().find((mark) => (0, _embedpdf_core.annotationKey)(mark.ref) === key) ?? null;
52
+ };
53
+ const estimateCollateral = (refs) => {
54
+ const wanted = refs ? new Set(refs.map(_embedpdf_core.annotationKey)) : null;
55
+ const hits = [];
56
+ for (const page of pages()) {
57
+ const onPage = annotation.list({ pages: [page] });
58
+ const marks = onPage.filter((dto) => dto.subtype === "redact" && (!wanted || wanted.has((0, _embedpdf_core.annotationKey)(dto.ref))));
59
+ if (marks.length === 0) continue;
60
+ const regions = marks.flatMap(regionsOf);
61
+ for (const other of onPage) {
62
+ if (other.subtype === "redact") continue;
63
+ if (regions.some((region) => (0, _embedpdf_core_geometry.rectsOverlap)(region, other.rect))) hits.push(other.ref);
64
+ }
65
+ }
66
+ return {
67
+ count: hits.length,
68
+ refs: hits
69
+ };
70
+ };
71
+ return {
72
+ listPending,
73
+ markFor,
74
+ api: {
75
+ listPending,
76
+ getPending,
77
+ getPendingCount: (page) => listPending(page === void 0 ? void 0 : { page }).length,
78
+ estimateCollateral
79
+ }
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region src/services/events.ts
84
+ function createEvents(ctx) {
85
+ return {
86
+ applied: ctx.events.source(),
87
+ pendingChanged: ctx.events.source()
88
+ };
89
+ }
90
+ //#endregion
91
+ //#region src/services/siblings.ts
92
+ /** The sibling plugins: annotation (required, host lens), selection and search (optional). */
93
+ function resolveSiblings(ctx) {
94
+ return {
95
+ annotation: ctx.get(_embedpdf_plugin_annotation_contract_host.AnnotationToken),
96
+ selection: () => ctx.tryGet(_embedpdf_plugin_selection_contract.SelectionToken),
97
+ search: () => ctx.tryGet(_embedpdf_plugin_search_contract.SearchToken)
98
+ };
99
+ }
100
+ //#endregion
101
+ //#region src/services/store.ts
102
+ /** The page registry reads and the engine's redaction service. */
103
+ /** Applying destroys content: its own granted power, narrower than annotate.
104
+ * The engine's apply asserts all three (the local redaction service and the
105
+ * cloud route guards alike): the redact grant itself, the page-content
11
106
  * rewrite, and the annotation consumption. `canApply` mirrors the full set. */
12
107
  const APPLY_CAPABILITIES = [
13
108
  "doc.redact",
14
109
  "doc.pages.modify",
15
110
  "doc.annotate.modify"
16
111
  ];
17
- /** The annotation plugin's model-id convention for a durable ref. */
18
- const idOf = (ref) => ref.kind === "objectNumber" ? `obj:${ref.annotObjectNumber}` : ref.kind === "nm" ? `nm:${ref.nm}` : `idx:${ref.pageObjectNumber}:${ref.index}`;
19
- /** The PDF-space regions a redact mark targets: its quads' boxes, else `/Rect`. */
20
- function regionsOf(dto) {
21
- if (dto.quadPoints.length === 0) return [dto.rect];
22
- return dto.quadPoints.map((q) => {
23
- const xs = [
24
- q.p1.x,
25
- q.p2.x,
26
- q.p3.x,
27
- q.p4.x
28
- ];
29
- const ys = [
30
- q.p1.y,
31
- q.p2.y,
32
- q.p3.y,
33
- q.p4.y
34
- ];
35
- return {
36
- left: Math.min(...xs),
37
- right: Math.max(...xs),
38
- bottom: Math.min(...ys),
39
- top: Math.max(...ys)
40
- };
41
- });
112
+ function createStore(ctx) {
113
+ const requireService = () => {
114
+ const service = ctx.doc.redaction;
115
+ if (!service) throw new _embedpdf_core.PluginError("unsupported", "redaction", "this engine has no redaction service");
116
+ return service;
117
+ };
118
+ /** Every page's ref, in display order. */
119
+ const pages = () => (ctx.document()?.pages ?? []).map((page) => page.ref);
120
+ /** Page object number → display index, rebuilt only when the page registry changes. */
121
+ const indexByPage = (0, _embedpdf_core.memo)(() => [ctx.document()?.pages], (layouts) => new Map((layouts ?? []).map((page) => [page.ref.objectNumber, page.index])));
122
+ const pageIndexOf = (page) => indexByPage().get(page.objectNumber) ?? -1;
123
+ const canApply = () => ctx.doc.redaction !== void 0 && APPLY_CAPABILITIES.every((capability) => ctx.doc.security.allows(capability));
124
+ return {
125
+ requireService,
126
+ pages,
127
+ pageIndexOf,
128
+ canApply
129
+ };
42
130
  }
43
- /** Positive-area intersection — the engine's collateral rule. */
44
- const intersects = (a, b) => Math.min(a.right, b.right) > Math.max(a.left, b.left) && Math.min(a.top, b.top) > Math.max(a.bottom, b.bottom);
45
- function createRedactionCapability(ctx) {
46
- const anno = ctx.get(_embedpdf_plugin_annotation_contract_host.AnnotationToken);
47
- const appliedCallbacks = /* @__PURE__ */ new Set();
48
- const requireDoc = () => {
49
- const doc = ctx.doc;
50
- if (!doc) throw new Error("[redaction] no document bound");
51
- return doc;
131
+ //#endregion
132
+ //#region src/services/index.ts
133
+ function createServices(ctx) {
134
+ return {
135
+ events: createEvents(ctx),
136
+ store: createStore(ctx),
137
+ siblings: resolveSiblings(ctx)
52
138
  };
53
- const requireService = () => {
54
- const doc = requireDoc();
55
- if (!doc.redaction) throw new Error("[redaction] engine has no redaction service");
56
- return doc.redaction;
57
- };
58
- const pons = () => (ctx.document()?.pages ?? []).map((p) => p.pageObjectNumber);
59
- const pageIndexOf = (pon) => ctx.document()?.pages.find((p) => p.pageObjectNumber === pon)?.index ?? -1;
60
- const pendingOn = (pon) => anno.list(pon).filter((a) => a.subtype === "redact");
61
- const collectPending = () => {
62
- const items = [];
63
- for (const pon of pons()) for (const dto of pendingOn(pon)) items.push({
64
- id: idOf(dto.ref),
65
- ref: dto.ref,
66
- pageObjectNumber: pon,
67
- pageIndex: pageIndexOf(pon),
68
- kind: dto.quadPoints.length > 0 ? "text" : "area",
69
- overlayText: dto.overlayText
139
+ }
140
+ //#endregion
141
+ //#region src/model.ts
142
+ const initialRedactionState = () => ({
143
+ applying: false,
144
+ lastResult: null
145
+ });
146
+ const startApply = (state) => state.applying ? state : {
147
+ ...state,
148
+ applying: true
149
+ };
150
+ const finishApply = (state) => state.applying ? {
151
+ ...state,
152
+ applying: false
153
+ } : state;
154
+ const setLastResult = (state, result) => state.lastResult === result ? state : {
155
+ ...state,
156
+ lastResult: result
157
+ };
158
+ //#endregion
159
+ //#region src/sync/document-events.ts
160
+ function subscribeChanges(ctx, { events, siblings }) {
161
+ const { applied, pendingChanged } = events;
162
+ const { annotation } = siblings;
163
+ ctx.listen(ctx.doc.events, (event) => {
164
+ if (event.type !== "redaction.applied") return;
165
+ const { type: _type, origin, ...result } = event;
166
+ ctx.state.update(setLastResult, result);
167
+ applied.emit({
168
+ result,
169
+ origin
70
170
  });
71
- return items;
72
- };
73
- const fireApplied = (result) => {
74
- for (const cb of appliedCallbacks) try {
75
- cb(result);
76
- } catch (err) {
77
- console.error("[redaction] onApplied callback failed", err);
78
- }
171
+ });
172
+ const markChanged = ({ annotation: changed }) => {
173
+ if (changed.subtype === "redact") pendingChanged.emit({ pages: [changed.page] });
79
174
  };
80
- const runApply = async (scope) => {
175
+ ctx.listen(annotation.onCreated, markChanged);
176
+ ctx.listen(annotation.onUpdated, markChanged);
177
+ ctx.listen(annotation.onDeleted, (event) => pendingChanged.emit({ pages: [event.page] }));
178
+ }
179
+ //#endregion
180
+ //#region src/sync/overlay.ts
181
+ /** The redact tool's fields the overlay settings stand for. */
182
+ const toolDefaultsOf = ({ overlay }) => ({
183
+ interiorColor: overlay.fill,
184
+ fontColor: overlay.text.color,
185
+ fontFamily: overlay.text.fontFamily,
186
+ fontSize: overlay.text.fontSize
187
+ });
188
+ function syncOverlay(ctx, { siblings }) {
189
+ const { annotation } = siblings;
190
+ const settings = ctx.settings();
191
+ const write = () => annotation.tools.updateDefaults("redact", toolDefaultsOf(settings.get()));
192
+ write();
193
+ ctx.listen(settings.api.onSettingsChanged, ({ changed }) => {
194
+ if (changed.includes("overlay")) write();
195
+ });
196
+ }
197
+ //#endregion
198
+ //#region src/write/apply.ts
199
+ /**
200
+ * The destructive half: `doc.redaction.apply` in refs or pages scope. Applies
201
+ * run one at a time, in call order, so each one sees the marks the previous
202
+ * one left. The result reaches `lastResult` and `onApplied` through the
203
+ * confirmed `redaction.applied` event, which the engine publishes before the
204
+ * apply resolves; this area only brackets the call with the `applying` flag.
205
+ */
206
+ function createApplying(ctx, { store }, { listPending }) {
207
+ const { requireService, pages } = store;
208
+ const queue = ctx.serialQueue("apply");
209
+ ctx.onSettle(() => queue.idle());
210
+ /**
211
+ * Queue an apply whose scope is computed when its turn comes. One whose
212
+ * signal fired while it waited never starts; one cancelled while the
213
+ * engine applies rejects `operation-cancelled` at once, and the engine's
214
+ * `redaction.applied` says what it removed.
215
+ */
216
+ const runApply = (scopeOf, options) => queue(async () => {
81
217
  const service = requireService();
82
- ctx.dispatch({ type: "APPLY_STARTED" });
218
+ const scope = scopeOf();
219
+ ctx.state.update(startApply);
83
220
  try {
84
- const result = await service.apply(scope);
85
- ctx.dispatch({
86
- type: "APPLY_FINISHED",
87
- result
88
- });
89
- fireApplied(result);
90
- return result;
91
- } catch (err) {
92
- ctx.dispatch({
93
- type: "APPLY_FINISHED",
94
- result: null
95
- });
96
- throw err;
221
+ return await ctx.cancellable(options?.signal, service.apply(scope));
222
+ } finally {
223
+ ctx.state.update(finishApply);
97
224
  }
225
+ }, { signal: options?.signal });
226
+ const apply = (refs, options) => runApply(() => {
227
+ const wanted = new Set(refs.map(_embedpdf_core.annotationKey));
228
+ const marks = listPending().filter((mark) => wanted.has((0, _embedpdf_core.annotationKey)(mark.ref))).map((mark) => mark.ref);
229
+ if (marks.length === 0) throw new _embedpdf_core.PluginError("not-found", "redaction", "no matching pending marks");
230
+ return { annotations: marks };
231
+ }, options);
232
+ const applyPages = async (targets, options) => {
233
+ if (targets.length === 0) throw new _embedpdf_core.PluginError("invalid-input", "redaction", "no pages given");
234
+ const pages = targets.map((page) => ctx.pageOf(page).ref);
235
+ return runApply(() => ({ pages }), options);
98
236
  };
99
- const doc = ctx.doc;
100
- if (doc) ctx.cleanup(doc.events.subscribe((event) => {
101
- if (event.type !== "redaction.applied") return;
102
- if (event.origin.kind !== "remote") return;
103
- ctx.dispatch({
104
- type: "APPLY_FINISHED",
105
- result: event
237
+ const applyAll = (options) => runApply(() => {
238
+ const all = pages();
239
+ if (all.length === 0) throw new _embedpdf_core.PluginError("not-ready", "redaction", "the document has no pages");
240
+ return { pages: all };
241
+ }, options);
242
+ return { api: {
243
+ apply,
244
+ applyPages,
245
+ applyAll
246
+ } };
247
+ }
248
+ //#endregion
249
+ //#region src/write/marks.ts
250
+ /**
251
+ * Marking: every mark is a `redact` annotation created through the annotation
252
+ * plugin, in the style of the redact tool's defaults, which follow the
253
+ * `overlay` settings (`sync/overlay.ts`). Removing a mark and changing its
254
+ * label are annotation deletes and updates, checked per mark.
255
+ */
256
+ /** Stop before the next step once the caller cancelled. */
257
+ const throwIfCancelled = (signal) => {
258
+ if (signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "redaction", "operation cancelled", { cause: signal.reason });
259
+ };
260
+ function createMarking(ctx, { siblings }, { listPending, markFor }) {
261
+ const { annotation, selection, search } = siblings;
262
+ /**
263
+ * A new mark's style, as the engine takes it: the redact tool's current
264
+ * defaults, which carry the `overlay` settings, so a mark from code looks
265
+ * like one drawn with the tool.
266
+ */
267
+ const markStyle = () => {
268
+ const tool = annotation.tools.getDefaults("redact");
269
+ return {
270
+ color: tool.color,
271
+ interiorColor: tool.interiorColor,
272
+ opacity: tool.opacity,
273
+ fontFamily: tool.fontFamily,
274
+ fontSize: tool.fontSize,
275
+ fontColor: tool.fontColor,
276
+ textAlign: tool.textAlign
277
+ };
278
+ };
279
+ /** The mark a created or changed record is. */
280
+ const markOfRecord = (record) => {
281
+ const mark = markFor(record);
282
+ if (!mark) throw new _embedpdf_core.PluginError("operation-failed", "redaction", "the record is not a mark");
283
+ return mark;
284
+ };
285
+ /** Whether a ref is a mark waiting to be applied. */
286
+ const isPending = (ref) => {
287
+ const key = (0, _embedpdf_core.annotationKey)(ref);
288
+ return listPending().some((mark) => (0, _embedpdf_core.annotationKey)(mark.ref) === key);
289
+ };
290
+ const canUnmark = (ref) => isPending(ref) && annotation.canDelete(ref);
291
+ const canUpdateLabel = (ref) => isPending(ref) && annotation.canUpdate(ref);
292
+ const markSelection = async (options) => {
293
+ ctx.assertAllowed("annotations:create", "redaction.markSelection");
294
+ const plane = selection();
295
+ if (!plane) throw new _embedpdf_core.PluginError("unsupported", "redaction", "no selection plugin");
296
+ if (!plane.hasSelection()) return { marks: [] };
297
+ const { annotations } = await annotation.createFromSelection("redact", {
298
+ clear: true,
299
+ signal: options?.signal
106
300
  });
107
- fireApplied(event);
108
- }));
109
- return {
110
- canMark: () => anno.canCreate(),
111
- canApply: () => {
112
- const doc = ctx.doc;
113
- if (!doc || doc.redaction === void 0) return false;
114
- return APPLY_CAPABILITIES.every((cap) => doc.security.allows(cap));
115
- },
116
- isApplying: () => ctx.getState().applying,
117
- lastResult: () => ctx.getState().lastResult,
118
- enableRedact: () => ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activateTool("redact"),
119
- toggleRedact: () => {
120
- const interaction = ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken);
121
- if (!interaction) return;
122
- interaction.activateTool(interaction.activeToolId() === "redact" ? "pointer" : "redact");
123
- },
124
- isRedactActive: () => ctx.tryGet(_embedpdf_plugin_interaction_contract.InteractionToken)?.activeToolId() === "redact",
125
- queueCurrentSelection: async () => {
126
- if (!anno.canCreate()) return false;
127
- const selection = ctx.tryGet(_embedpdf_plugin_selection_contract.SelectionToken);
128
- if (!selection || !selection.hasSelection()) return false;
129
- const snapshot = selection.snapshot();
130
- for (const page of snapshot.pages) {
131
- if (page.segments.length === 0) continue;
132
- anno.createMarkup("redact", page.pon, page.segments.map((segment) => segment.quad), "redact");
301
+ return { marks: annotations.map(markOfRecord) };
302
+ };
303
+ const createMark = async (page, region, signal) => {
304
+ const created = await annotation.create(page, {
305
+ subtype: "redact",
306
+ ...region,
307
+ ...markStyle()
308
+ }, void 0, { signal });
309
+ return markOfRecord(created.annotation);
310
+ };
311
+ const markArea = async (page, bounds, options) => {
312
+ ctx.assertAllowed("annotations:create", "redaction.markArea");
313
+ const target = ctx.pageOf(page).ref;
314
+ return { mark: await createMark(target, { rect: bounds }, options?.signal) };
315
+ };
316
+ const markPage = async (page, options) => {
317
+ ctx.assertAllowed("annotations:create", "redaction.markPage");
318
+ const layout = ctx.pageOf(page);
319
+ const whole = {
320
+ x: 0,
321
+ y: 0,
322
+ width: layout.size.width,
323
+ height: layout.size.height
324
+ };
325
+ return { mark: await createMark(layout.ref, { rect: whole }, options?.signal) };
326
+ };
327
+ const markMatches = async (query, options) => {
328
+ ctx.assertAllowed("annotations:create", "redaction.markMatches");
329
+ const signal = options?.signal;
330
+ const finder = search();
331
+ if (!finder) throw new _embedpdf_core.PluginError("unsupported", "redaction", "no search plugin");
332
+ const wanted = options?.pages ? new Set(options.pages.map((page) => ctx.pageOf(page).ref.objectNumber)) : null;
333
+ await finder.search(query, { signal });
334
+ const marks = [];
335
+ for (const hit of finder.listHits()) {
336
+ if (wanted && !wanted.has(hit.page.objectNumber)) continue;
337
+ if (hit.segments.length === 0) continue;
338
+ throwIfCancelled(signal);
339
+ marks.push(await createMark(hit.page, { quadPoints: hit.segments.map((segment) => segment.quad) }, signal));
340
+ }
341
+ return { marks };
342
+ };
343
+ const unmark = async (refs, options) => {
344
+ const signal = options?.signal;
345
+ throwIfCancelled(signal);
346
+ const applied = [];
347
+ const skipped = [];
348
+ const failed = [];
349
+ for (const ref of refs) {
350
+ if (!isPending(ref)) {
351
+ skipped.push({
352
+ ref,
353
+ reason: "not a pending redaction mark"
354
+ });
355
+ continue;
356
+ }
357
+ if (!annotation.canDelete(ref)) {
358
+ const refused = new _embedpdf_core.PluginError("permission-denied", "redaction", "redaction.unmark requires annotations:delete", { permission: "annotations:delete" });
359
+ failed.push({
360
+ ref,
361
+ error: (0, _embedpdf_core.toPluginErrorInfo)(refused)
362
+ });
363
+ continue;
133
364
  }
134
- selection.clear();
135
- return true;
136
- },
137
- preparePending: async () => {
138
- await Promise.all(pons().map((pon) => anno.ensurePage(pon)));
139
- },
140
- getPending: collectPending,
141
- pendingCount: () => collectPending().length,
142
- estimateCollateral: (ids) => {
143
- const wanted = ids ? new Set(ids) : null;
144
- let count = 0;
145
- for (const pon of pons()) {
146
- const all = anno.list(pon);
147
- const marks = all.filter((a) => a.subtype === "redact" && (!wanted || wanted.has(idOf(a.ref))));
148
- if (marks.length === 0) continue;
149
- const regions = marks.flatMap(regionsOf);
150
- for (const other of all) {
151
- if (other.subtype === "redact") continue;
152
- if (regions.some((r) => intersects(r, other.rect))) count++;
153
- }
365
+ try {
366
+ await annotation.delete(ref, { signal });
367
+ applied.push(ref);
368
+ } catch (error) {
369
+ const refused = (0, _embedpdf_core.toPluginError)("redaction", error);
370
+ if (refused.code === "operation-cancelled") throw refused;
371
+ failed.push({
372
+ ref,
373
+ error: (0, _embedpdf_core.toPluginErrorInfo)(refused)
374
+ });
154
375
  }
155
- return count;
156
- },
157
- setLabel: async (ref, patch) => {
158
- const current = anno.get(ref);
159
- if (!current || current.subtype !== "redact") throw new Error("[redaction] setLabel target is not a redact annotation");
160
- await anno.update(ref, {
376
+ }
377
+ return {
378
+ applied,
379
+ skipped,
380
+ failed
381
+ };
382
+ };
383
+ const clearPending = (options) => unmark(listPending().map((mark) => mark.ref), options);
384
+ const updateLabel = async (ref, patch, options) => {
385
+ const current = annotation.get(ref);
386
+ if (!current || current.subtype !== "redact") throw new _embedpdf_core.PluginError("not-found", "redaction", "the target is not a redaction mark");
387
+ if (!annotation.canUpdate(ref)) throw new _embedpdf_core.PluginError("permission-denied", "redaction", "redaction.updateLabel requires annotations:update", { permission: "annotations:update" });
388
+ try {
389
+ const updated = await annotation.update(ref, {
161
390
  subtype: "redact",
162
391
  ...patch.overlayText !== void 0 ? { overlayText: patch.overlayText } : {},
163
392
  ...patch.repeat !== void 0 ? { repeat: patch.repeat } : {},
@@ -165,74 +394,110 @@ function createRedactionCapability(ctx) {
165
394
  fontSize: current.fontSize,
166
395
  fontColor: current.fontColor,
167
396
  textAlign: current.textAlign
168
- });
169
- },
170
- apply: async (ids) => {
171
- const wanted = new Set(ids);
172
- const refs = collectPending().filter((p) => wanted.has(p.id)).map((p) => p.ref);
173
- if (refs.length === 0) throw new Error("[redaction] no matching pending redactions");
174
- return runApply({
175
- kind: "annotations",
176
- refs
177
- });
178
- },
179
- applyAll: async () => {
180
- const pageObjectNumbers = pons();
181
- if (pageObjectNumbers.length === 0) throw new Error("[redaction] document has no pages");
182
- return runApply({
183
- kind: "pages",
184
- pageObjectNumbers
185
- });
186
- },
187
- onApplied: (cb) => {
188
- appliedCallbacks.add(cb);
189
- return () => appliedCallbacks.delete(cb);
397
+ }, void 0, { signal: options?.signal });
398
+ return { mark: markOfRecord(updated.annotation) };
399
+ } catch (error) {
400
+ throw (0, _embedpdf_core.toPluginError)("redaction", error);
190
401
  }
191
402
  };
403
+ return { api: {
404
+ canUnmark,
405
+ canUpdateLabel,
406
+ markSelection,
407
+ markArea,
408
+ markPage,
409
+ markMatches,
410
+ unmark,
411
+ clearPending,
412
+ updateLabel
413
+ } };
192
414
  }
193
415
  //#endregion
194
- //#region src/reducer.ts
195
- const initialRedactionState = () => ({
196
- applying: false,
197
- lastResult: null
198
- });
199
- const redactionReducer = (state, action) => {
200
- switch (action.type) {
201
- case "APPLY_STARTED": return {
202
- ...state,
203
- applying: true
204
- };
205
- case "APPLY_FINISHED": return {
206
- applying: false,
207
- lastResult: action.result ?? state.lastResult
208
- };
209
- default: return state;
210
- }
211
- };
416
+ //#region src/controller.ts
417
+ /**
418
+ * The redaction controller: the composition root. It builds the plugin's
419
+ * services once, wires each area with the services it declares, and
420
+ * assembles the capability from the areas' API slices. No behavior lives
421
+ * here: every verb and read has a home in `read/`, `write/` or `sync/`.
422
+ */
423
+ function createRedactionController(ctx) {
424
+ const services = createServices(ctx);
425
+ const { events, store } = services;
426
+ const settings = ctx.settings();
427
+ const pending = createPendingReads(ctx, services);
428
+ const marking = createMarking(ctx, services, pending);
429
+ const applying = createApplying(ctx, services, pending);
430
+ return {
431
+ api: (0, _embedpdf_core.composeApi)("redaction", [
432
+ settings.api,
433
+ pending.api,
434
+ marking.api,
435
+ applying.api,
436
+ {
437
+ canMark: () => ctx.allows("annotations:create"),
438
+ canApply: store.canApply,
439
+ isApplying: () => ctx.state.get().applying,
440
+ getLastResult: () => ctx.state.get().lastResult,
441
+ onApplied: events.applied.on,
442
+ onPendingChanged: events.pendingChanged.on
443
+ }
444
+ ]),
445
+ connect() {
446
+ subscribeChanges(ctx, services);
447
+ syncOverlay(ctx, services);
448
+ }
449
+ };
450
+ }
212
451
  //#endregion
213
452
  //#region src/redaction.plugin.ts
214
453
  /**
215
- * Document-scoped redaction plugin — the DESTRUCTIVE half of the two-stage
216
- * model. Marking is annotation-plane work (the composed `redact` tool ships
217
- * with plugin-annotation); this plugin wraps `doc.redaction.apply`, projects
218
- * the pending queue out of annotation state, and estimates collateral for
454
+ * Document-scoped redaction plugin: the destructive half of the two-stage
455
+ * model. Marking is annotation work (the composed `redact` tool ships with
456
+ * plugin-annotation); this plugin wraps `doc.redaction.apply`, projects the
457
+ * pending queue out of annotation state, and estimates collateral for
219
458
  * confirm dialogs. It owns no mark state of its own.
220
459
  *
221
- * Trust boundary: on a layered document, applying rewrites the LAYER's bytes;
222
- * the immutable base keeps the original. See the package README.
460
+ * Trust boundary: on a layered document, applying rewrites the layer's bytes;
461
+ * the immutable base keeps the original. The package README explains the consequences.
462
+ * `config` is the settings the app registers, over {@link REDACTION_DEFAULTS}.
223
463
  */
224
- const redactionPlugin = () => (0, _embedpdf_core.definePlugin)({
464
+ const redactionPlugin = (config) => (0, _embedpdf_core.definePlugin)({
225
465
  id: "redaction",
226
466
  token: require_contract.RedactionToken,
227
467
  scope: "document",
228
468
  requires: [_embedpdf_plugin_annotation_contract_host.AnnotationToken],
229
- optional: [_embedpdf_plugin_interaction_contract.InteractionToken, _embedpdf_plugin_selection_contract.SelectionToken],
230
- initialState: initialRedactionState,
231
- reduce: redactionReducer,
232
- capability: createRedactionCapability
469
+ optional: [_embedpdf_plugin_selection_contract.SelectionToken, _embedpdf_plugin_search_contract.SearchToken],
470
+ state: initialRedactionState,
471
+ settings: {
472
+ defaults: require_contract.REDACTION_DEFAULTS,
473
+ registered: config
474
+ },
475
+ create: createRedactionController
476
+ });
477
+ //#endregion
478
+ //#region src/state.ts
479
+ /**
480
+ * The redaction page's State table as code: what `useRedactionState()`
481
+ * returns, and the same fields in every other framework. The marks
482
+ * themselves are not here: `usePendingRedactions()` reads them, so a
483
+ * component that only shows the count doesn't re-render for every mark.
484
+ */
485
+ const redactionState = (0, _embedpdf_core.defineState)(require_contract.RedactionToken, {
486
+ read: (redaction) => ({
487
+ pendingCount: redaction.getPendingCount(),
488
+ applying: redaction.isApplying(),
489
+ lastResult: redaction.getLastResult()
490
+ }),
491
+ empty: {
492
+ pendingCount: 0,
493
+ applying: false,
494
+ lastResult: null
495
+ }
233
496
  });
234
497
  //#endregion
498
+ exports.REDACTION_DEFAULTS = require_contract.REDACTION_DEFAULTS;
235
499
  exports.RedactionToken = require_contract.RedactionToken;
236
500
  exports.redactionPlugin = redactionPlugin;
501
+ exports.redactionState = redactionState;
237
502
 
238
503
  //# sourceMappingURL=index.cjs.map