@embedpdf/plugin-page-edit 3.0.0-next.14 → 3.0.0-next.16
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/contract-B4DeVaKv.cjs +25 -0
- package/dist/contract-B4DeVaKv.cjs.map +1 -0
- package/dist/contract-CYWYR5Pd.js +21 -0
- package/dist/contract-CYWYR5Pd.js.map +1 -0
- package/dist/contract-IN8rk5LG.d.cts +91 -0
- package/dist/contract-IN8rk5LG.d.ts +91 -0
- package/dist/contract.cjs +1 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +1 -1
- package/dist/index.cjs +284 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -8
- package/dist/index.d.ts +20 -8
- package/dist/index.js +285 -86
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/dist/contract-BnXi11KD.d.cts +0 -82
- package/dist/contract-BnXi11KD.d.ts +0 -82
- package/dist/contract-BsNelyd8.js +0 -7
- package/dist/contract-BsNelyd8.js.map +0 -1
- package/dist/contract-UeGRCn-t.cjs +0 -11
- package/dist/contract-UeGRCn-t.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,117 +1,316 @@
|
|
|
1
|
-
import { t as PageEditToken } from "./contract-
|
|
2
|
-
import { definePlugin } from "@embedpdf/core";
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { t as PageEditToken } from "./contract-CYWYR5Pd.js";
|
|
2
|
+
import { DocumentsToken, PluginError, definePlugin, defineState, memo, toPluginError } from "@embedpdf/core";
|
|
3
|
+
//#region src/model.ts
|
|
4
|
+
const initialPageEditState = () => ({ selected: [] });
|
|
5
|
+
/** The selection becomes `refs`, each page once. */
|
|
6
|
+
function selected(state, refs) {
|
|
7
|
+
const numbers = refs.map((ref) => ref.objectNumber);
|
|
8
|
+
const unique = refs.filter((_ref, i) => numbers.indexOf(numbers[i]) === i);
|
|
9
|
+
return unique.length === state.selected.length && unique.every((ref, i) => ref.objectNumber === state.selected[i].objectNumber) ? state : {
|
|
10
|
+
...state,
|
|
11
|
+
selected: unique
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/controller.ts
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* The page-edit controller: page edits that take refs or indexes, and the
|
|
18
|
+
* page selection. Turning a page relative to its own rotation, and fixing a
|
|
19
|
+
* position's page, live here once instead of in every app's click handler.
|
|
20
|
+
*
|
|
21
|
+
* A verb refuses before anything starts (permission, pages, position), then
|
|
22
|
+
* stages its edit as one change on the document's queue (`ctx.changes`): one
|
|
23
|
+
* step of the history, sent after everything staged before it. The page list
|
|
24
|
+
* shows it once the engine answers; nothing predicts pages yet.
|
|
18
25
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
/** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */
|
|
27
|
+
const ASSEMBLE = "doc.pages.assemble";
|
|
28
|
+
/** Copying pages out of a document is a partial download. The engine enforces it too. */
|
|
29
|
+
const DOWNLOAD = "doc.download";
|
|
30
|
+
/** The size of a blank page when the document has no page to match: Letter, in points. */
|
|
31
|
+
const LETTER_SIZE = {
|
|
32
|
+
width: 612,
|
|
33
|
+
height: 792
|
|
34
|
+
};
|
|
35
|
+
const NO_PAGES = Object.freeze([]);
|
|
36
|
+
const NO_LAYOUTS = Object.freeze([]);
|
|
37
|
+
function createPageEditController(ctx) {
|
|
38
|
+
/** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */
|
|
39
|
+
const mapErrors = async (work) => {
|
|
40
|
+
try {
|
|
41
|
+
return await work();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw toPluginError("page-edit", error);
|
|
44
|
+
}
|
|
24
45
|
};
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
46
|
+
const registry = () => ctx.document()?.pages ?? NO_LAYOUTS;
|
|
47
|
+
const noPages = () => new PluginError("invalid-input", "page-edit", "no pages given");
|
|
48
|
+
/**
|
|
49
|
+
* A verb's pages as refs, checked when it is called: an index names the
|
|
50
|
+
* page at that position now, which is the page the user clicked.
|
|
51
|
+
*/
|
|
52
|
+
const refsOf = (pages) => {
|
|
53
|
+
if (pages.length === 0) throw noPages();
|
|
54
|
+
return pages.map((page) => ctx.pageOf(page).ref);
|
|
55
|
+
};
|
|
56
|
+
/** A position, its page checked when the verb is called, like the verb's own pages. Default `'end'`. */
|
|
57
|
+
const positionOf = (position) => {
|
|
58
|
+
if (!position || position === "start" || position === "end") return position ?? "end";
|
|
59
|
+
if ("after" in position) return { after: ctx.pageOf(position.after).ref };
|
|
60
|
+
return { before: ctx.pageOf(position.before).ref };
|
|
28
61
|
};
|
|
29
62
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* a concurrent reorder. Returns the anchor page too, so `addBlank` can
|
|
33
|
-
* default its size without a second lookup.
|
|
63
|
+
* Default blank-page size: the page the position names, else the page the
|
|
64
|
+
* new ones go next to (the first or the last), else Letter.
|
|
34
65
|
*/
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
66
|
+
const sizeAt = (position) => {
|
|
67
|
+
if (typeof position === "object") return ctx.pageOf("after" in position ? position.after : position.before).size;
|
|
68
|
+
const pages = registry();
|
|
69
|
+
if (pages.length === 0) return LETTER_SIZE;
|
|
70
|
+
return (position === "start" ? pages[0] : pages[pages.length - 1]).size;
|
|
71
|
+
};
|
|
72
|
+
const getSelected = memo(() => [registry(), ctx.state.get().selected], (pages, refs) => {
|
|
73
|
+
if (refs.length === 0) return NO_PAGES;
|
|
74
|
+
const present = new Set(pages.map((page) => page.ref.objectNumber));
|
|
75
|
+
return refs.filter((ref) => present.has(ref.objectNumber));
|
|
76
|
+
});
|
|
77
|
+
const select = (pages) => {
|
|
78
|
+
const refs = pages.flatMap((page) => {
|
|
79
|
+
const found = ctx.getPage(page);
|
|
80
|
+
return found ? [found.ref] : [];
|
|
81
|
+
});
|
|
82
|
+
ctx.state.update(selected, refs);
|
|
83
|
+
};
|
|
84
|
+
/** Refuse before anything starts: without the right, or once the caller cancelled. */
|
|
85
|
+
const refuse = (permission, operation, options) => {
|
|
86
|
+
ctx.assertAllowed(permission, operation);
|
|
87
|
+
if (options?.signal?.aborted) throw new PluginError("operation-cancelled", "page-edit", `${operation} was cancelled`);
|
|
51
88
|
};
|
|
52
89
|
/**
|
|
53
|
-
*
|
|
54
|
-
* the
|
|
55
|
-
* with an empty registry, which an open document never has — US Letter.
|
|
90
|
+
* Stage one user action: one change, one step of the history. Its label
|
|
91
|
+
* counts the pages it acts on, when the verb knows how many.
|
|
56
92
|
*/
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
height: 792
|
|
93
|
+
const stageChange = (verb, count, ops, undo) => {
|
|
94
|
+
const label = count === null ? { key: `pages.${verb}` } : {
|
|
95
|
+
key: `pages.${verb}`,
|
|
96
|
+
count
|
|
62
97
|
};
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
return ctx.changes.stage({
|
|
99
|
+
label,
|
|
100
|
+
ops,
|
|
101
|
+
...undo ? { undo } : {}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
/** The engine's answer to a staged change: its first item. */
|
|
105
|
+
const answer = async (change, options) => {
|
|
106
|
+
try {
|
|
107
|
+
return (await ctx.cancellable(options?.signal, change.result)).items[0];
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw toPluginError("page-edit", error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
/** What an insert's answer names: the new pages. */
|
|
113
|
+
const inserted = async (change, options) => {
|
|
114
|
+
const item = await answer(change, options);
|
|
115
|
+
return { pages: "insertedPages" in item ? item.insertedPages : NO_PAGES };
|
|
65
116
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
/**
|
|
118
|
+
* A page's rotation once this session's changes on their way apply: the
|
|
119
|
+
* last rotation one of them gives it, else the page's own. What a turn
|
|
120
|
+
* starts from, so two quick quarter turns are a half turn.
|
|
121
|
+
*/
|
|
122
|
+
const rotationOf = (ref) => {
|
|
123
|
+
let rotation = ctx.pageOf(ref).rotation;
|
|
124
|
+
for (const change of ctx.changes.pending()) for (const op of change.shows) {
|
|
125
|
+
if (op.type !== "pages.rotate") continue;
|
|
126
|
+
if (op.pages.some((page) => page.objectNumber === ref.objectNumber)) rotation = op.rotation;
|
|
127
|
+
}
|
|
128
|
+
return rotation;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Rotate pages as one change: one op per rotation they end up with. What
|
|
132
|
+
* undoing it looks like is each page turned back, so a turn right after an
|
|
133
|
+
* undo starts from where the undo leaves the page.
|
|
134
|
+
*/
|
|
135
|
+
const rotate = (refs, rotationAfter) => {
|
|
136
|
+
const after = /* @__PURE__ */ new Map();
|
|
137
|
+
const before = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const ref of refs) {
|
|
139
|
+
const add = (groups, rotation) => groups.set(rotation, [...groups.get(rotation) ?? [], ref]);
|
|
140
|
+
add(before, rotationOf(ref));
|
|
141
|
+
add(after, rotationAfter(ref));
|
|
142
|
+
}
|
|
143
|
+
const opsOf = (groups) => [...groups].map(([rotation, pages]) => ({
|
|
144
|
+
type: "pages.rotate",
|
|
145
|
+
pages,
|
|
146
|
+
rotation
|
|
147
|
+
}));
|
|
148
|
+
return stageChange("rotate", refs.length, opsOf(after), opsOf(before));
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The pages as they show: every change on its way sent and answered first,
|
|
152
|
+
* so a copy holds the edits made before it.
|
|
153
|
+
*/
|
|
154
|
+
const settled = async (options) => {
|
|
155
|
+
ctx.changes.sendHolds();
|
|
156
|
+
await ctx.cancellable(options?.signal, ctx.changes.whenSettled());
|
|
157
|
+
};
|
|
158
|
+
/** Copy pages of this document out, as they show. */
|
|
159
|
+
const extractShown = async (refs, options) => {
|
|
160
|
+
await settled(options);
|
|
161
|
+
return ctx.cancellable(options?.signal, ctx.doc.pages.extract([...refs]));
|
|
162
|
+
};
|
|
163
|
+
/** Stage the insert of a PDF's pages; resolves the new pages. */
|
|
164
|
+
const insertBytes = (verb, count, bytes, position, options) => {
|
|
165
|
+
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
166
|
+
const change = stageChange(verb, count, [{
|
|
167
|
+
type: "pages.insert",
|
|
168
|
+
source,
|
|
169
|
+
position
|
|
170
|
+
}]);
|
|
171
|
+
return inserted(change, options);
|
|
172
|
+
};
|
|
173
|
+
/** Some pages of a PDF: open the bytes beside the document and extract them. */
|
|
174
|
+
const pagesOfBytes = async (bytes, pageIndexes, signal) => {
|
|
175
|
+
const source = await mapErrors(() => ctx.engine.open({
|
|
176
|
+
kind: "bytes",
|
|
177
|
+
id: `page-edit-source-${Date.now().toString(36)}`,
|
|
178
|
+
bytes: new Uint8Array(bytes)
|
|
179
|
+
}, { scope: ["*"] }));
|
|
180
|
+
try {
|
|
181
|
+
const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));
|
|
182
|
+
const refs = pageIndexes.map((index) => {
|
|
183
|
+
const page = layout.pages[index];
|
|
184
|
+
if (!page) throw new PluginError("not-found", "page-edit", `the PDF has no page ${index}`);
|
|
185
|
+
return page.ref;
|
|
186
|
+
});
|
|
187
|
+
return await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));
|
|
188
|
+
} finally {
|
|
189
|
+
await source.close();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
return { api: {
|
|
193
|
+
canEdit: () => ctx.allows(ASSEMBLE),
|
|
194
|
+
canExtract: () => ctx.allows(DOWNLOAD),
|
|
195
|
+
getSelected,
|
|
196
|
+
select,
|
|
197
|
+
rotateBy: async (pages, delta, options) => {
|
|
198
|
+
refuse(ASSEMBLE, "pageEdit.rotateBy", options);
|
|
199
|
+
const refs = refsOf(pages);
|
|
200
|
+
const change = rotate(refs, (ref) => ((rotationOf(ref) + delta) % 360 + 360) % 360);
|
|
201
|
+
await answer(change, options);
|
|
202
|
+
},
|
|
203
|
+
setRotation: async (pages, rotation, options) => {
|
|
204
|
+
refuse(ASSEMBLE, "pageEdit.setRotation", options);
|
|
205
|
+
const change = rotate(refsOf(pages), () => rotation);
|
|
206
|
+
await answer(change, options);
|
|
69
207
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
208
|
+
reorder: async (pages, position, options) => {
|
|
209
|
+
refuse(ASSEMBLE, "pageEdit.reorder", options);
|
|
210
|
+
const refs = refsOf(pages);
|
|
211
|
+
const change = stageChange("reorder", refs.length, [{
|
|
212
|
+
type: "pages.reorder",
|
|
213
|
+
pages: refs,
|
|
214
|
+
position: positionOf(position)
|
|
215
|
+
}]);
|
|
216
|
+
await answer(change, options);
|
|
73
217
|
},
|
|
74
|
-
|
|
75
|
-
|
|
218
|
+
delete: async (pages, options) => {
|
|
219
|
+
refuse(ASSEMBLE, "pageEdit.delete", options);
|
|
220
|
+
const refs = refsOf(pages);
|
|
221
|
+
const deleting = new Set(refs.map((ref) => ref.objectNumber));
|
|
222
|
+
if (registry().every((page) => deleting.has(page.ref.objectNumber))) throw new PluginError("invalid-input", "page-edit", "a document keeps at least one page");
|
|
223
|
+
const change = stageChange("delete", refs.length, [{
|
|
224
|
+
type: "pages.delete",
|
|
225
|
+
pages: refs
|
|
226
|
+
}]);
|
|
227
|
+
await answer(change, options);
|
|
76
228
|
},
|
|
77
|
-
|
|
78
|
-
|
|
229
|
+
insertBlank: async (options = {}) => {
|
|
230
|
+
refuse(ASSEMBLE, "pageEdit.insertBlank", options);
|
|
231
|
+
const position = positionOf(options.position);
|
|
232
|
+
const count = options.count ?? 1;
|
|
233
|
+
const size = options.size ?? sizeAt(position);
|
|
234
|
+
const change = stageChange("insertBlank", count, [{
|
|
235
|
+
type: "pages.insertBlank",
|
|
236
|
+
spec: {
|
|
237
|
+
size,
|
|
238
|
+
count
|
|
239
|
+
},
|
|
240
|
+
position
|
|
241
|
+
}]);
|
|
242
|
+
return inserted(change, options);
|
|
79
243
|
},
|
|
80
|
-
|
|
81
|
-
|
|
244
|
+
insertFromBytes: async (bytes, options = {}) => {
|
|
245
|
+
refuse(ASSEMBLE, "pageEdit.insertFromBytes", options);
|
|
246
|
+
const position = positionOf(options.position);
|
|
247
|
+
const { pageIndexes, signal } = options;
|
|
248
|
+
if (pageIndexes?.length === 0) throw noPages();
|
|
249
|
+
const source = pageIndexes ? await pagesOfBytes(bytes, pageIndexes, signal) : bytes;
|
|
250
|
+
return insertBytes("insert", pageIndexes?.length ?? null, source, position, options);
|
|
82
251
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
252
|
+
insertFromDocument: async (documentId, pages, options = {}) => {
|
|
253
|
+
refuse(ASSEMBLE, "pageEdit.insertFromDocument", options);
|
|
254
|
+
if (pages.length === 0) throw noPages();
|
|
255
|
+
const other = ctx.documentHandle(documentId);
|
|
256
|
+
if (!other) throw new PluginError("not-found", "page-edit", `document '${documentId}' is not open`);
|
|
257
|
+
const otherPages = ctx.get(DocumentsToken).listPages(documentId);
|
|
258
|
+
const refs = pages.map((page) => {
|
|
259
|
+
const found = typeof page === "number" ? otherPages[page] : otherPages.find((candidate) => candidate.ref.objectNumber === page.objectNumber);
|
|
260
|
+
if (!found) throw new PluginError("not-found", "page-edit", `document '${documentId}' has no such page`);
|
|
261
|
+
return found.ref;
|
|
262
|
+
});
|
|
263
|
+
const position = positionOf(options.position);
|
|
264
|
+
const bytes = await mapErrors(() => ctx.cancellable(options.signal, other.pages.extract(refs)));
|
|
265
|
+
return insertBytes("insert", refs.length, bytes, position, options);
|
|
91
266
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
267
|
+
duplicate: async (pages, options = {}) => {
|
|
268
|
+
refuse(ASSEMBLE, "pageEdit.duplicate", options);
|
|
269
|
+
ctx.assertAllowed(DOWNLOAD, "pageEdit.duplicate");
|
|
270
|
+
const refs = refsOf(pages);
|
|
271
|
+
const position = options.position ? positionOf(options.position) : { after: refs[refs.length - 1] };
|
|
272
|
+
const bytes = await extractShown(refs, options);
|
|
273
|
+
return insertBytes("duplicate", refs.length, bytes, position, options);
|
|
274
|
+
},
|
|
275
|
+
extract: async (pages, options) => {
|
|
276
|
+
refuse(DOWNLOAD, "pageEdit.extract", options);
|
|
277
|
+
return extractShown(refsOf(pages), options);
|
|
96
278
|
}
|
|
97
|
-
};
|
|
279
|
+
} };
|
|
98
280
|
}
|
|
99
281
|
//#endregion
|
|
100
282
|
//#region src/page-edit.plugin.ts
|
|
101
283
|
/**
|
|
102
|
-
* Document-scoped
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* home for client-side edit state (pending/optimistic/undo) when it lands —
|
|
106
|
-
* adapters keep their thin hook over `PageEditToken` unchanged.
|
|
284
|
+
* Document-scoped: the document's page edits, each one change of its
|
|
285
|
+
* history, and which of its pages are selected. Turning a page relative to
|
|
286
|
+
* its own rotation lives in the controller, so no app re-derives it.
|
|
107
287
|
*/
|
|
108
288
|
const pageEditPlugin = () => definePlugin({
|
|
109
289
|
id: "page-edit",
|
|
110
290
|
scope: "document",
|
|
111
291
|
token: PageEditToken,
|
|
112
|
-
|
|
292
|
+
state: initialPageEditState,
|
|
293
|
+
create: createPageEditController
|
|
294
|
+
});
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/state.ts
|
|
297
|
+
/**
|
|
298
|
+
* The pages page's State table as code: what `usePageEditState()` returns,
|
|
299
|
+
* and the same fields in every other framework.
|
|
300
|
+
*/
|
|
301
|
+
const pageEditState = defineState(PageEditToken, {
|
|
302
|
+
read: (pageEdit) => ({
|
|
303
|
+
selected: pageEdit.getSelected(),
|
|
304
|
+
canEdit: pageEdit.canEdit(),
|
|
305
|
+
canExtract: pageEdit.canExtract()
|
|
306
|
+
}),
|
|
307
|
+
empty: {
|
|
308
|
+
selected: [],
|
|
309
|
+
canEdit: false,
|
|
310
|
+
canExtract: false
|
|
311
|
+
}
|
|
113
312
|
});
|
|
114
313
|
//#endregion
|
|
115
|
-
export { PageEditToken, pageEditPlugin };
|
|
314
|
+
export { PageEditToken, pageEditPlugin, pageEditState };
|
|
116
315
|
|
|
117
316
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/capability.ts","../src/page-edit.plugin.ts"],"sourcesContent":["import type {\n PluginContext,\n PageObjectNumber,\n PageRotation,\n DocCapability,\n PdfSize,\n} from '@embedpdf/core';\nimport type { PageEditCapability, PagePlacement } from './types';\n\n/**\n * Authorization for structural page edits. Maps to PDF bit 11 (ASSEMBLE =\n * insert/rotate/delete pages). The engine ALSO enforces this — every\n * page-structure verb throws `PermissionDenied` without it, identically on\n * local and cloud — so `canEdit()` is the UI mirror of a real guard, not the\n * guard itself. Typed as `DocCapability` so a typo is a compile error.\n */\nconst ASSEMBLE_CAPABILITY: DocCapability = 'doc.pages.assemble';\n\n/**\n * Forwards structural edits to the document's engine handle, addressed by PON.\n * The handle (`ctx.doc`) and the page registry (`ctx.document()`, kept in sync\n * by the kernel's event→registry bridge) are both already on the plugin\n * context — this capability is the thin layer that turns the relative rotate\n * gesture into the engine's absolute wire.\n */\nexport function createPageEditCapability(ctx: PluginContext<unknown>): PageEditCapability {\n const requireDoc = () => {\n const doc = ctx.doc;\n if (!doc) throw new Error('[page-edit] no document bound');\n return doc;\n };\n\n /** Current absolute rotation of a page from the registry; 0 if unknown. */\n const rotationOf = (pon: PageObjectNumber): PageRotation => {\n const page = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n return page?.rotation ?? 0;\n };\n\n /**\n * Resolve a placement to the engine's index wire, from the registry at\n * call time — the PON anchors are why \"add after this thumbnail\" survives\n * a concurrent reorder. Returns the anchor page too, so `addBlank` can\n * default its size without a second lookup.\n */\n const resolvePlacement = (placement?: PagePlacement) => {\n if (!placement) return { destIndex: undefined, anchor: undefined };\n if ('index' in placement) return { destIndex: placement.index, anchor: undefined };\n const pon = 'after' in placement ? placement.after : placement.before;\n const anchor = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);\n if (!anchor) throw new Error(`[page-edit] placement page not found: ${pon}`);\n return { destIndex: 'after' in placement ? anchor.index + 1 : anchor.index, anchor };\n };\n\n /**\n * Default blank-page size: the insertion point's predecessor (the page\n * the new one will follow), else the first page, else — only reachable\n * with an empty registry, which an open document never has — US Letter.\n */\n const neighbourSize = (destIndex: number | undefined): PdfSize => {\n const pages = ctx.document()?.pages ?? [];\n if (pages.length === 0) return { width: 612, height: 792 };\n if (destIndex === undefined) return pages[pages.length - 1].size;\n return pages[Math.max(0, Math.min(destIndex - 1, pages.length - 1))].size;\n };\n\n return {\n canEdit() {\n // Wildcard-aware predicate (mirrors the engine's own enforcement),\n // identical on both engines. NOT `effectiveScope.includes(...)` — that\n // enumeration drops the `*` admin grant, so it would hide the UI on a\n // default admin open even though the engine allows the edit.\n return ctx.doc?.security.allows(ASSEMBLE_CAPABILITY) ?? false;\n },\n\n rotateBy(pon, delta) {\n // Wrap to [0, 360) — the double-mod keeps -90 from current 0 landing on 270.\n const next = ((((rotationOf(pon) + delta) % 360) + 360) % 360) as PageRotation;\n return requireDoc().pages.rotate([pon], next);\n },\n\n setRotation(pons, rotation) {\n return requireDoc().pages.rotate(pons, rotation);\n },\n\n move(pons, destIndex) {\n return requireDoc().pages.move(pons, destIndex);\n },\n\n delete(pons) {\n return requireDoc().pages.delete(pons);\n },\n\n addBlank(opts = {}) {\n const doc = requireDoc();\n const { destIndex, anchor } = resolvePlacement(opts.placement);\n // PON placements match the anchor the user is looking at; everything\n // else matches the neighbour the new page will follow.\n const size = opts.size ?? anchor?.size ?? neighbourSize(destIndex);\n return doc.pages.insertBlank({ size, count: opts.count }, destIndex);\n },\n\n insert(bytes, opts = {}) {\n const doc = requireDoc();\n const { destIndex } = resolvePlacement(opts.placement);\n return doc.pages.insert(bytes, destIndex);\n },\n };\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { createPageEditCapability } from './capability';\nimport { PageEditToken } from './types';\n\n/**\n * Document-scoped, stateless: turns the engine handle's page service into a\n * PON-addressed edit capability. The relative→absolute rotation lives in the\n * capability so the four framework adapters never re-derive it, and this is the\n * home for client-side edit state (pending/optimistic/undo) when it lands —\n * adapters keep their thin hook over `PageEditToken` unchanged.\n */\nexport const pageEditPlugin = () =>\n definePlugin({\n id: 'page-edit',\n scope: 'document',\n token: PageEditToken,\n capability: createPageEditCapability,\n });\n"],"mappings":";;;;;;;;;;AAgBA,MAAM,sBAAqC;;;;;;;;AAS3C,SAAgB,yBAAyB,KAAiD;CACxF,MAAM,mBAAmB;EACvB,MAAM,MAAM,IAAI;EAChB,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,+BAA+B;EACzD,OAAO;CACT;;CAGA,MAAM,cAAc,QAAwC;EAE1D,QADa,IAAI,SAAS,CAAC,EAAE,MAAM,MAAM,MAAM,EAAE,qBAAqB,GAAG,EAAA,EAC5D,YAAY;CAC3B;;;;;;;CAQA,MAAM,oBAAoB,cAA8B;EACtD,IAAI,CAAC,WAAW,OAAO;GAAE,WAAW,KAAA;GAAW,QAAQ,KAAA;EAAU;EACjE,IAAI,WAAW,WAAW,OAAO;GAAE,WAAW,UAAU;GAAO,QAAQ,KAAA;EAAU;EACjF,MAAM,MAAM,WAAW,YAAY,UAAU,QAAQ,UAAU;EAC/D,MAAM,SAAS,IAAI,SAAS,CAAC,EAAE,MAAM,MAAM,MAAM,EAAE,qBAAqB,GAAG;EAC3E,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,yCAAyC,KAAK;EAC3E,OAAO;GAAE,WAAW,WAAW,YAAY,OAAO,QAAQ,IAAI,OAAO;GAAO;EAAO;CACrF;;;;;;CAOA,MAAM,iBAAiB,cAA2C;EAChE,MAAM,QAAQ,IAAI,SAAS,CAAC,EAAE,SAAS,CAAC;EACxC,IAAI,MAAM,WAAW,GAAG,OAAO;GAAE,OAAO;GAAK,QAAQ;EAAI;EACzD,IAAI,cAAc,KAAA,GAAW,OAAO,MAAM,MAAM,SAAS,EAAE,CAAC;EAC5D,OAAO,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,YAAY,GAAG,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC;CACvE;CAEA,OAAO;EACL,UAAU;GAKR,OAAO,IAAI,KAAK,SAAS,OAAO,mBAAmB,KAAK;EAC1D;EAEA,SAAS,KAAK,OAAO;GAEnB,MAAM,SAAW,WAAW,GAAG,IAAI,SAAS,MAAO,OAAO;GAC1D,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,GAAG,IAAI;EAC9C;EAEA,YAAY,MAAM,UAAU;GAC1B,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,MAAM,QAAQ;EACjD;EAEA,KAAK,MAAM,WAAW;GACpB,OAAO,WAAW,CAAC,CAAC,MAAM,KAAK,MAAM,SAAS;EAChD;EAEA,OAAO,MAAM;GACX,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,IAAI;EACvC;EAEA,SAAS,OAAO,CAAC,GAAG;GAClB,MAAM,MAAM,WAAW;GACvB,MAAM,EAAE,WAAW,WAAW,iBAAiB,KAAK,SAAS;GAG7D,MAAM,OAAO,KAAK,QAAQ,QAAQ,QAAQ,cAAc,SAAS;GACjE,OAAO,IAAI,MAAM,YAAY;IAAE;IAAM,OAAO,KAAK;GAAM,GAAG,SAAS;EACrE;EAEA,OAAO,OAAO,OAAO,CAAC,GAAG;GACvB,MAAM,MAAM,WAAW;GACvB,MAAM,EAAE,cAAc,iBAAiB,KAAK,SAAS;GACrD,OAAO,IAAI,MAAM,OAAO,OAAO,SAAS;EAC1C;CACF;AACF;;;;;;;;;;AChGA,MAAa,uBACX,aAAa;CACX,IAAI;CACJ,OAAO;CACP,OAAO;CACP,YAAY;AACd,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/model.ts","../src/controller.ts","../src/page-edit.plugin.ts","../src/state.ts"],"sourcesContent":["/**\n * The page-edit session state: which pages are selected. A plain list of\n * refs, changed only by the transitions here; the controller reads it over the\n * document's pages, so a page that went leaves the selection.\n */\nimport type { PageRef } from '@embedpdf/core';\n\nexport interface PageEditState {\n /** The selected pages, in the order they were selected. */\n readonly selected: readonly PageRef[];\n}\n\nexport const initialPageEditState = (): PageEditState => ({ selected: [] });\n\n/** The selection becomes `refs`, each page once. */\nexport function selected(state: PageEditState, refs: readonly PageRef[]): PageEditState {\n const numbers = refs.map((ref) => ref.objectNumber);\n const unique = refs.filter((_ref, i) => numbers.indexOf(numbers[i]!) === i);\n const same =\n unique.length === state.selected.length &&\n unique.every((ref, i) => ref.objectNumber === state.selected[i]!.objectNumber);\n return same ? state : { ...state, selected: unique };\n}\n","/**\n * The page-edit controller: page edits that take refs or indexes, and the\n * page selection. Turning a page relative to its own rotation, and fixing a\n * position's page, live here once instead of in every app's click handler.\n *\n * A verb refuses before anything starts (permission, pages, position), then\n * stages its edit as one change on the document's queue (`ctx.changes`): one\n * step of the history, sent after everything staged before it. The page list\n * shows it once the engine answers; nothing predicts pages yet.\n */\nimport { memo, PluginError, toPluginError, DocumentsToken } from '@embedpdf/core';\nimport type {\n ChangeLabel,\n DocCapability,\n OperationOptions,\n PageLayout,\n PagePosition,\n PageRef,\n PdfRotation,\n PdfSize,\n PendingChange,\n PluginContext,\n PredictedOp,\n} from '@embedpdf/core';\n\nimport type { PageEditCapability, PageEditInsertResult } from './contract';\nimport { selected, type PageEditState } from './model';\n\n/** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */\nconst ASSEMBLE: DocCapability = 'doc.pages.assemble';\n/** Copying pages out of a document is a partial download. The engine enforces it too. */\nconst DOWNLOAD: DocCapability = 'doc.download';\n\n/** The size of a blank page when the document has no page to match: Letter, in points. */\nconst LETTER_SIZE: PdfSize = { width: 612, height: 792 };\n\nconst NO_PAGES: readonly PageRef[] = Object.freeze([]);\nconst NO_LAYOUTS: readonly PageLayout[] = Object.freeze([]);\n\n/** A page op, as a verb stages it. */\ntype PageOp = Extract<PredictedOp, { type: `pages.${string}` }>;\n\n/** What a change is called in history and refusals. */\ntype PageVerb = 'rotate' | 'reorder' | 'delete' | 'insertBlank' | 'insert' | 'duplicate';\n\nexport function createPageEditController(ctx: PluginContext<PageEditState>) {\n /** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */\n const mapErrors = async <T>(work: () => Promise<T>): Promise<T> => {\n try {\n return await work();\n } catch (error) {\n throw toPluginError('page-edit', error);\n }\n };\n const registry = (): readonly PageLayout[] => ctx.document()?.pages ?? NO_LAYOUTS;\n const noPages = () => new PluginError('invalid-input', 'page-edit', 'no pages given');\n\n /**\n * A verb's pages as refs, checked when it is called: an index names the\n * page at that position now, which is the page the user clicked.\n */\n const refsOf = (pages: readonly (PageRef | number)[]): PageRef[] => {\n if (pages.length === 0) throw noPages();\n return pages.map((page) => ctx.pageOf(page).ref);\n };\n\n /** A position, its page checked when the verb is called, like the verb's own pages. Default `'end'`. */\n const positionOf = (position: PagePosition | undefined): PagePosition => {\n if (!position || position === 'start' || position === 'end') return position ?? 'end';\n if ('after' in position) return { after: ctx.pageOf(position.after).ref };\n return { before: ctx.pageOf(position.before).ref };\n };\n /**\n * Default blank-page size: the page the position names, else the page the\n * new ones go next to (the first or the last), else Letter.\n */\n const sizeAt = (position: PagePosition): PdfSize => {\n if (typeof position === 'object') {\n return ctx.pageOf('after' in position ? position.after : position.before).size;\n }\n const pages = registry();\n if (pages.length === 0) return LETTER_SIZE;\n return (position === 'start' ? pages[0] : pages[pages.length - 1]).size;\n };\n\n /* ── the selection ─────────────────────────────────────────────────────── */\n\n const getSelected = memo(\n () => [registry(), ctx.state.get().selected] as const,\n (pages, refs) => {\n if (refs.length === 0) return NO_PAGES;\n const present = new Set(pages.map((page) => page.ref.objectNumber));\n return refs.filter((ref) => present.has(ref.objectNumber));\n },\n );\n const select = (pages: readonly (PageRef | number)[]): void => {\n const refs = pages.flatMap((page) => {\n const found = ctx.getPage(page);\n return found ? [found.ref] : [];\n });\n ctx.state.update(selected, refs);\n };\n\n /* ── writes ────────────────────────────────────────────────────────────── */\n\n /** Refuse before anything starts: without the right, or once the caller cancelled. */\n const refuse = (\n permission: DocCapability,\n operation: string,\n options: OperationOptions | undefined,\n ): void => {\n ctx.assertAllowed(permission, operation);\n if (options?.signal?.aborted) {\n throw new PluginError('operation-cancelled', 'page-edit', `${operation} was cancelled`);\n }\n };\n\n /**\n * Stage one user action: one change, one step of the history. Its label\n * counts the pages it acts on, when the verb knows how many.\n */\n const stageChange = (\n verb: PageVerb,\n count: number | null,\n ops: readonly PageOp[],\n undo?: readonly PageOp[],\n ): PendingChange => {\n const label: ChangeLabel =\n count === null ? { key: `pages.${verb}` } : { key: `pages.${verb}`, count };\n return ctx.changes.stage({ label, ops, ...(undo ? { undo } : {}) });\n };\n\n /** The engine's answer to a staged change: its first item. */\n const answer = async (change: PendingChange, options: OperationOptions | undefined) => {\n try {\n const result = await ctx.cancellable(options?.signal, change.result);\n return result.items[0]!;\n } catch (error) {\n throw toPluginError('page-edit', error);\n }\n };\n\n /** What an insert's answer names: the new pages. */\n const inserted = async (\n change: PendingChange,\n options: OperationOptions | undefined,\n ): Promise<PageEditInsertResult> => {\n const item = await answer(change, options);\n return { pages: 'insertedPages' in item ? item.insertedPages : NO_PAGES };\n };\n\n /**\n * A page's rotation once this session's changes on their way apply: the\n * last rotation one of them gives it, else the page's own. What a turn\n * starts from, so two quick quarter turns are a half turn.\n */\n const rotationOf = (ref: PageRef): PdfRotation => {\n let rotation = ctx.pageOf(ref).rotation;\n for (const change of ctx.changes.pending()) {\n for (const op of change.shows) {\n if (op.type !== 'pages.rotate') continue;\n if (op.pages.some((page) => page.objectNumber === ref.objectNumber)) {\n rotation = op.rotation;\n }\n }\n }\n return rotation;\n };\n\n /**\n * Rotate pages as one change: one op per rotation they end up with. What\n * undoing it looks like is each page turned back, so a turn right after an\n * undo starts from where the undo leaves the page.\n */\n const rotate = (refs: readonly PageRef[], rotationAfter: (ref: PageRef) => PdfRotation) => {\n const after = new Map<PdfRotation, PageRef[]>();\n const before = new Map<PdfRotation, PageRef[]>();\n for (const ref of refs) {\n const add = (groups: Map<PdfRotation, PageRef[]>, rotation: PdfRotation) =>\n groups.set(rotation, [...(groups.get(rotation) ?? []), ref]);\n add(before, rotationOf(ref));\n add(after, rotationAfter(ref));\n }\n const opsOf = (groups: Map<PdfRotation, PageRef[]>): PageOp[] =>\n [...groups].map(([rotation, pages]) => ({ type: 'pages.rotate', pages, rotation }));\n return stageChange('rotate', refs.length, opsOf(after), opsOf(before));\n };\n\n /**\n * The pages as they show: every change on its way sent and answered first,\n * so a copy holds the edits made before it.\n */\n const settled = async (options: OperationOptions | undefined): Promise<void> => {\n ctx.changes.sendHolds();\n await ctx.cancellable(options?.signal, ctx.changes.whenSettled());\n };\n\n /** Copy pages of this document out, as they show. */\n const extractShown = async (\n refs: readonly PageRef[],\n options: OperationOptions | undefined,\n ): Promise<Uint8Array> => {\n await settled(options);\n return ctx.cancellable(options?.signal, ctx.doc.pages.extract([...refs]));\n };\n\n /** Stage the insert of a PDF's pages; resolves the new pages. */\n const insertBytes = (\n verb: 'insert' | 'duplicate',\n count: number | null,\n bytes: Uint8Array | ArrayBuffer,\n position: PagePosition,\n options: OperationOptions | undefined,\n ): Promise<PageEditInsertResult> => {\n const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n const change = stageChange(verb, count, [{ type: 'pages.insert', source, position }]);\n return inserted(change, options);\n };\n\n /** Some pages of a PDF: open the bytes beside the document and extract them. */\n const pagesOfBytes = async (\n bytes: Uint8Array | ArrayBuffer,\n pageIndexes: readonly number[],\n signal: AbortSignal | undefined,\n ): Promise<Uint8Array> => {\n const source = await mapErrors(() =>\n ctx.engine.open(\n {\n kind: 'bytes',\n id: `page-edit-source-${Date.now().toString(36)}`,\n bytes: new Uint8Array(bytes),\n },\n { scope: ['*'] },\n ),\n );\n try {\n const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));\n const refs = pageIndexes.map((index) => {\n const page = layout.pages[index];\n if (!page) {\n throw new PluginError('not-found', 'page-edit', `the PDF has no page ${index}`);\n }\n return page.ref;\n });\n return await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));\n } finally {\n await source.close();\n }\n };\n\n const api: PageEditCapability = {\n canEdit: () => ctx.allows(ASSEMBLE),\n canExtract: () => ctx.allows(DOWNLOAD),\n\n getSelected,\n select,\n\n rotateBy: async (pages, delta, options) => {\n refuse(ASSEMBLE, 'pageEdit.rotateBy', options);\n const refs = refsOf(pages);\n // The double modulo keeps -90 from 0 at 270.\n const change = rotate(\n refs,\n (ref) => ((((rotationOf(ref) + delta) % 360) + 360) % 360) as PdfRotation,\n );\n await answer(change, options);\n },\n\n setRotation: async (pages, rotation, options) => {\n refuse(ASSEMBLE, 'pageEdit.setRotation', options);\n const change = rotate(refsOf(pages), () => rotation);\n await answer(change, options);\n },\n\n reorder: async (pages, position, options) => {\n refuse(ASSEMBLE, 'pageEdit.reorder', options);\n const refs = refsOf(pages);\n const change = stageChange('reorder', refs.length, [\n { type: 'pages.reorder', pages: refs, position: positionOf(position) },\n ]);\n await answer(change, options);\n },\n\n delete: async (pages, options) => {\n refuse(ASSEMBLE, 'pageEdit.delete', options);\n const refs = refsOf(pages);\n const deleting = new Set(refs.map((ref) => ref.objectNumber));\n if (registry().every((page) => deleting.has(page.ref.objectNumber))) {\n throw new PluginError('invalid-input', 'page-edit', 'a document keeps at least one page');\n }\n const change = stageChange('delete', refs.length, [{ type: 'pages.delete', pages: refs }]);\n await answer(change, options);\n },\n\n insertBlank: async (options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.insertBlank', options);\n const position = positionOf(options.position);\n const count = options.count ?? 1;\n const size = options.size ?? sizeAt(position);\n const change = stageChange('insertBlank', count, [\n { type: 'pages.insertBlank', spec: { size, count }, position },\n ]);\n return inserted(change, options);\n },\n\n insertFromBytes: async (bytes, options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.insertFromBytes', options);\n const position = positionOf(options.position);\n const { pageIndexes, signal } = options;\n if (pageIndexes?.length === 0) throw noPages();\n const source = pageIndexes ? await pagesOfBytes(bytes, pageIndexes, signal) : bytes;\n return insertBytes('insert', pageIndexes?.length ?? null, source, position, options);\n },\n\n insertFromDocument: async (documentId, pages, options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.insertFromDocument', options);\n if (pages.length === 0) throw noPages();\n const other = ctx.documentHandle(documentId);\n if (!other) {\n throw new PluginError('not-found', 'page-edit', `document '${documentId}' is not open`);\n }\n // The pages are the other document's, so its page list resolves them.\n const otherPages = ctx.get(DocumentsToken).listPages(documentId);\n const refs = pages.map((page) => {\n const found =\n typeof page === 'number'\n ? otherPages[page]\n : otherPages.find((candidate) => candidate.ref.objectNumber === page.objectNumber);\n if (!found) {\n throw new PluginError(\n 'not-found',\n 'page-edit',\n `document '${documentId}' has no such page`,\n );\n }\n return found.ref;\n });\n const position = positionOf(options.position);\n // The other document's session decides whether its pages may be copied\n // out: the engine refuses `doc.download` there with the permission named.\n const bytes = await mapErrors(() =>\n ctx.cancellable(options.signal, other.pages.extract(refs)),\n );\n return insertBytes('insert', refs.length, bytes, position, options);\n },\n\n duplicate: async (pages, options = {}) => {\n refuse(ASSEMBLE, 'pageEdit.duplicate', options);\n ctx.assertAllowed(DOWNLOAD, 'pageEdit.duplicate');\n const refs = refsOf(pages);\n const position = options.position\n ? positionOf(options.position)\n : { after: refs[refs.length - 1]! };\n const bytes = await extractShown(refs, options);\n return insertBytes('duplicate', refs.length, bytes, position, options);\n },\n\n extract: async (pages, options) => {\n refuse(DOWNLOAD, 'pageEdit.extract', options);\n return extractShown(refsOf(pages), options);\n },\n };\n return { api };\n}\n","import { definePlugin } from '@embedpdf/core';\n\nimport { PageEditToken } from './contract';\nimport { createPageEditController } from './controller';\nimport { initialPageEditState } from './model';\n\n/**\n * Document-scoped: the document's page edits, each one change of its\n * history, and which of its pages are selected. Turning a page relative to\n * its own rotation lives in the controller, so no app re-derives it.\n */\nexport const pageEditPlugin = () =>\n definePlugin({\n id: 'page-edit',\n scope: 'document',\n token: PageEditToken,\n state: initialPageEditState,\n create: createPageEditController,\n });\n","/**\n * The pages page's State table as code: what `usePageEditState()` returns,\n * and the same fields in every other framework.\n */\nimport { defineState } from '@embedpdf/core';\n\nimport { PageEditToken } from './contract';\n\nexport const pageEditState = defineState(PageEditToken, {\n read: (pageEdit) => ({\n selected: pageEdit.getSelected(),\n canEdit: pageEdit.canEdit(),\n canExtract: pageEdit.canExtract(),\n }),\n empty: {\n selected: [],\n canEdit: false,\n canExtract: false,\n },\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embedpdf/plugin-page-edit",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.16",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Document-scoped page-edit capability:
|
|
5
|
+
"description": "Document-scoped page-edit capability: ref-addressed rotate/move/delete/insert over the engine DocumentHandle, each one change of the history, and the page selection. Owns relative→absolute rotation and position resolution so framework adapters never re-derive them.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"module": "./dist/index.js",
|
|
@@ -22,11 +22,14 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@embedpdf/core": "3.0.0-next.
|
|
25
|
+
"@embedpdf/core": "3.0.0-next.16"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"typescript": "^5.0.0",
|
|
29
29
|
"vitest": "^2.1.9",
|
|
30
|
+
"@embedpdf/engine": "3.0.0-next.16",
|
|
31
|
+
"@embedpdf/engine-core": "3.0.0-next.16",
|
|
32
|
+
"@embedpdf/plugin-history": "3.0.0-next.16",
|
|
30
33
|
"@embedpdf/tooling-build": "0.0.0"
|
|
31
34
|
},
|
|
32
35
|
"license": "Apache-2.0",
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { AbortablePromise, PageDeleteResult, PageInsertResult, PageMoveResult, PageObjectNumber, PageRotateResult, PageRotation, PdfSize } from "@embedpdf/core";
|
|
2
|
-
//#region src/types.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Where added pages land. The PON anchors are the thumbnail gestures ("+
|
|
5
|
-
* after this page") and stay correct across a concurrent reorder between
|
|
6
|
-
* click and call — a raw index does not; `index` remains for absolute
|
|
7
|
-
* positions ("at the start"). Omitted → append. Resolved to the engine's
|
|
8
|
-
* index wire by the capability at call time, from the registry.
|
|
9
|
-
*/
|
|
10
|
-
type PagePlacement = {
|
|
11
|
-
after: PageObjectNumber;
|
|
12
|
-
} | {
|
|
13
|
-
before: PageObjectNumber;
|
|
14
|
-
} | {
|
|
15
|
-
index: number;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Structural page edits, addressed by durable PON (never display index — an
|
|
19
|
-
* index shifts the moment a sibling is moved or deleted). Mirrors the engine's
|
|
20
|
-
* `DocumentPagesService` 1:1, with one addition the engine can't have: the
|
|
21
|
-
* relative `rotateBy` gesture. The engine wire is always ABSOLUTE; turning a
|
|
22
|
-
* "+90° on this thumbnail" click into an absolute value needs the page's
|
|
23
|
-
* current rotation, so that read+arithmetic lives HERE — once — instead of
|
|
24
|
-
* being re-derived in every framework adapter's click handler.
|
|
25
|
-
*
|
|
26
|
-
* Document-scoped: rotation is shared document metadata, so editing through the
|
|
27
|
-
* sidebar lens and seeing it in the main lens is automatic (both read the same
|
|
28
|
-
* registry, which the kernel keeps in sync from the engine's event stream).
|
|
29
|
-
*/
|
|
30
|
-
interface PageEditCapability {
|
|
31
|
-
/**
|
|
32
|
-
* Whether this caller is authorized to perform structural page edits —
|
|
33
|
-
* `effectiveScope` includes `doc.pages.assemble` (PDF bit 11). UIs gate
|
|
34
|
-
* ALL their edit affordances on this, the add verbs included (rotate,
|
|
35
|
-
* move, delete, addBlank, and insert share the one capability); the
|
|
36
|
-
* engine independently enforces the same capability and throws
|
|
37
|
-
* `PermissionDenied` if a call slips through.
|
|
38
|
-
*/
|
|
39
|
-
canEdit(): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Rotate a SINGLE page by a relative quarter-turn — the per-thumbnail button
|
|
42
|
-
* gesture. Reads the page's current rotation from the registry and forwards
|
|
43
|
-
* the resulting absolute rotation to the engine.
|
|
44
|
-
*/
|
|
45
|
-
rotateBy(pon: PageObjectNumber, delta: 90 | -90): AbortablePromise<PageRotateResult>;
|
|
46
|
-
/**
|
|
47
|
-
* Set the ABSOLUTE rotation of one or more pages to a single shared value —
|
|
48
|
-
* the multi-select gesture. Maps 1:1 to the engine.
|
|
49
|
-
*/
|
|
50
|
-
setRotation(pons: PageObjectNumber[], rotation: PageRotation): AbortablePromise<PageRotateResult>;
|
|
51
|
-
/** Reorder pages (by PON) as a contiguous block starting at `destIndex`. */
|
|
52
|
-
move(pons: PageObjectNumber[], destIndex: number): AbortablePromise<PageMoveResult>;
|
|
53
|
-
/** Delete pages by PON. The engine rejects deleting every page. */
|
|
54
|
-
delete(pons: PageObjectNumber[]): AbortablePromise<PageDeleteResult>;
|
|
55
|
-
/**
|
|
56
|
-
* Create blank pages — gate the affordance on `canEdit()`, like every
|
|
57
|
-
* other structure verb (the add verbs share the `doc.pages.assemble`
|
|
58
|
-
* capability and are mandatory on the engine contract). `size` defaults
|
|
59
|
-
* to the page the new pages sit beside — the anchor for PON placements,
|
|
60
|
-
* else the insertion point's predecessor, else the last page — so "add
|
|
61
|
-
* page" matches the document without an A4-vs-Letter guess. That default
|
|
62
|
-
* is a registry read, which is why it lives HERE and the engine wire
|
|
63
|
-
* stays explicit (the `rotateBy` law). `count` defaults to 1.
|
|
64
|
-
*/
|
|
65
|
-
addBlank(opts?: {
|
|
66
|
-
size?: PdfSize;
|
|
67
|
-
count?: number;
|
|
68
|
-
placement?: PagePlacement;
|
|
69
|
-
}): AbortablePromise<PageInsertResult>;
|
|
70
|
-
/**
|
|
71
|
-
* Copy every page of a standalone PDF (`bytes`) in at the placement —
|
|
72
|
-
* the merge/import gesture. Forwards to `pages.insert`; the inserted
|
|
73
|
-
* copies get fresh PONs, returned in insertion order.
|
|
74
|
-
*/
|
|
75
|
-
insert(bytes: Uint8Array | ArrayBuffer, opts?: {
|
|
76
|
-
placement?: PagePlacement;
|
|
77
|
-
}): AbortablePromise<PageInsertResult>;
|
|
78
|
-
}
|
|
79
|
-
declare const PageEditToken: import("@embedpdf/core").CapabilityToken<PageEditCapability>;
|
|
80
|
-
//#endregion
|
|
81
|
-
export { PageEditToken as n, PageEditCapability as t };
|
|
82
|
-
//# sourceMappingURL=contract-BnXi11KD.d.cts.map
|