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