@embedpdf/plugin-link 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/index.js CHANGED
@@ -1,196 +1,245 @@
1
- import { t as LinkToken } from "./contract-HRggERzU.js";
2
- import { definePlugin } from "@embedpdf/core";
1
+ import { n as LinkToken, t as resolveLinkTarget } from "./contract-CJ9mhj7i.js";
2
+ import { LinkToken as LinkToken$1 } from "./contract/host.js";
3
+ import { PluginError, definePlugin } from "@embedpdf/core";
3
4
  import { ActionsToken } from "@embedpdf/plugin-actions/contract";
4
5
  import { AnnotationToken } from "@embedpdf/plugin-annotation/contract";
5
- import { AnnotationToken as AnnotationToken$1 } from "@embedpdf/plugin-annotation/contract/host";
6
6
  import { InteractionToken } from "@embedpdf/plugin-interaction/contract";
7
7
  import { StageToken } from "@embedpdf/plugin-stage/contract";
8
- import { destinationToReveal } from "@embedpdf/plugin-stage/destination";
9
- import { pdfToContentRect } from "@embedpdf/core-annotation";
8
+ import { AnnotationToken as AnnotationToken$1 } from "@embedpdf/plugin-annotation/contract/host";
9
+ import { InteractionToken as InteractionToken$1 } from "@embedpdf/plugin-interaction/contract/host";
10
+ import { annotationKey } from "@embedpdf/engine-core/runtime";
11
+ //#region src/connect.ts
12
+ /**
13
+ * Links are annotations: while a navigation tool is active the link layer
14
+ * owns their input (its anchors take the clicks), so the annotation plane
15
+ * stands down for them. A link still shows as the file draws it.
16
+ */
17
+ function connectLink(ctx) {
18
+ const annotation = ctx.tryGet(AnnotationToken$1);
19
+ if (!annotation) return;
20
+ const interaction = ctx.get(InteractionToken$1);
21
+ ctx.cleanup(annotation.registerBehavior({
22
+ id: "link-nav",
23
+ matches: (target) => target.subtype === "link",
24
+ engaged: () => interaction.activeToolEnables("link-nav")
25
+ }));
26
+ }
27
+ //#endregion
10
28
  //#region src/source.ts
11
- function loadLinksPage(io, pon) {
12
- const doc = io.doc;
13
- const crop = io.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop;
14
- if (!doc || !crop) return;
15
- doc.page(pon).annotations.list().then((snap) => {
16
- const items = [];
17
- for (const dto of snap.annotations) {
18
- if (dto.subtype !== "link" || dto.target == null) continue;
19
- if (dto.flags.hidden || dto.flags.noView) continue;
20
- items.push({
21
- id: dto.ref.kind === "objectNumber" ? `obj:${dto.ref.annotObjectNumber}` : `idx:${pon}:${dto.index}`,
22
- rect: pdfToContentRect(dto.rect, crop),
23
- target: dto.target,
24
- ...dto.actions?.activate ? { activate: dto.actions.activate } : {},
25
- ...dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root ? { hoverEvents: {
26
- enter: Boolean(dto.actions?.cursorEnter?.root),
27
- exit: Boolean(dto.actions?.cursorExit?.root)
28
- } } : {},
29
- ref: dto.ref,
30
- attached: dto.replyType === "group" && dto.inReplyTo != null
31
- });
32
- }
33
- io.dispatch({
34
- type: "SET_PAGE",
35
- pon,
36
- items
29
+ /**
30
+ * The stand-alone source, used when no annotation plugin is installed: the
31
+ * clickable areas among one page's annotations, in page space.
32
+ */
33
+ /** The visible link annotations of a page that have a target. */
34
+ function linksOf(annotations) {
35
+ const links = [];
36
+ for (const dto of annotations) {
37
+ if (dto.subtype !== "link" || dto.target == null) continue;
38
+ if (dto.hidden || dto.noView) continue;
39
+ links.push({
40
+ id: annotationKey(dto.ref),
41
+ bounds: dto.rect,
42
+ target: dto.target,
43
+ ...dto.actions?.activate ? { activate: dto.actions.activate } : {},
44
+ ...dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root ? { hoverEvents: {
45
+ enter: Boolean(dto.actions?.cursorEnter?.root),
46
+ exit: Boolean(dto.actions?.cursorExit?.root)
47
+ } } : {},
48
+ ref: dto.ref,
49
+ attached: dto.reply?.type === "group"
37
50
  });
38
- }, () => {});
51
+ }
52
+ return links;
39
53
  }
40
54
  //#endregion
41
- //#region src/capability.ts
42
- const EMPTY = [];
55
+ //#region src/controller.ts
56
+ const EMPTY = Object.freeze([]);
57
+ const isLink = (value) => "target" in value && "bounds" in value;
58
+ const contains = (bounds, point) => point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height;
59
+ /** The pages whose annotation lists an event changed, each once. */
60
+ function pagesChangedBy(event) {
61
+ switch (event.type) {
62
+ case "annotations.created": return [event.annotation.page];
63
+ case "annotations.updated": return [event.annotation.page];
64
+ case "annotations.deleted":
65
+ case "annotations.reordered": return [event.page];
66
+ case "redaction.applied":
67
+ case "pages.flattened": return event.results.filter((result) => result.status === "applied").map((result) => result.page);
68
+ case "annotations.flattened": return event.results.some((result) => result.status === "applied") ? [event.page] : null;
69
+ default: return null;
70
+ }
71
+ }
43
72
  /**
44
- * The navigation plane's surface. Data source is decided PER CALL (lazily,
45
- * so plugin registration order can't matter): the annotation plugin's
46
- * folded model when present — its `linkItemsOn` lens is memoized by model
47
- * identity and already excludes hidden links — else this plugin's own
48
- * engine-backed page cache (see source.ts).
73
+ * The link controller. Two sources for the clickable areas: the annotation
74
+ * plugin's folded model when it is installed (always current), else a page
75
+ * mirror of this plugin's own per-page reads, re-read when a confirmed
76
+ * change (an annotation write, a redaction, a flatten) touches a loaded page. Activation is pure resolution
77
+ * (`resolve`) plus the one side effect this plugin owns, a stage reveal;
78
+ * everything else is reported to the host, which keeps the user gesture.
49
79
  */
50
- function createLinkCapability(ctx, config = {}) {
51
- const anno = () => ctx.tryGet(AnnotationToken$1);
52
- const activate = (target, context) => {
53
- const activation = (() => {
54
- const actions = ctx.tryGet(ActionsToken);
55
- if (actions && context?.activate) return {
56
- outcome: "dispatched",
57
- dispatch: actions.execute(context.activate, {
58
- origin: "user",
59
- source: {
60
- kind: "link",
61
- annotation: context.ref,
62
- pon: context.pon
63
- },
64
- event: { scope: "activate" }
65
- })
66
- };
67
- switch (target.kind) {
68
- case "goto": {
69
- const stage = ctx.tryGet(StageToken);
70
- const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === target.destination.pageObjectNumber);
71
- if (!stage || !layout) return {
72
- outcome: "destination",
73
- destination: target.destination
74
- };
75
- const { pageIndex, options } = destinationToReveal(target.destination, layout);
76
- stage.reveal(pageIndex, {
77
- ...options,
78
- behavior: "smooth"
79
- });
80
- return { outcome: "revealed" };
81
- }
82
- case "uri": return {
83
- outcome: "uri",
84
- uri: target.uri
80
+ function createLinkController(ctx) {
81
+ const annotationHost = () => ctx.tryGet(AnnotationToken$1);
82
+ const activated = ctx.events.source();
83
+ const loaded = ctx.events.source();
84
+ const pages = ctx.pageMirror({
85
+ name: "links",
86
+ load: async (doc, page) => {
87
+ if (!ctx.getPage(page)) throw new PluginError("not-found", "link", `page ${page.objectNumber} is not in this document`);
88
+ return linksOf((await doc.page(page).annotations.list()).annotations);
89
+ },
90
+ affected: pagesChangedBy,
91
+ changed: ({ page, cause }) => {
92
+ if (cause === "load") loaded.emit({ page });
93
+ }
94
+ });
95
+ const listLinks = (pageArgument) => {
96
+ const page = ctx.getPage(pageArgument)?.ref;
97
+ if (!page) return EMPTY;
98
+ const host = annotationHost();
99
+ if (host) return host.listLinkItems(page);
100
+ return pages.get(page) ?? EMPTY;
101
+ };
102
+ const ensureLoaded = async (pageArgument, options) => {
103
+ const { ref } = ctx.pageOf(pageArgument);
104
+ if (annotationHost()) return;
105
+ await ctx.cancellable(options?.signal, pages.ensureLoaded(ref));
106
+ };
107
+ const uriOpeners = [];
108
+ const resolve = (target) => resolveLinkTarget(target, (page) => ctx.getPage(page) !== null);
109
+ const perform = (target, context) => {
110
+ const actions = ctx.tryGet(ActionsToken);
111
+ if (actions && context?.activate) return {
112
+ outcome: "dispatched",
113
+ dispatch: actions.execute(context.activate, {
114
+ origin: "user",
115
+ source: {
116
+ kind: "link",
117
+ annotation: context.ref,
118
+ page: context.page
119
+ },
120
+ event: { scope: "activate" }
121
+ })
122
+ };
123
+ const resolution = resolve(target);
124
+ switch (resolution.kind) {
125
+ case "destination": {
126
+ const stage = context?.stage ?? ctx.tryGet(StageToken);
127
+ if (!stage) return {
128
+ outcome: "destination",
129
+ destination: resolution.destination
85
130
  };
86
- case "named": return {
87
- outcome: "named",
88
- name: target.name
131
+ stage.goToDestination(resolution.destination);
132
+ return { outcome: "revealed" };
133
+ }
134
+ case "uri": {
135
+ const open = uriOpeners[0];
136
+ if (!open || open(resolution.uri)) return {
137
+ outcome: "uri",
138
+ uri: resolution.uri
89
139
  };
90
- default: return {
140
+ return {
91
141
  outcome: "reported",
92
142
  target
93
143
  };
94
144
  }
95
- })();
96
- config.onActivate?.({
145
+ case "named": return {
146
+ outcome: "named",
147
+ name: resolution.name
148
+ };
149
+ default: return {
150
+ outcome: "reported",
151
+ target
152
+ };
153
+ }
154
+ };
155
+ const activate = (input, context) => {
156
+ const target = isLink(input) ? input.target : input;
157
+ const linkContext = isLink(input) ? {
158
+ activate: input.activate,
159
+ ref: input.ref,
160
+ ...context
161
+ } : context;
162
+ const activation = perform(target, linkContext);
163
+ activated.emit({
97
164
  target,
98
165
  activation
99
166
  });
100
167
  return activation;
101
168
  };
102
- return {
103
- linksOn: (pon) => {
104
- const host = anno();
105
- if (host) return host.linkItemsOn(pon);
106
- return ctx.getState().pages[pon] ?? EMPTY;
107
- },
108
- ensurePage: (pon) => {
109
- const host = anno();
110
- if (host) {
111
- host.ensurePage(pon);
112
- return;
113
- }
114
- if (!(pon in ctx.getState().pages)) loadLinksPage(ctx, pon);
115
- },
116
- engaged: () => ctx.tryGet(InteractionToken)?.activeTool()?.enables.has("link-nav") ?? false,
117
- activate
118
- };
119
- }
120
- //#endregion
121
- //#region src/effects.ts
122
- /**
123
- * Keeps the ENGINE-BACKED page cache honest in viewer-only deployments:
124
- * any annotation mutation on a cached page (own or remote — the cache has
125
- * no other refresh path, unlike the annotation plugin's optimistic model)
126
- * refetches that page's links. With the annotation plugin present the
127
- * folded model is the source and this effect never fires a fetch.
128
- */
129
- function registerLinkEffects(ctx) {
130
- const doc = ctx.doc;
131
- if (!doc) return;
132
- const refetch = (pon) => {
133
- if (ctx.tryGet(AnnotationToken$1)) return;
134
- if (pon in ctx.getState().pages) loadLinksPage(ctx, pon);
169
+ const getLinkAt = (page, point) => {
170
+ let best = null;
171
+ for (const link of listLinks(page)) {
172
+ if (!contains(link.bounds, point)) continue;
173
+ if (!best || link.bounds.width * link.bounds.height < best.bounds.width * best.bounds.height) best = link;
174
+ }
175
+ return best;
135
176
  };
136
- const unsubscribe = doc.events.subscribe((event) => {
137
- switch (event.type) {
138
- case "annotation.created":
139
- refetch(event.created.pageObjectNumber);
140
- break;
141
- case "annotation.updated":
142
- refetch(event.updated.pageObjectNumber);
143
- break;
144
- case "annotation.moved":
145
- if (event.moved.length) refetch(event.moved[0].pageObjectNumber);
146
- break;
147
- case "annotation.deleted":
148
- refetch(event.pageObjectNumber);
149
- break;
150
- default: break;
177
+ const getLabel = (input) => {
178
+ const target = isLink(input) ? input.target : input;
179
+ switch (target.kind) {
180
+ case "uri": return target.uri;
181
+ case "goto": return "Go to destination";
182
+ case "named": return target.name;
183
+ default: return "Link";
151
184
  }
152
- });
153
- ctx.cleanup(unsubscribe);
154
- }
155
- //#endregion
156
- //#region src/reducer.ts
157
- const initialLinkState = () => ({ pages: {} });
158
- /** Pure. Engine-backed page cache only — the annotation-backed mode reads the
159
- * annotation plugin's model and never touches this state. */
160
- function linkReducer(state, a) {
161
- switch (a.type) {
162
- case "SET_PAGE": return {
163
- ...state,
164
- pages: {
165
- ...state.pages,
166
- [a.pon]: a.items
167
- }
168
- };
169
- case "DROP_PAGE": {
170
- if (!(a.pon in state.pages)) return state;
171
- const pages = { ...state.pages };
172
- delete pages[a.pon];
173
- return {
174
- ...state,
175
- pages
176
- };
185
+ };
186
+ return {
187
+ api: {
188
+ listLinks,
189
+ getLink: (page, linkId) => listLinks(page).find((link) => link.id === linkId) ?? null,
190
+ getLinkAt,
191
+ listAllLinks: async (options) => {
192
+ const layouts = ctx.document()?.pages ?? [];
193
+ await ctx.cancellable(options?.signal, Promise.all(layouts.map((layout) => ensureLoaded(layout.ref, options))));
194
+ return layouts.flatMap((layout) => listLinks(layout.ref));
195
+ },
196
+ ensureLoaded,
197
+ isLoaded: (page) => {
198
+ const ref = ctx.getPage(page)?.ref;
199
+ return !!ref && (annotationHost() !== null || pages.getStatus(ref) === "ready");
200
+ },
201
+ getStatus: (page) => {
202
+ const ref = ctx.getPage(page)?.ref;
203
+ if (!ref) return "idle";
204
+ return annotationHost() ? "ready" : pages.getStatus(ref);
205
+ },
206
+ resolve,
207
+ activate,
208
+ activateAt: (pageArgument, point, context) => {
209
+ const page = ctx.pageOf(pageArgument).ref;
210
+ const link = getLinkAt(page, point);
211
+ return link ? activate(link, {
212
+ page,
213
+ ...context
214
+ }) : null;
215
+ },
216
+ getLabel,
217
+ onActivated: activated.on,
218
+ onLoaded: loaded.on,
219
+ registerUriOpener: (opener) => {
220
+ uriOpeners.unshift(opener);
221
+ return () => {
222
+ const index = uriOpeners.indexOf(opener);
223
+ if (index >= 0) uriOpeners.splice(index, 1);
224
+ };
225
+ },
226
+ isNavigationEngaged: () => ctx.tryGet(InteractionToken$1)?.activeToolEnables("link-nav") ?? false
227
+ },
228
+ connect() {
229
+ connectLink(ctx);
177
230
  }
178
- }
231
+ };
179
232
  }
180
233
  //#endregion
181
234
  //#region src/link.plugin.ts
182
235
  /**
183
- * The link plugin: the NAVIGATION plane. Document-scoped; requires only the
184
- * interaction hub. Navigation works with no annotation plugin at all —
185
- * links are read from the engine's link DTOs. When the annotation plugin IS
186
- * present, its folded model becomes the data source (no double fetch) and a
187
- * Behavior keeps links navigation-owned while a `link-nav` tool is active
188
- * (pointer/pan/form-fill by default) — the single-active-tool hub IS the
189
- * mode switch: grab the link tool and every link becomes an editable rect.
236
+ * Clickable link regions and their activation. Document-scoped; needs the
237
+ * interaction hub (the navigation tool tag), optionally the stage (reveal),
238
+ * the annotation plugin (the folded link model) and actions (`/A` trees).
190
239
  */
191
- const linkPlugin = (config) => definePlugin({
240
+ const linkPlugin = () => definePlugin({
192
241
  id: "link",
193
- token: LinkToken,
242
+ token: LinkToken$1,
194
243
  scope: "document",
195
244
  requires: [InteractionToken],
196
245
  optional: [
@@ -198,21 +247,9 @@ const linkPlugin = (config) => definePlugin({
198
247
  AnnotationToken,
199
248
  ActionsToken
200
249
  ],
201
- initialState: initialLinkState,
202
- reduce: linkReducer,
203
- capability: (ctx) => createLinkCapability(ctx, config),
204
- effects: registerLinkEffects,
205
- init: (ctx) => {
206
- const interaction = ctx.get(InteractionToken);
207
- const annotation = ctx.tryGet(AnnotationToken$1);
208
- if (annotation) annotation.registerBehavior({
209
- id: "link-nav",
210
- matches: (a) => a.subtype === "link",
211
- engaged: () => interaction.activeTool()?.enables.has("link-nav") ?? false
212
- });
213
- }
250
+ create: createLinkController
214
251
  });
215
252
  //#endregion
216
- export { LinkToken, linkPlugin };
253
+ export { LinkToken, linkPlugin, resolveLinkTarget };
217
254
 
218
255
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["AnnotationHostToken","AnnotationHostToken","AnnotationHostToken"],"sources":["../src/source.ts","../src/capability.ts","../src/effects.ts","../src/reducer.ts","../src/link.plugin.ts"],"sourcesContent":["import type { DocumentHandle, PageLayout } from '@embedpdf/core';\nimport { pdfToContentRect } from '@embedpdf/core-annotation';\nimport type { LinkAction, LinkNavItem } from './types';\n\n/**\n * The ENGINE-BACKED link source, for viewer-only deployments (no annotation\n * plugin): read a page's annotation list and keep just the navigable links —\n * standalone AND attached children alike (both are raw link DTOs here).\n * Shared by the capability's lazy `ensurePage` and the effects module's\n * event-driven refetch; both PluginContext and EffectContext satisfy the\n * structural `io` slice.\n */\nexport interface LinkSourceIO {\n doc: DocumentHandle | null;\n document(): { pages: readonly PageLayout[] } | null;\n dispatch(action: LinkAction): void;\n}\n\nexport function loadLinksPage(io: LinkSourceIO, pon: number): void {\n const doc = io.doc;\n const crop = io.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop;\n if (!doc || !crop) return;\n doc\n .page(pon)\n .annotations.list()\n .then(\n (snap) => {\n const items: LinkNavItem[] = [];\n for (const dto of snap.annotations) {\n if (dto.subtype !== 'link' || dto.target == null) continue;\n if (dto.flags.hidden || dto.flags.noView) continue;\n items.push({\n id:\n dto.ref.kind === 'objectNumber'\n ? `obj:${dto.ref.annotObjectNumber}`\n : `idx:${pon}:${dto.index}`,\n rect: pdfToContentRect(dto.rect, crop),\n target: dto.target,\n ...(dto.actions?.activate ? { activate: dto.actions.activate } : {}),\n ...(dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root\n ? {\n hoverEvents: {\n enter: Boolean(dto.actions?.cursorEnter?.root),\n exit: Boolean(dto.actions?.cursorExit?.root),\n },\n }\n : {}),\n ref: dto.ref,\n // A `/RT /Group` child riding another annotation — labeled so the\n // nav layer can defer to editing (moot in viewer-only deployments,\n // but the item contract stays truthful either way).\n attached: dto.replyType === 'group' && dto.inReplyTo != null,\n });\n }\n io.dispatch({ type: 'SET_PAGE', pon, items });\n },\n () => {},\n );\n}\n","import type { PluginContext } from '@embedpdf/core';\nimport type { PdfLinkTarget } from '@embedpdf/engine-core/runtime';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\n// The host lens (linkItemsOn) — same runtime token as the public one.\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\nimport { destinationToReveal } from '@embedpdf/plugin-stage/destination';\nimport { loadLinksPage } from './source';\nimport type {\n LinkAction,\n LinkActivateContext,\n LinkActivation,\n LinkCapability,\n LinkNavItem,\n LinkPluginConfig,\n LinkState,\n} from './types';\n\nconst EMPTY: LinkNavItem[] = [];\n\n/**\n * The navigation plane's surface. Data source is decided PER CALL (lazily,\n * so plugin registration order can't matter): the annotation plugin's\n * folded model when present — its `linkItemsOn` lens is memoized by model\n * identity and already excludes hidden links — else this plugin's own\n * engine-backed page cache (see source.ts).\n */\nexport function createLinkCapability(\n ctx: PluginContext<LinkState, LinkAction>,\n config: LinkPluginConfig = {},\n): LinkCapability {\n const anno = () => ctx.tryGet(AnnotationHostToken);\n\n const activate = (target: PdfLinkTarget, context?: LinkActivateContext): LinkActivation => {\n const activation = ((): LinkActivation => {\n // The action engine takes precedence when it is installed and the item\n // carries its payload tree: named verbs execute, mixed /Next chains\n // run, and the dispatcher enforces policy + the incomplete-tree law.\n // Without it, the classic root-projection path below is the fallback.\n const actions = ctx.tryGet(ActionsToken);\n if (actions && context?.activate) {\n const dispatch = actions.execute(context.activate, {\n origin: 'user',\n source: { kind: 'link', annotation: context.ref, pon: context.pon },\n event: { scope: 'activate' },\n });\n return { outcome: 'dispatched', dispatch };\n }\n switch (target.kind) {\n case 'goto': {\n const stage = ctx.tryGet(StageToken);\n const layout = ctx\n .document()\n ?.pages.find((p) => p.pageObjectNumber === target.destination.pageObjectNumber);\n if (!stage || !layout) {\n // No camera to drive — hand the embedder the explicit destination.\n return { outcome: 'destination', destination: target.destination };\n }\n const { pageIndex, options } = destinationToReveal(target.destination, layout);\n stage.reveal(pageIndex, { ...options, behavior: 'smooth' });\n return { outcome: 'revealed' };\n }\n case 'uri':\n // Opening is the FRAMEWORK layer's job (a real <a href> after\n // sanitizing) — this plugin never touches the DOM.\n return { outcome: 'uri', uri: target.uri };\n case 'named':\n return { outcome: 'named', name: target.name };\n default:\n // javascript / goto-remote / launch / unsupported: reported, never\n // executed here. JavaScript activation belongs to the scripting\n // orchestrator once it lands — the chain already rides the\n // annotation's base `actions`.\n return { outcome: 'reported', target };\n }\n })();\n config.onActivate?.({ target, activation });\n return activation;\n };\n\n return {\n linksOn: (pon) => {\n const host = anno();\n if (host) return host.linkItemsOn(pon);\n return ctx.getState().pages[pon] ?? EMPTY;\n },\n ensurePage: (pon) => {\n const host = anno();\n if (host) {\n host.ensurePage(pon); // the folded model is the source; it lazy-loads\n return;\n }\n if (!(pon in ctx.getState().pages)) loadLinksPage(ctx, pon);\n },\n engaged: () => ctx.tryGet(InteractionToken)?.activeTool()?.enables.has('link-nav') ?? false,\n activate,\n };\n}\n","import type { DocumentEvent, EffectContext } from '@embedpdf/core';\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { loadLinksPage } from './source';\nimport type { LinkAction, LinkState } from './types';\n\n/**\n * Keeps the ENGINE-BACKED page cache honest in viewer-only deployments:\n * any annotation mutation on a cached page (own or remote — the cache has\n * no other refresh path, unlike the annotation plugin's optimistic model)\n * refetches that page's links. With the annotation plugin present the\n * folded model is the source and this effect never fires a fetch.\n */\nexport function registerLinkEffects(ctx: EffectContext<LinkState, LinkAction>): void {\n const doc = ctx.doc;\n if (!doc) return;\n\n const refetch = (pon: number): void => {\n if (ctx.tryGet(AnnotationHostToken)) return; // annotation model owns the data\n if (pon in ctx.getState().pages) loadLinksPage(ctx, pon);\n };\n\n const unsubscribe = doc.events.subscribe((event: DocumentEvent) => {\n switch (event.type) {\n case 'annotation.created':\n refetch(event.created.pageObjectNumber);\n break;\n case 'annotation.updated':\n refetch(event.updated.pageObjectNumber);\n break;\n case 'annotation.moved':\n if (event.moved.length) refetch(event.moved[0].pageObjectNumber);\n break;\n case 'annotation.deleted':\n refetch(event.pageObjectNumber);\n break;\n default:\n break;\n }\n });\n ctx.cleanup(unsubscribe);\n}\n","import type { LinkAction, LinkState } from './types';\n\nexport const initialLinkState = (): LinkState => ({ pages: {} });\n\n/** Pure. Engine-backed page cache only — the annotation-backed mode reads the\n * annotation plugin's model and never touches this state. */\nexport function linkReducer(state: LinkState, a: LinkAction): LinkState {\n switch (a.type) {\n case 'SET_PAGE':\n return { ...state, pages: { ...state.pages, [a.pon]: a.items } };\n case 'DROP_PAGE': {\n if (!(a.pon in state.pages)) return state;\n const pages = { ...state.pages };\n delete pages[a.pon];\n return { ...state, pages };\n }\n }\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\nimport { AnnotationToken } from '@embedpdf/plugin-annotation/contract';\n// Behavior registration lives on the HOST capability (framework/plugin\n// surface) — same runtime token, wider type. The form plugin's precedent.\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\n\nimport { createLinkCapability } from './capability';\nimport { registerLinkEffects } from './effects';\nimport { initialLinkState, linkReducer } from './reducer';\nimport { LinkToken } from './types';\nimport type { LinkAction, LinkCapability, LinkPluginConfig, LinkState } from './types';\n\n/**\n * The link plugin: the NAVIGATION plane. Document-scoped; requires only the\n * interaction hub. Navigation works with no annotation plugin at all —\n * links are read from the engine's link DTOs. When the annotation plugin IS\n * present, its folded model becomes the data source (no double fetch) and a\n * Behavior keeps links navigation-owned while a `link-nav` tool is active\n * (pointer/pan/form-fill by default) — the single-active-tool hub IS the\n * mode switch: grab the link tool and every link becomes an editable rect.\n */\nexport const linkPlugin = (config?: LinkPluginConfig) =>\n definePlugin<LinkState, LinkAction, LinkCapability>({\n id: 'link',\n token: LinkToken,\n scope: 'document',\n requires: [InteractionToken],\n optional: [StageToken, AnnotationToken, ActionsToken],\n initialState: initialLinkState,\n reduce: linkReducer,\n capability: (ctx) => createLinkCapability(ctx, config),\n effects: registerLinkEffects,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const annotation = ctx.tryGet(AnnotationHostToken);\n if (annotation) {\n annotation.registerBehavior({\n id: 'link-nav',\n matches: (a) => a.subtype === 'link',\n engaged: () => interaction.activeTool()?.enables.has('link-nav') ?? false,\n });\n }\n },\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["AnnotationToken","InteractionToken","AnnotationHostToken","InteractionToken","LinkToken"],"sources":["../src/connect.ts","../src/source.ts","../src/controller.ts","../src/link.plugin.ts"],"sourcesContent":["/** The link plugin's wiring to its siblings, run from the controller's `connect`. */\nimport type { PluginContext } from '@embedpdf/core';\nimport { AnnotationToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract/host';\n\n/**\n * Links are annotations: while a navigation tool is active the link layer\n * owns their input (its anchors take the clicks), so the annotation plane\n * stands down for them. A link still shows as the file draws it.\n */\nexport function connectLink(ctx: PluginContext<void>): void {\n const annotation = ctx.tryGet(AnnotationToken);\n if (!annotation) return;\n const interaction = ctx.get(InteractionToken);\n ctx.cleanup(\n annotation.registerBehavior({\n id: 'link-nav',\n matches: (target) => target.subtype === 'link',\n engaged: () => interaction.activeToolEnables('link-nav'),\n }),\n );\n}\n","/**\n * The stand-alone source, used when no annotation plugin is installed: the\n * clickable areas among one page's annotations, in page space.\n */\nimport { annotationKey, type Annotation } from '@embedpdf/engine-core/runtime';\n\nimport type { Link } from './contract';\n\n/** The visible link annotations of a page that have a target. */\nexport function linksOf(annotations: readonly Annotation[]): readonly Link[] {\n const links: Link[] = [];\n for (const dto of annotations) {\n if (dto.subtype !== 'link' || dto.target == null) continue;\n if (dto.hidden || dto.noView) continue;\n links.push({\n id: annotationKey(dto.ref),\n bounds: dto.rect,\n target: dto.target,\n ...(dto.actions?.activate ? { activate: dto.actions.activate } : {}),\n ...(dto.actions?.cursorEnter?.root || dto.actions?.cursorExit?.root\n ? {\n hoverEvents: {\n enter: Boolean(dto.actions?.cursorEnter?.root),\n exit: Boolean(dto.actions?.cursorExit?.root),\n },\n }\n : {}),\n ref: dto.ref,\n attached: dto.reply?.type === 'group',\n });\n }\n return links;\n}\n","import {\n PluginError,\n type PluginContext,\n type DocumentEvent,\n type OperationOptions,\n type PageRef,\n} from '@embedpdf/core';\nimport type { Point } from '@embedpdf/core-geometry';\nimport type { PdfLinkTarget } from '@embedpdf/engine-core/runtime';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\nimport { AnnotationToken as AnnotationHostToken } from '@embedpdf/plugin-annotation/contract/host';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract/host';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\n\nimport { connectLink } from './connect';\nimport {\n resolveLinkTarget,\n type Link,\n type LinkActivateContext,\n type LinkActivatedEvent,\n type LinkActivation,\n type LinkLoadedEvent,\n type LinkResolution,\n} from './contract';\nimport type { LinkHostCapability, UriOpener } from './host-contract';\nimport { linksOf } from './source';\n\nconst EMPTY: readonly Link[] = Object.freeze([]);\n\nconst isLink = (value: PdfLinkTarget | Link): value is Link =>\n 'target' in value && 'bounds' in value;\nconst contains = (\n bounds: { x: number; y: number; width: number; height: number },\n point: Point,\n): boolean =>\n point.x >= bounds.x &&\n point.x <= bounds.x + bounds.width &&\n point.y >= bounds.y &&\n point.y <= bounds.y + bounds.height;\n\n/** The pages whose annotation lists an event changed, each once. */\nfunction pagesChangedBy(event: DocumentEvent): readonly PageRef[] | null {\n switch (event.type) {\n case 'annotations.created':\n return [event.annotation.page];\n case 'annotations.updated':\n return [event.annotation.page];\n case 'annotations.deleted':\n case 'annotations.reordered':\n return [event.page];\n // These can remove annotations, links included, from the pages they applied to.\n case 'redaction.applied':\n case 'pages.flattened':\n return event.results\n .filter((result) => result.status === 'applied')\n .map((result) => result.page);\n case 'annotations.flattened':\n return event.results.some((result) => result.status === 'applied') ? [event.page] : null;\n default:\n return null;\n }\n}\n\n/**\n * The link controller. Two sources for the clickable areas: the annotation\n * plugin's folded model when it is installed (always current), else a page\n * mirror of this plugin's own per-page reads, re-read when a confirmed\n * change (an annotation write, a redaction, a flatten) touches a loaded page. Activation is pure resolution\n * (`resolve`) plus the one side effect this plugin owns, a stage reveal;\n * everything else is reported to the host, which keeps the user gesture.\n */\nexport function createLinkController(ctx: PluginContext<void>) {\n const annotationHost = () => ctx.tryGet(AnnotationHostToken);\n const activated = ctx.events.source<LinkActivatedEvent>();\n const loaded = ctx.events.source<LinkLoadedEvent>();\n\n const pages = ctx.pageMirror<readonly Link[]>({\n name: 'links',\n load: async (doc, page) => {\n if (!ctx.getPage(page)) {\n throw new PluginError(\n 'not-found',\n 'link',\n `page ${page.objectNumber} is not in this document`,\n );\n }\n const snapshot = await doc.page(page).annotations.list();\n return linksOf(snapshot.annotations);\n },\n affected: pagesChangedBy,\n changed: ({ page, cause }) => {\n if (cause === 'load') loaded.emit({ page });\n },\n });\n\n // Reads take a page's ref or its index; a page that isn't there has no links.\n const listLinks = (pageArgument: PageRef | number): readonly Link[] => {\n const page = ctx.getPage(pageArgument)?.ref;\n if (!page) return EMPTY;\n const host = annotationHost();\n if (host) return host.listLinkItems(page);\n return pages.get(page) ?? EMPTY;\n };\n\n const ensureLoaded = async (\n pageArgument: PageRef | number,\n options?: OperationOptions,\n ): Promise<void> => {\n const { ref } = ctx.pageOf(pageArgument);\n // With the annotation plugin installed, its model owns the data.\n if (annotationHost()) return;\n await ctx.cancellable(options?.signal, pages.ensureLoaded(ref));\n };\n\n // The framework's website openers, the latest first: the plugin never opens one itself.\n const uriOpeners: UriOpener[] = [];\n\n // A page the document doesn't have can't be shown.\n const resolve = (target: PdfLinkTarget): LinkResolution =>\n resolveLinkTarget(target, (page) => ctx.getPage(page) !== null);\n\n const perform = (target: PdfLinkTarget, context?: LinkActivateContext): LinkActivation => {\n const actions = ctx.tryGet(ActionsToken);\n if (actions && context?.activate) {\n const dispatch = actions.execute(context.activate, {\n origin: 'user',\n source: { kind: 'link', annotation: context.ref, page: context.page },\n event: { scope: 'activate' },\n });\n return { outcome: 'dispatched', dispatch };\n }\n const resolution = resolve(target);\n switch (resolution.kind) {\n case 'destination': {\n // The view the link was followed in, by its own scroll behavior.\n const stage = context?.stage ?? ctx.tryGet(StageToken);\n if (!stage) return { outcome: 'destination', destination: resolution.destination };\n stage.goToDestination(resolution.destination);\n return { outcome: 'revealed' };\n }\n case 'uri': {\n const open = uriOpeners[0];\n if (!open || open(resolution.uri)) return { outcome: 'uri', uri: resolution.uri };\n return { outcome: 'reported', target };\n }\n case 'named':\n return { outcome: 'named', name: resolution.name };\n default:\n return { outcome: 'reported', target };\n }\n };\n\n const activate = (input: PdfLinkTarget | Link, context?: LinkActivateContext): LinkActivation => {\n const target = isLink(input) ? input.target : input;\n const linkContext: LinkActivateContext | undefined = isLink(input)\n ? { activate: input.activate, ref: input.ref, ...context }\n : context;\n const activation = perform(target, linkContext);\n activated.emit({ target, activation });\n return activation;\n };\n\n const getLinkAt = (page: PageRef | number, point: Point): Link | null => {\n let best: Link | null = null;\n for (const link of listLinks(page)) {\n if (!contains(link.bounds, point)) continue;\n if (\n !best ||\n link.bounds.width * link.bounds.height < best.bounds.width * best.bounds.height\n ) {\n best = link;\n }\n }\n return best;\n };\n\n const getLabel = (input: Link | PdfLinkTarget): string => {\n const target = isLink(input) ? input.target : input;\n switch (target.kind) {\n case 'uri':\n return target.uri;\n case 'goto':\n return 'Go to destination';\n case 'named':\n return target.name;\n default:\n return 'Link';\n }\n };\n\n const api: LinkHostCapability = {\n listLinks,\n getLink: (page, linkId) => listLinks(page).find((link) => link.id === linkId) ?? null,\n getLinkAt,\n listAllLinks: async (options) => {\n const layouts = ctx.document()?.pages ?? [];\n await ctx.cancellable(\n options?.signal,\n Promise.all(layouts.map((layout) => ensureLoaded(layout.ref, options))),\n );\n return layouts.flatMap((layout) => listLinks(layout.ref));\n },\n ensureLoaded,\n isLoaded: (page) => {\n const ref = ctx.getPage(page)?.ref;\n return !!ref && (annotationHost() !== null || pages.getStatus(ref) === 'ready');\n },\n getStatus: (page) => {\n const ref = ctx.getPage(page)?.ref;\n if (!ref) return 'idle';\n return annotationHost() ? 'ready' : pages.getStatus(ref);\n },\n resolve,\n activate,\n activateAt: (pageArgument, point, context) => {\n const page = ctx.pageOf(pageArgument).ref;\n const link = getLinkAt(page, point);\n return link ? activate(link, { page, ...context }) : null;\n },\n getLabel,\n onActivated: activated.on,\n onLoaded: loaded.on,\n registerUriOpener: (opener) => {\n uriOpeners.unshift(opener);\n return () => {\n const index = uriOpeners.indexOf(opener);\n if (index >= 0) uriOpeners.splice(index, 1);\n };\n },\n isNavigationEngaged: () => ctx.tryGet(InteractionToken)?.activeToolEnables('link-nav') ?? false,\n };\n\n return {\n api,\n connect() {\n connectLink(ctx);\n },\n };\n}\n","import { definePlugin } from '@embedpdf/core';\nimport { ActionsToken } from '@embedpdf/plugin-actions/contract';\nimport { AnnotationToken } from '@embedpdf/plugin-annotation/contract';\nimport { InteractionToken } from '@embedpdf/plugin-interaction/contract';\nimport { StageToken } from '@embedpdf/plugin-stage/contract';\n\nimport { createLinkController } from './controller';\nimport { LinkToken } from './host-contract';\n\n/**\n * Clickable link regions and their activation. Document-scoped; needs the\n * interaction hub (the navigation tool tag), optionally the stage (reveal),\n * the annotation plugin (the folded link model) and actions (`/A` trees).\n */\nexport const linkPlugin = () =>\n definePlugin({\n id: 'link',\n token: LinkToken,\n scope: 'document',\n requires: [InteractionToken],\n optional: [StageToken, AnnotationToken, ActionsToken],\n create: createLinkController,\n });\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embedpdf/plugin-link",
3
- "version": "3.0.0-next.14",
3
+ "version": "3.0.0-next.16",
4
4
  "private": false,
5
5
  "description": "The navigation plane for PDF link annotations: per-page clickable link areas + one activate() entry point (GoTo → stage reveal, URI → framework opener). Works standalone in viewer deployments; with the annotation plugin present it reads the folded model (no double fetch) and a Behavior hands links to the editing plane when the link tool is active.",
6
6
  "type": "module",
@@ -16,20 +16,24 @@
16
16
  "import": "./dist/contract.js",
17
17
  "require": "./dist/contract.cjs"
18
18
  },
19
+ "./contract/host": {
20
+ "import": "./dist/contract/host.js",
21
+ "require": "./dist/contract/host.cjs"
22
+ },
19
23
  "./package.json": "./package.json"
20
24
  },
21
25
  "files": [
22
26
  "dist"
23
27
  ],
24
28
  "dependencies": {
25
- "@embedpdf/core-annotation": "3.0.0-next.14",
26
- "@embedpdf/core": "3.0.0-next.14",
27
- "@embedpdf/plugin-annotation": "3.0.0-next.14",
28
- "@embedpdf/core-geometry": "3.0.0-next.14",
29
- "@embedpdf/plugin-interaction": "3.0.0-next.14",
30
- "@embedpdf/engine-core": "3.0.0-next.14",
31
- "@embedpdf/plugin-actions": "3.0.0-next.14",
32
- "@embedpdf/plugin-stage": "3.0.0-next.14"
29
+ "@embedpdf/core-annotation": "3.0.0-next.16",
30
+ "@embedpdf/core": "3.0.0-next.16",
31
+ "@embedpdf/plugin-annotation": "3.0.0-next.16",
32
+ "@embedpdf/plugin-interaction": "3.0.0-next.16",
33
+ "@embedpdf/plugin-stage": "3.0.0-next.16",
34
+ "@embedpdf/core-geometry": "3.0.0-next.16",
35
+ "@embedpdf/plugin-actions": "3.0.0-next.16",
36
+ "@embedpdf/engine-core": "3.0.0-next.16"
33
37
  },
34
38
  "devDependencies": {
35
39
  "typescript": "^5.0.0",
@@ -1,92 +0,0 @@
1
- import { PageObjectNumber } from "@embedpdf/core";
2
- import { ActionDispatchResult } from "@embedpdf/plugin-actions/contract";
3
- import { LinkNavItem } from "@embedpdf/plugin-annotation/contract";
4
- import { AnnotationRef, PdfActionTree, PdfDestination, PdfLinkTarget } from "@embedpdf/engine-core/runtime";
5
- //#region src/types.d.ts
6
- /**
7
- * What one activation did. `revealed` = the stage moved the camera;
8
- * `destination` = no stage plugin — the embedder gets the explicit
9
- * destination to handle; `uri` = opening is the FRAMEWORK layer's job (the
10
- * plugin is DOM-free); `named` = a viewer verb (NextPage…) reported until
11
- * stage verbs land; `reported` = read-only/executable-shaped targets
12
- * (javascript / goto-remote / launch / unsupported) — surfaced, NEVER
13
- * executed here (javascript belongs to the scripting orchestrator).
14
- */
15
- type LinkActivation = {
16
- outcome: 'revealed';
17
- } | {
18
- outcome: 'destination';
19
- destination: PdfDestination;
20
- } | {
21
- outcome: 'uri';
22
- uri: string;
23
- } | {
24
- outcome: 'named';
25
- name: string;
26
- } | {
27
- outcome: 'reported';
28
- target: PdfLinkTarget;
29
- } |
30
- /** The action engine took the activation (full payload tree, mixed /Next
31
- * chains included). `dispatch` is the in-flight result; the framework
32
- * opener must do NOTHING for this outcome — the actions UI adapter owns
33
- * any URI open (the no-double-open rule). */
34
- {
35
- outcome: 'dispatched';
36
- dispatch: Promise<ActionDispatchResult>;
37
- } | {
38
- outcome: 'none';
39
- };
40
- /** Optional activation context: the payload-carrying /A tree (and its
41
- * annotation identity) from the LinkNavItem, enabling action-engine
42
- * delegation. Without it — or without the actions plugin — activation
43
- * follows the classic root-projection path. */
44
- interface LinkActivateContext {
45
- activate?: PdfActionTree;
46
- ref?: AnnotationRef;
47
- pon?: PageObjectNumber;
48
- }
49
- interface LinkActivateEvent {
50
- target: PdfLinkTarget;
51
- activation: LinkActivation;
52
- }
53
- interface LinkPluginConfig {
54
- /** Every activation (navigations AND reported targets) — analytics /
55
- * custom handling. Called after the built-in handling ran. */
56
- onActivate?: (event: LinkActivateEvent) => void;
57
- }
58
- /**
59
- * The navigation plane of link annotations. Data comes from ONE of two
60
- * sources, picked at init: the annotation plugin's folded model when that
61
- * plugin is present (zero extra engine reads — it already lists every
62
- * page), or the plugin's own engine reads in viewer-only deployments.
63
- */
64
- interface LinkCapability {
65
- /** The clickable link areas of a page (content space, y-down). */
66
- linksOn(pon: PageObjectNumber): LinkNavItem[];
67
- /** Lazy-load a page's links (no-op when the annotation plugin owns the data). */
68
- ensurePage(pon: PageObjectNumber): void;
69
- /** Whether the navigation plane currently owns links: the active tool
70
- * enables `link-nav` (pointer/pan/form-fill by default). While false —
71
- * the link tool or any authoring tool is active — the annotation plane
72
- * owns them and the LinkLayer stands down. */
73
- engaged(): boolean;
74
- /** THE one activation entry point (framework layers call it on click). */
75
- activate(target: PdfLinkTarget, context?: LinkActivateContext): LinkActivation;
76
- }
77
- /** Engine-backed page cache (viewer-only deployments). */
78
- interface LinkState {
79
- pages: Record<number, LinkNavItem[]>;
80
- }
81
- type LinkAction = {
82
- type: 'SET_PAGE';
83
- pon: number;
84
- items: LinkNavItem[];
85
- } | {
86
- type: 'DROP_PAGE';
87
- pon: number;
88
- };
89
- declare const LinkToken: import("@embedpdf/core").CapabilityToken<LinkCapability>;
90
- //#endregion
91
- export { LinkCapability as a, LinkState as c, PdfLinkTarget as d, LinkActivation as i, LinkToken as l, LinkActivateContext as n, LinkNavItem as o, LinkActivateEvent as r, LinkPluginConfig as s, LinkAction as t, PdfDestination as u };
92
- //# sourceMappingURL=contract-B8bEMeH8.d.ts.map