@embedpdf/plugin-page-edit 3.0.0-next.15 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["PluginError","DocumentsToken","PageEditToken"],"sources":["../src/controller.ts","../src/page-edit.plugin.ts"],"sourcesContent":["/**\n * The page-edit controller. Stateless: it turns the engine handle's page\n * service into page edits that take refs or indexes. Turning a page relative\n * to its own rotation, and fixing a placement's page, live here once instead\n * of in every app's click handler.\n *\n * A verb refuses before anything starts (permission, pages, placement), then\n * queues its edit: edits run one at a time, in the order they were called.\n */\nimport { DocumentsToken, PluginError, toPluginError } from '@embedpdf/core';\nimport type {\n DocCapability,\n PageInfo,\n PagePosition,\n PageRef,\n PdfRotation,\n PdfSize,\n PluginContext,\n} from '@embedpdf/core';\n\nimport type { PageEditCapability, PageEditInsertResult, PagePlacement } from './contract';\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\nexport function createPageEditController(ctx: PluginContext<void>) {\n const enqueue = ctx.serialQueue('edits');\n // A download waits for the page edits on their way.\n ctx.onSettle(() => enqueue.idle());\n\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 PageInfo[] => ctx.document()?.pages ?? [];\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 /**\n * A placement as the engine's position, its page fixed when the verb is\n * called, like the verb's own pages. Default `'end'`.\n */\n const positionOf = (placement: PagePlacement | undefined): PagePosition => {\n if (!placement || placement === 'start' || placement === 'end') return placement ?? 'end';\n if ('after' in placement) return { after: ctx.pageOf(placement.after).ref };\n return { before: ctx.pageOf(placement.before).ref };\n };\n /**\n * Default blank-page size: the page the placement 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 /** Insert a PDF's pages; resolves the new pages. */\n const insertBytes = async (\n bytes: Uint8Array | ArrayBuffer,\n position: PagePosition,\n signal: AbortSignal | undefined,\n ): Promise<PageEditInsertResult> => {\n const result = await ctx.cancellable(signal, ctx.doc.pages.insert(bytes, position));\n return { pages: result.insertedPages };\n };\n\n const api: PageEditCapability = {\n canEdit: () => ctx.allows(ASSEMBLE),\n canExtract: () => ctx.allows(DOWNLOAD),\n\n rotateBy: async (pages, delta, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.rotateBy');\n const refs = refsOf(pages);\n const signal = options?.signal;\n await enqueue(async () => {\n // The engine sets one rotation per call, so pages are grouped by where\n // they end up. Each page's rotation is read when the edit runs, after\n // the edits queued before it; the double modulo keeps -90 from 0 at 270.\n const groups = new Map<PdfRotation, PageRef[]>();\n for (const ref of refs) {\n const turned = (ctx.pageOf(ref).rotation + delta) % 360;\n const rotation = ((turned + 360) % 360) as PdfRotation;\n groups.set(rotation, [...(groups.get(rotation) ?? []), ref]);\n }\n for (const [rotation, group] of groups) {\n await ctx.cancellable(signal, ctx.doc.pages.rotate(group, rotation));\n }\n }, options);\n },\n\n setRotation: async (pages, rotation, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.setRotation');\n const refs = refsOf(pages);\n await enqueue(\n () => ctx.cancellable(options?.signal, ctx.doc.pages.rotate(refs, rotation)),\n options,\n );\n },\n\n reorder: async (pages, placement, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.reorder');\n const refs = refsOf(pages);\n const position = positionOf(placement);\n await enqueue(\n () => ctx.cancellable(options?.signal, ctx.doc.pages.reorder(refs, position)),\n options,\n );\n },\n\n delete: async (pages, options) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.delete');\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 await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.delete(refs)), options);\n },\n\n insertBlank: async (options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.insertBlank');\n const position = positionOf(options.placement);\n return enqueue(async () => {\n const size = options.size ?? sizeAt(position);\n const result = await ctx.cancellable(\n options.signal,\n ctx.doc.pages.insertBlank({ size, count: options.count }, position),\n );\n return { pages: result.insertedPages };\n }, options);\n },\n\n insertFromBytes: async (bytes, options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.insertFromBytes');\n const position = positionOf(options.placement);\n const { pageIndexes, signal } = options;\n if (pageIndexes?.length === 0) throw noPages();\n return enqueue(async () => {\n if (!pageIndexes) return insertBytes(bytes, position, signal);\n // Some of the pages: open the bytes beside the document, extract them, insert those.\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 const subset = await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));\n return await insertBytes(subset, position, signal);\n } finally {\n await source.close();\n }\n }, options);\n },\n\n insertFromDocument: async (documentId, pages, options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.insertFromDocument');\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.placement);\n return enqueue(async () => {\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(bytes, position, options.signal);\n }, options);\n },\n\n duplicate: async (pages, options = {}) => {\n ctx.assertAllowed(ASSEMBLE, 'pageEdit.duplicate');\n ctx.assertAllowed(DOWNLOAD, 'pageEdit.duplicate');\n const refs = refsOf(pages);\n const position = options.placement\n ? positionOf(options.placement)\n : { after: refs[refs.length - 1]! };\n return enqueue(async () => {\n const bytes = await ctx.cancellable(options.signal, ctx.doc.pages.extract(refs));\n return insertBytes(bytes, position, options.signal);\n }, options);\n },\n\n extract: async (pages, options) => {\n ctx.assertAllowed(DOWNLOAD, 'pageEdit.extract');\n const refs = refsOf(pages);\n return enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.extract(refs)), options);\n },\n };\n return { api };\n}\n","import { definePlugin } from '@embedpdf/core';\n\nimport { PageEditToken } from './contract';\nimport { createPageEditController } from './controller';\n\n/**\n * Document-scoped and stateless: the engine handle's page service as page\n * edits that take refs or indexes. Turning a page relative to its own\n * 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 create: createPageEditController,\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["PluginError","DocumentsToken","PageEditToken","PageEditToken"],"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/dist/index.d.cts CHANGED
@@ -1,11 +1,25 @@
1
- import { a as PagePlacement, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePlacementOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-dl4LfrRX.cjs";
1
+ import { a as PagePosition, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePositionOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-IN8rk5LG.cjs";
2
+ import { PageRef } from "@embedpdf/core";
3
+ //#region src/model.d.ts
4
+ interface PageEditState {
5
+ /** The selected pages, in the order they were selected. */
6
+ readonly selected: readonly PageRef[];
7
+ }
8
+ //#endregion
2
9
  //#region src/page-edit.plugin.d.ts
3
10
  /**
4
- * Document-scoped and stateless: the engine handle's page service as page
5
- * edits that take refs or indexes. Turning a page relative to its own
6
- * rotation lives in the controller, so no app re-derives it.
11
+ * Document-scoped: the document's page edits, each one change of its
12
+ * history, and which of its pages are selected. Turning a page relative to
13
+ * its own rotation lives in the controller, so no app re-derives it.
7
14
  */
8
- declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<void, PageEditCapability, import("@embedpdf/core").NoSettings>;
15
+ declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<PageEditState, PageEditCapability, import("@embedpdf/core").NoSettings>;
16
+ //#endregion
17
+ //#region src/state.d.ts
18
+ declare const pageEditState: import("@embedpdf/core").StateDeclaration<PageEditCapability, {
19
+ selected: readonly import("@embedpdf/core").PageRef[];
20
+ canEdit: boolean;
21
+ canExtract: boolean;
22
+ }>;
9
23
  //#endregion
10
- export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, PagePlacement, PagePlacementOptions, type PdfRotation, type PdfSize, pageEditPlugin };
24
+ export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, type PagePosition, PagePositionOptions, type PdfRotation, type PdfSize, pageEditPlugin, pageEditState };
11
25
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,11 +1,25 @@
1
- import { a as PagePlacement, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePlacementOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-dl4LfrRX.js";
1
+ import { a as PagePosition, c as PdfSize, i as PageInsertFromBytesOptions, l as PageEditToken, n as PageEditInsertResult, o as PagePositionOptions, r as PageInsertBlankOptions, s as PdfRotation, t as PageEditCapability } from "./contract-IN8rk5LG.js";
2
+ import { PageRef } from "@embedpdf/core";
3
+ //#region src/model.d.ts
4
+ interface PageEditState {
5
+ /** The selected pages, in the order they were selected. */
6
+ readonly selected: readonly PageRef[];
7
+ }
8
+ //#endregion
2
9
  //#region src/page-edit.plugin.d.ts
3
10
  /**
4
- * Document-scoped and stateless: the engine handle's page service as page
5
- * edits that take refs or indexes. Turning a page relative to its own
6
- * rotation lives in the controller, so no app re-derives it.
11
+ * Document-scoped: the document's page edits, each one change of its
12
+ * history, and which of its pages are selected. Turning a page relative to
13
+ * its own rotation lives in the controller, so no app re-derives it.
7
14
  */
8
- declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<void, PageEditCapability, import("@embedpdf/core").NoSettings>;
15
+ declare const pageEditPlugin: () => import("@embedpdf/core").PluginDef<PageEditState, PageEditCapability, import("@embedpdf/core").NoSettings>;
16
+ //#endregion
17
+ //#region src/state.d.ts
18
+ declare const pageEditState: import("@embedpdf/core").StateDeclaration<PageEditCapability, {
19
+ selected: readonly import("@embedpdf/core").PageRef[];
20
+ canEdit: boolean;
21
+ canExtract: boolean;
22
+ }>;
9
23
  //#endregion
10
- export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, PagePlacement, PagePlacementOptions, type PdfRotation, type PdfSize, pageEditPlugin };
24
+ export { PageEditCapability, PageEditInsertResult, PageEditToken, PageInsertBlankOptions, PageInsertFromBytesOptions, type PagePosition, PagePositionOptions, type PdfRotation, type PdfSize, pageEditPlugin, pageEditState };
11
25
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,14 +1,27 @@
1
1
  import { t as PageEditToken } from "./contract-CYWYR5Pd.js";
2
- import { DocumentsToken, PluginError, definePlugin, toPluginError } from "@embedpdf/core";
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
3
15
  //#region src/controller.ts
4
16
  /**
5
- * The page-edit controller. Stateless: it turns the engine handle's page
6
- * service into page edits that take refs or indexes. Turning a page relative
7
- * to its own rotation, and fixing a placement's page, live here once instead
8
- * of in every app's click handler.
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.
9
20
  *
10
- * A verb refuses before anything starts (permission, pages, placement), then
11
- * queues its edit: edits run one at a time, in the order they were called.
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.
12
25
  */
13
26
  /** Inserting, rotating and deleting pages (PDF permission bit 4 or 11). The engine enforces it too. */
14
27
  const ASSEMBLE = "doc.pages.assemble";
@@ -19,9 +32,9 @@ const LETTER_SIZE = {
19
32
  width: 612,
20
33
  height: 792
21
34
  };
35
+ const NO_PAGES = Object.freeze([]);
36
+ const NO_LAYOUTS = Object.freeze([]);
22
37
  function createPageEditController(ctx) {
23
- const enqueue = ctx.serialQueue("edits");
24
- ctx.onSettle(() => enqueue.idle());
25
38
  /** Engine calls outside the guarded `ctx.doc` (another document, a scratch document) map their errors here. */
26
39
  const mapErrors = async (work) => {
27
40
  try {
@@ -30,7 +43,7 @@ function createPageEditController(ctx) {
30
43
  throw toPluginError("page-edit", error);
31
44
  }
32
45
  };
33
- const registry = () => ctx.document()?.pages ?? [];
46
+ const registry = () => ctx.document()?.pages ?? NO_LAYOUTS;
34
47
  const noPages = () => new PluginError("invalid-input", "page-edit", "no pages given");
35
48
  /**
36
49
  * A verb's pages as refs, checked when it is called: an index names the
@@ -40,17 +53,14 @@ function createPageEditController(ctx) {
40
53
  if (pages.length === 0) throw noPages();
41
54
  return pages.map((page) => ctx.pageOf(page).ref);
42
55
  };
43
- /**
44
- * A placement as the engine's position, its page fixed when the verb is
45
- * called, like the verb's own pages. Default `'end'`.
46
- */
47
- const positionOf = (placement) => {
48
- if (!placement || placement === "start" || placement === "end") return placement ?? "end";
49
- if ("after" in placement) return { after: ctx.pageOf(placement.after).ref };
50
- return { before: ctx.pageOf(placement.before).ref };
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 };
51
61
  };
52
62
  /**
53
- * Default blank-page size: the page the placement names, else the page the
63
+ * Default blank-page size: the page the position names, else the page the
54
64
  * new ones go next to (the first or the last), else Letter.
55
65
  */
56
66
  const sizeAt = (position) => {
@@ -59,83 +69,188 @@ function createPageEditController(ctx) {
59
69
  if (pages.length === 0) return LETTER_SIZE;
60
70
  return (position === "start" ? pages[0] : pages[pages.length - 1]).size;
61
71
  };
62
- /** Insert a PDF's pages; resolves the new pages. */
63
- const insertBytes = async (bytes, position, signal) => {
64
- return { pages: (await ctx.cancellable(signal, ctx.doc.pages.insert(bytes, position))).insertedPages };
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`);
88
+ };
89
+ /**
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.
92
+ */
93
+ const stageChange = (verb, count, ops, undo) => {
94
+ const label = count === null ? { key: `pages.${verb}` } : {
95
+ key: `pages.${verb}`,
96
+ count
97
+ };
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 };
116
+ };
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
+ }
65
191
  };
66
192
  return { api: {
67
193
  canEdit: () => ctx.allows(ASSEMBLE),
68
194
  canExtract: () => ctx.allows(DOWNLOAD),
195
+ getSelected,
196
+ select,
69
197
  rotateBy: async (pages, delta, options) => {
70
- ctx.assertAllowed(ASSEMBLE, "pageEdit.rotateBy");
198
+ refuse(ASSEMBLE, "pageEdit.rotateBy", options);
71
199
  const refs = refsOf(pages);
72
- const signal = options?.signal;
73
- await enqueue(async () => {
74
- const groups = /* @__PURE__ */ new Map();
75
- for (const ref of refs) {
76
- const rotation = ((ctx.pageOf(ref).rotation + delta) % 360 + 360) % 360;
77
- groups.set(rotation, [...groups.get(rotation) ?? [], ref]);
78
- }
79
- for (const [rotation, group] of groups) await ctx.cancellable(signal, ctx.doc.pages.rotate(group, rotation));
80
- }, options);
200
+ const change = rotate(refs, (ref) => ((rotationOf(ref) + delta) % 360 + 360) % 360);
201
+ await answer(change, options);
81
202
  },
82
203
  setRotation: async (pages, rotation, options) => {
83
- ctx.assertAllowed(ASSEMBLE, "pageEdit.setRotation");
84
- const refs = refsOf(pages);
85
- await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.rotate(refs, rotation)), options);
204
+ refuse(ASSEMBLE, "pageEdit.setRotation", options);
205
+ const change = rotate(refsOf(pages), () => rotation);
206
+ await answer(change, options);
86
207
  },
87
- reorder: async (pages, placement, options) => {
88
- ctx.assertAllowed(ASSEMBLE, "pageEdit.reorder");
208
+ reorder: async (pages, position, options) => {
209
+ refuse(ASSEMBLE, "pageEdit.reorder", options);
89
210
  const refs = refsOf(pages);
90
- const position = positionOf(placement);
91
- await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.reorder(refs, position)), options);
211
+ const change = stageChange("reorder", refs.length, [{
212
+ type: "pages.reorder",
213
+ pages: refs,
214
+ position: positionOf(position)
215
+ }]);
216
+ await answer(change, options);
92
217
  },
93
218
  delete: async (pages, options) => {
94
- ctx.assertAllowed(ASSEMBLE, "pageEdit.delete");
219
+ refuse(ASSEMBLE, "pageEdit.delete", options);
95
220
  const refs = refsOf(pages);
96
221
  const deleting = new Set(refs.map((ref) => ref.objectNumber));
97
222
  if (registry().every((page) => deleting.has(page.ref.objectNumber))) throw new PluginError("invalid-input", "page-edit", "a document keeps at least one page");
98
- await enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.delete(refs)), options);
223
+ const change = stageChange("delete", refs.length, [{
224
+ type: "pages.delete",
225
+ pages: refs
226
+ }]);
227
+ await answer(change, options);
99
228
  },
100
229
  insertBlank: async (options = {}) => {
101
- ctx.assertAllowed(ASSEMBLE, "pageEdit.insertBlank");
102
- const position = positionOf(options.placement);
103
- return enqueue(async () => {
104
- const size = options.size ?? sizeAt(position);
105
- return { pages: (await ctx.cancellable(options.signal, ctx.doc.pages.insertBlank({
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: {
106
237
  size,
107
- count: options.count
108
- }, position))).insertedPages };
109
- }, options);
238
+ count
239
+ },
240
+ position
241
+ }]);
242
+ return inserted(change, options);
110
243
  },
111
244
  insertFromBytes: async (bytes, options = {}) => {
112
- ctx.assertAllowed(ASSEMBLE, "pageEdit.insertFromBytes");
113
- const position = positionOf(options.placement);
245
+ refuse(ASSEMBLE, "pageEdit.insertFromBytes", options);
246
+ const position = positionOf(options.position);
114
247
  const { pageIndexes, signal } = options;
115
248
  if (pageIndexes?.length === 0) throw noPages();
116
- return enqueue(async () => {
117
- if (!pageIndexes) return insertBytes(bytes, position, signal);
118
- const source = await mapErrors(() => ctx.engine.open({
119
- kind: "bytes",
120
- id: `page-edit-source-${Date.now().toString(36)}`,
121
- bytes: new Uint8Array(bytes)
122
- }, { scope: ["*"] }));
123
- try {
124
- const layout = await mapErrors(() => ctx.cancellable(signal, source.pages.list()));
125
- const refs = pageIndexes.map((index) => {
126
- const page = layout.pages[index];
127
- if (!page) throw new PluginError("not-found", "page-edit", `the PDF has no page ${index}`);
128
- return page.ref;
129
- });
130
- const subset = await mapErrors(() => ctx.cancellable(signal, source.pages.extract(refs)));
131
- return await insertBytes(subset, position, signal);
132
- } finally {
133
- await source.close();
134
- }
135
- }, options);
249
+ const source = pageIndexes ? await pagesOfBytes(bytes, pageIndexes, signal) : bytes;
250
+ return insertBytes("insert", pageIndexes?.length ?? null, source, position, options);
136
251
  },
137
252
  insertFromDocument: async (documentId, pages, options = {}) => {
138
- ctx.assertAllowed(ASSEMBLE, "pageEdit.insertFromDocument");
253
+ refuse(ASSEMBLE, "pageEdit.insertFromDocument", options);
139
254
  if (pages.length === 0) throw noPages();
140
255
  const other = ctx.documentHandle(documentId);
141
256
  if (!other) throw new PluginError("not-found", "page-edit", `document '${documentId}' is not open`);
@@ -145,43 +260,57 @@ function createPageEditController(ctx) {
145
260
  if (!found) throw new PluginError("not-found", "page-edit", `document '${documentId}' has no such page`);
146
261
  return found.ref;
147
262
  });
148
- const position = positionOf(options.placement);
149
- return enqueue(async () => {
150
- const bytes = await mapErrors(() => ctx.cancellable(options.signal, other.pages.extract(refs)));
151
- return insertBytes(bytes, position, options.signal);
152
- }, options);
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);
153
266
  },
154
267
  duplicate: async (pages, options = {}) => {
155
- ctx.assertAllowed(ASSEMBLE, "pageEdit.duplicate");
268
+ refuse(ASSEMBLE, "pageEdit.duplicate", options);
156
269
  ctx.assertAllowed(DOWNLOAD, "pageEdit.duplicate");
157
270
  const refs = refsOf(pages);
158
- const position = options.placement ? positionOf(options.placement) : { after: refs[refs.length - 1] };
159
- return enqueue(async () => {
160
- const bytes = await ctx.cancellable(options.signal, ctx.doc.pages.extract(refs));
161
- return insertBytes(bytes, position, options.signal);
162
- }, options);
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);
163
274
  },
164
275
  extract: async (pages, options) => {
165
- ctx.assertAllowed(DOWNLOAD, "pageEdit.extract");
166
- const refs = refsOf(pages);
167
- return enqueue(() => ctx.cancellable(options?.signal, ctx.doc.pages.extract(refs)), options);
276
+ refuse(DOWNLOAD, "pageEdit.extract", options);
277
+ return extractShown(refsOf(pages), options);
168
278
  }
169
279
  } };
170
280
  }
171
281
  //#endregion
172
282
  //#region src/page-edit.plugin.ts
173
283
  /**
174
- * Document-scoped and stateless: the engine handle's page service as page
175
- * edits that take refs or indexes. Turning a page relative to its own
176
- * rotation lives in the controller, so no app re-derives it.
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.
177
287
  */
178
288
  const pageEditPlugin = () => definePlugin({
179
289
  id: "page-edit",
180
290
  scope: "document",
181
291
  token: PageEditToken,
292
+ state: initialPageEditState,
182
293
  create: createPageEditController
183
294
  });
184
295
  //#endregion
185
- export { PageEditToken, pageEditPlugin };
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
+ }
312
+ });
313
+ //#endregion
314
+ export { PageEditToken, pageEditPlugin, pageEditState };
186
315
 
187
316
  //# sourceMappingURL=index.js.map