@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/README.md +66 -24
- package/dist/contract-Ib0iOWF3.cjs +43 -0
- package/dist/contract-Ib0iOWF3.cjs.map +1 -0
- package/dist/contract-TU9orxrO.d.cts +184 -0
- package/dist/contract-TU9orxrO.d.ts +184 -0
- package/dist/contract-eQ3P0UU-.js +33 -0
- package/dist/contract-eQ3P0UU-.js.map +1 -0
- package/dist/contract.cjs +2 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +2 -2
- package/dist/index.cjs +455 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -9
- package/dist/index.d.ts +26 -9
- package/dist/index.js +455 -192
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/dist/contract--fJOCVRN.d.cts +0 -84
- package/dist/contract--fJOCVRN.d.ts +0 -84
- package/dist/contract-BZLrD3p2.js +0 -7
- package/dist/contract-BZLrD3p2.js.map +0 -1
- package/dist/contract-CyLm5eeQ.cjs +0 -11
- package/dist/contract-CyLm5eeQ.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,162 +1,391 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
+
import { SearchToken } from "@embedpdf/plugin-search/contract";
|
|
5
5
|
import { SelectionToken } from "@embedpdf/plugin-selection/contract";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
const scope = scopeOf();
|
|
218
|
+
ctx.state.update(startApply);
|
|
82
219
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
215
|
-
* model. Marking is annotation
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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
|
|
221
|
-
* the immutable base keeps the original.
|
|
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: [
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|