@docs.plus/extension-hyperlink 1.5.1 → 2.0.0

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.
@@ -0,0 +1,414 @@
1
+ import { Editor, Mark } from '@tiptap/core';
2
+ import { MarkType } from '@tiptap/pm/model';
3
+ import { Placement } from '@floating-ui/dom';
4
+ export { registerCustomProtocol } from 'linkifyjs';
5
+
6
+ /**
7
+ * Canonicalize a user-supplied href so stored marks point somewhere
8
+ * absolute (bare `google.com` would otherwise resolve against
9
+ * `document.baseURI` at render time). See README → Normalization for
10
+ * the full pipeline order.
11
+ */
12
+ declare const normalizeHref: (raw: string, defaultProtocol?: string) => string;
13
+ /**
14
+ * Shape `findLinks` produces (linkifyjs match + our synthetic
15
+ * special-scheme / phone entries). Loosened so linkifyjs types don't
16
+ * leak across the public boundary.
17
+ */
18
+ type LinkifyMatchLike = {
19
+ type: string;
20
+ value: string;
21
+ href: string;
22
+ };
23
+
24
+ /**
25
+ * Schemes blocked at every write boundary: `javascript:` / `vbscript:`
26
+ * (script execution), `data:` (arbitrary HTML), `file:` (local FS),
27
+ * `blob:` (in-page memory; never legitimate in stored content).
28
+ *
29
+ * Twin of `extension-hypermultimedia`'s copy. Adding a scheme to one and not
30
+ * the other opens a hole in the twin, so preflight diffs them byte-for-byte.
31
+ */
32
+ declare const DANGEROUS_SCHEME_RE: RegExp;
33
+ /**
34
+ * Single XSS gate used at every read/write boundary. Returns `false`
35
+ * for nullish/empty hrefs and any scheme matched by
36
+ * {@link DANGEROUS_SCHEME_RE}. See README → Security.
37
+ */
38
+ declare const isSafeHref: (href: string | null | undefined) => href is string;
39
+ type ValidateURLOptions = {
40
+ customValidator?: (url: string) => boolean;
41
+ };
42
+ /**
43
+ * Shape-validate a URL: web schemes with a plausible host, app schemes from
44
+ * the `getSpecialUrlInfo` catalog, and bare E.164 phones. Rejects schemes
45
+ * matched by {@link DANGEROUS_SCHEME_RE}. See README → Validation.
46
+ */
47
+ declare const validateURL: (url: string, options?: ValidateURLOptions) => boolean;
48
+
49
+ /** Discriminated input to {@link URLDecisions.forWrite}. */
50
+ type WriteInput = {
51
+ kind: 'text';
52
+ text: string;
53
+ } | {
54
+ kind: 'href';
55
+ href: string;
56
+ } | {
57
+ kind: 'match';
58
+ match: LinkifyMatchLike;
59
+ };
60
+ type WriteResult = {
61
+ /** Canonical, gated, write-safe href ready to set on the mark. */
62
+ href: string;
63
+ /** Source offset; `0` for `href`/`match` inputs. */
64
+ start: number;
65
+ end: number;
66
+ };
67
+ type ReadDecision = {
68
+ /** Passes the full gate (`isSafeHref` + `isAllowedUri`) — required before any `window.open`. */
69
+ navigable: boolean;
70
+ };
71
+ type WriteOptions = {
72
+ /** Per-call override of the controller's `validate`. */
73
+ validate?: (url: string) => boolean;
74
+ };
75
+ interface URLDecisions {
76
+ /** Resolve a write candidate to its canonical, gated href(s). Returns `[]` when nothing passes. */
77
+ forWrite(input: WriteInput, opts?: WriteOptions): WriteResult[];
78
+ /** Read-side gate for render + navigation. */
79
+ forRead(href: string | null | undefined): ReadDecision;
80
+ /**
81
+ * Pure shape check — skips the gate stack on purpose. That keeps a tightened
82
+ * `isAllowedUri` policy out of autolink's "is the existing link still
83
+ * link-shaped?" cleanup pass.
84
+ */
85
+ detect(text: string): boolean;
86
+ }
87
+
88
+ /** Deps every interaction (input rule, paste rule, autolink, paste handler, click handler) reads. */
89
+ interface LinkContext {
90
+ type: MarkType;
91
+ editor: Editor;
92
+ /** URL Decisions pipeline; one per `createLinkContext` call (the extension allocates one per editor, cached on `storage.context`). */
93
+ urls: URLDecisions;
94
+ options: HyperlinkOptions;
95
+ /** Convenience accessor for `options.validate`. */
96
+ validate?: (url: string) => boolean;
97
+ /** Preview popover factory, or `null` when the host opted out. */
98
+ previewPopover: ((options: PreviewHyperlinkOptions) => HTMLElement | null) | null;
99
+ }
100
+
101
+ type SetHyperlinkAttributes = {
102
+ href: string;
103
+ target?: string | null;
104
+ title?: string | null;
105
+ image?: string | null;
106
+ } & Record<string, unknown>;
107
+ type EditHyperlinkAttributes = {
108
+ newText?: string;
109
+ newURL?: string;
110
+ title?: string;
111
+ image?: string;
112
+ };
113
+ interface HyperlinkPublicCommands<ReturnType> {
114
+ /** Write the hyperlink mark; returns `false` when `href` is missing or fails the gate. See README → Commands. */
115
+ setHyperlink: (attributes: SetHyperlinkAttributes) => ReturnType;
116
+ /** Remove the hyperlink mark from the current selection. */
117
+ unsetHyperlink: () => ReturnType;
118
+ /** Toggle the hyperlink mark; same gates as `setHyperlink`. */
119
+ toggleHyperlink: (attributes: SetHyperlinkAttributes) => ReturnType;
120
+ /** Open the create popover (configured `popovers.createHyperlink` or the prebuilt fallback); no-op only when the factory returns `null`. See README → Commands. */
121
+ openCreateHyperlinkPopover: (attributes?: Partial<HyperlinkAttributes>) => ReturnType;
122
+ editHyperlinkText: (text: string) => ReturnType;
123
+ editHyperlinkHref: (href: string) => ReturnType;
124
+ editHyperlink: (attributes?: EditHyperlinkAttributes) => ReturnType;
125
+ /** Migration alias for `setHyperlink` (drop-in `@tiptap/extension-link` compat). */
126
+ setLink: (attributes: SetHyperlinkAttributes) => ReturnType;
127
+ /** Migration alias for `unsetHyperlink`. */
128
+ unsetLink: () => ReturnType;
129
+ /** Migration alias for `toggleHyperlink`. */
130
+ toggleLink: (attributes: SetHyperlinkAttributes) => ReturnType;
131
+ }
132
+ declare module '@tiptap/core' {
133
+ interface Commands<ReturnType> {
134
+ hyperlink: HyperlinkPublicCommands<ReturnType>;
135
+ }
136
+ }
137
+
138
+ interface LinkProtocolOptions {
139
+ scheme: string;
140
+ optionalSlashes?: boolean;
141
+ }
142
+ /** Context for the user `isAllowedUri` hook; mirrors `@tiptap/extension-link`. */
143
+ type IsAllowedUriContext = {
144
+ /** Built-in safety gate (`isSafeHref`). */
145
+ defaultValidate: (uri: string) => boolean;
146
+ /** Protocols registered via the `protocols` option. */
147
+ protocols: Array<LinkProtocolOptions | string>;
148
+ defaultProtocol: string;
149
+ };
150
+ /**
151
+ * `Extra` extends the built-ins with consumer-typed fields; it defaults to
152
+ * `Record<string, unknown>` to keep the v2 open index signature. Example: README → TypeScript.
153
+ */
154
+ type HyperlinkAttributes<Extra extends Record<string, unknown> = Record<string, unknown>> = {
155
+ href: string | null;
156
+ target: string | null;
157
+ rel: string | null;
158
+ class: string | null;
159
+ title: string | null;
160
+ image: string | null;
161
+ } & Extra;
162
+ type PreviewHyperlinkOptions = {
163
+ editor: Editor;
164
+ link: HTMLAnchorElement;
165
+ nodePos: number;
166
+ attrs: HyperlinkAttributes;
167
+ validate?: (url: string) => boolean;
168
+ /** Composed XSS + `isAllowedUri` gate; BYO popovers must call this before any `window.open`. */
169
+ isAllowedUri?: (uri: string) => boolean;
170
+ };
171
+ type CreateHyperlinkOptions = {
172
+ editor: Editor;
173
+ extensionName: string;
174
+ attributes: Partial<HyperlinkAttributes>;
175
+ validate?: (url: string) => boolean;
176
+ };
177
+ type EditHyperlinkOptions = {
178
+ editor: Editor;
179
+ link: HTMLAnchorElement;
180
+ /** Optional mark position; lets edit/back recover from ProseMirror replacing duplicate link DOM. */
181
+ nodePos?: number;
182
+ validate?: (url: string) => boolean;
183
+ /** Composed XSS + `isAllowedUri` gate to preserve preview policy across edit/back. */
184
+ isAllowedUri?: (uri: string) => boolean;
185
+ /** Override the default Back behaviour. Default re-opens the preview popover via `openPreviewHyperlink`. */
186
+ onBack?: () => void;
187
+ /** Mark name; defaults to `HYPERLINK_MARK_NAME`. */
188
+ markName?: string;
189
+ };
190
+ interface HyperlinkOptions {
191
+ autolink: boolean;
192
+ protocols: Array<LinkProtocolOptions | string>;
193
+ openOnClick: boolean;
194
+ linkOnPaste: boolean;
195
+ HTMLAttributes: Partial<HyperlinkAttributes>;
196
+ popovers: {
197
+ previewHyperlink?: ((options: PreviewHyperlinkOptions) => HTMLElement | null) | null;
198
+ editHyperlink?: ((options: EditHyperlinkOptions) => HTMLElement | null) | null;
199
+ createHyperlink?: ((options: CreateHyperlinkOptions) => HTMLElement | null) | null;
200
+ };
201
+ validate?: (url: string) => boolean;
202
+ /** Scheme used by `normalizeHref` to promote bare domains. Default `'https'`. See README → Options. */
203
+ defaultProtocol: string;
204
+ /** URI policy hook — runs AFTER `isSafeHref`, BEFORE the mark is written. Mirrors `@tiptap/extension-link`. */
205
+ isAllowedUri?: (uri: string, ctx: IsAllowedUriContext) => boolean;
206
+ /** Per-link autolink veto. Return `false` to skip autolinking a specific URI (manual `setHyperlink` still works). */
207
+ shouldAutoLink?: (uri: string) => boolean;
208
+ /** Click inside a link selects the whole mark range (editable mode only). Mirrors `@tiptap/extension-link`. */
209
+ enableClickSelection: boolean;
210
+ /** ArrowRight at the right edge of a link exits the mark. Mirrors `@tiptap/extension-link`. */
211
+ exitable: boolean;
212
+ }
213
+ /** Extension storage; one slot per editor instance. */
214
+ interface HyperlinkStorage {
215
+ /**
216
+ * Cached `LinkContext`; populated on first access so all four `add*`
217
+ * hooks share one URL Decisions pipeline instead of allocating per hook.
218
+ */
219
+ context: LinkContext | null;
220
+ }
221
+ declare const Hyperlink: Mark<HyperlinkOptions, HyperlinkStorage>;
222
+
223
+ interface BuildPreviewOptionsFromAnchorArgs {
224
+ editor: Editor;
225
+ link: HTMLAnchorElement;
226
+ /** Prefer this ProseMirror position before falling back to DOM similarity. */
227
+ nodePos?: number;
228
+ /** Forwarded onto the returned options; usually the configured `validate` option. */
229
+ validate?: (url: string) => boolean;
230
+ /** Preserve the host's composed navigation gate when rebuilding preview from edit/back. */
231
+ isAllowedUri?: (uri: string) => boolean;
232
+ /** Mark name to look up; defaults to `'hyperlink'`. Override only if the schema mark was renamed. */
233
+ markName?: string;
234
+ }
235
+ /**
236
+ * Exists so the edit popover's Back button and BYO `editHyperlink` factories
237
+ * share one `posAtDOM → mark.attrs` lookup. The DOM `link.href` property is
238
+ * never the raw-href fallback — it resolves relative hrefs against
239
+ * `document.baseURI`, leaking the host origin into a stored attribute.
240
+ */
241
+ declare function buildPreviewOptionsFromAnchor({ editor, link, nodePos: preferredNodePos, validate, isAllowedUri, markName }: BuildPreviewOptionsFromAnchorArgs): PreviewHyperlinkOptions;
242
+
243
+ /**
244
+ * Open the create-hyperlink popover anchored to the current selection.
245
+ * Returns `false` when the host opted out (factory returned `null`).
246
+ */
247
+ declare function openCreateHyperlink(editor: Editor, attributes?: Partial<HyperlinkAttributes>): boolean;
248
+
249
+ /**
250
+ * Open the edit popover anchored to a hyperlink. The prebuilt popover's
251
+ * Back button closes over `opts`, so no extra wiring is needed to
252
+ * return to the preview.
253
+ */
254
+ declare function openEditHyperlink(opts: EditHyperlinkOptions): void;
255
+
256
+ /**
257
+ * Returns `false` when the host opted out (factory returned `null`, e.g. a
258
+ * mobile bottom sheet). The click handler gates caret placement on it —
259
+ * `.focus()` on an opting-out host makes iOS Safari scroll the editor.
260
+ */
261
+ declare function openPreviewHyperlink(opts: PreviewHyperlinkOptions): boolean;
262
+
263
+ type PopoverKind = 'preview' | 'edit' | 'create' | (string & {});
264
+ type VirtualCoordinates = {
265
+ getBoundingClientRect: () => {
266
+ x: number;
267
+ y: number;
268
+ width: number;
269
+ height: number;
270
+ };
271
+ contextElement?: HTMLElement;
272
+ };
273
+ interface ManagedPopover {
274
+ hide: () => void;
275
+ destroy: () => void;
276
+ updateReference: (referenceElement?: HTMLElement, coordinates?: VirtualCoordinates) => void;
277
+ }
278
+ type AdoptMetadata = {
279
+ /** The popover's root element — what subscribers attach focus rings, scroll-freezes, or DOM observers to. */
280
+ element: HTMLElement;
281
+ /** The anchor the popover is glued to — `null` for virtual-coords popovers (e.g. selection-anchored create). */
282
+ referenceElement: HTMLElement | null;
283
+ };
284
+ type ControllerState = {
285
+ kind: 'idle';
286
+ } | ({
287
+ kind: 'mounted';
288
+ popoverKind: PopoverKind;
289
+ } & AdoptMetadata);
290
+ interface PopoverController {
291
+ adopt(popover: ManagedPopover, kind: PopoverKind, metadata: AdoptMetadata): () => void;
292
+ close(): void;
293
+ reposition(referenceElement?: HTMLElement, coordinates?: VirtualCoordinates): void;
294
+ getState(): ControllerState;
295
+ subscribe(listener: (state: ControllerState) => void): () => void;
296
+ }
297
+ declare function getDefaultController(): PopoverController;
298
+
299
+ type PopoverAnchor = {
300
+ referenceElement: HTMLElement;
301
+ coordinates?: never;
302
+ } | {
303
+ referenceElement?: never;
304
+ coordinates: VirtualCoordinates;
305
+ };
306
+ type PopoverShellOptions = {
307
+ content: HTMLElement;
308
+ placement?: Placement;
309
+ offset?: number;
310
+ showArrow?: boolean;
311
+ className?: string;
312
+ zIndex?: number;
313
+ /**
314
+ * ARIA has no popover role — the shell carries its content's role
315
+ * (`toolbar`, `dialog`, …) and stays role-less when omitted.
316
+ */
317
+ role?: string;
318
+ /** Same ARIA axis as `role` — shell-level so BYO content inherits the accessible name. */
319
+ ariaLabel?: string;
320
+ /**
321
+ * Nodes that must not light-dismiss (toggle triggers). When set, only these
322
+ * are ignored — not `referenceElement` — so a larger position surface (e.g.
323
+ * a toolbar bar) can still dismiss on sibling clicks. When omitted, the
324
+ * HTMLElement `referenceElement` is ignored instead.
325
+ */
326
+ ignoreOutsideClickOn?: HTMLElement | HTMLElement[];
327
+ /**
328
+ * When false, `shift` only moves on the main axis so end-aligned menus
329
+ * (e.g. toolbar overflow) stay pinned to the reference’s end edge.
330
+ * @default true
331
+ */
332
+ crossAxisShift?: boolean;
333
+ onShow?: () => void;
334
+ onHide?: () => void;
335
+ };
336
+ type PopoverOptions = PopoverAnchor & PopoverShellOptions;
337
+ /**
338
+ * One-shot once opened: hiding releases the controller's ownership and nothing
339
+ * re-adopts, so `show()` will not reopen it. Build a new popover instead — a
340
+ * re-shown one would sit outside the single-popover invariant, invisible to
341
+ * `close()` and never evicted by the next `adopt()`.
342
+ */
343
+ interface Popover {
344
+ element: HTMLElement;
345
+ /** No-op once the popover has been hidden or destroyed. */
346
+ show: () => void;
347
+ /** Terminal once shown. Before the first `show()` it no-ops and keeps ownership. */
348
+ hide: () => void;
349
+ destroy: () => void;
350
+ isVisible: () => boolean;
351
+ setContent: (content: HTMLElement) => void;
352
+ updateReference: (referenceElement?: HTMLElement, coordinates?: VirtualCoordinates) => void;
353
+ }
354
+ declare const DEFAULT_OFFSET = 8;
355
+ declare function createPopover(options: PopoverOptions): Popover;
356
+
357
+ /** Hide the shared tooltip bubble and cancel any pending show. */
358
+ declare function hideTooltip(): void;
359
+ /** Show `label` above `target` after a short hover/focus delay. Returns a detach function for surfaces that re-render in place. */
360
+ declare function attachTooltip(target: HTMLElement, label: string): () => void;
361
+
362
+ declare function createHyperlinkPopover(options: CreateHyperlinkOptions): HTMLElement;
363
+
364
+ declare function editHyperlinkPopover(options: EditHyperlinkOptions): HTMLElement;
365
+
366
+ declare function previewHyperlinkPopover(options: PreviewHyperlinkOptions): HTMLElement;
367
+
368
+ /**
369
+ * `window.open` features string for safe navigation — every
370
+ * `window.open(href, '_blank', …)` call needs this explicit arg or the
371
+ * opened tab can read `window.opener` and the Referer leaks. Re-exported
372
+ * so downstream popovers pin the same value.
373
+ */
374
+ declare const SAFE_WINDOW_FEATURES: "noopener,noreferrer";
375
+
376
+ declare const copyToClipboard: (text: string, callback?: (success: boolean) => void) => Promise<boolean>;
377
+
378
+ declare const createHTMLElement: <K extends keyof HTMLElementTagNameMap>(type: K, props?: Partial<HTMLElementTagNameMap[K]>) => HTMLElementTagNameMap[K];
379
+
380
+ interface IconProps {
381
+ size?: number;
382
+ fill?: string;
383
+ }
384
+ declare const Copy: ({ size, fill }?: IconProps) => string;
385
+ declare const LinkOff: ({ size, fill }?: IconProps) => string;
386
+ declare const Pencil: ({ size, fill }?: IconProps) => string;
387
+
388
+ /**
389
+ * Special URL schemes and well-known domains the extension classifies
390
+ * for the preview popover.
391
+ *
392
+ * Catalog source: https://github.com/bhagyas/app-urls
393
+ */
394
+ /**
395
+ * String-literal union so consumers can declare a
396
+ * `Partial<Record<SpecialUrlType, …>>` icon map with exhaustive
397
+ * autocomplete — type-only, zero runtime bytes. Naming convention lives
398
+ * in this package's AGENTS.md.
399
+ */
400
+ type SpecialUrlType = 'email' | 'phone' | 'sms' | 'facetime' | 'facetime-audio' | 'whatsapp' | 'telegram' | 'discord' | 'skype' | 'slack' | 'twitter' | 'facebook' | 'instagram' | 'linkedin' | 'snapchat' | 'reddit' | 'tiktok' | 'zoom' | 'teams' | 'webex' | 'meet' | 'calendar' | 'reminders' | 'contacts' | 'maps' | 'music' | 'apple-tv' | 'notes' | 'photos' | 'shortcuts' | 'app-store' | 'github' | 'gitlab' | 'vscode' | 'notion' | 'obsidian' | 'figma' | 'youtube' | 'spotify' | 'netflix' | 'twitch' | 'amazon' | 'uber' | 'lyft';
401
+ type SpecialUrlInfo = {
402
+ type: SpecialUrlType;
403
+ title: string;
404
+ category: 'communication' | 'social' | 'productivity' | 'entertainment' | 'shopping' | 'development' | 'apple' | 'other';
405
+ };
406
+ /**
407
+ * Classify `url` against the catalog (scheme prefix or host suffix).
408
+ * Returns `null` for plain web URLs — the consumer should fall back to
409
+ * a generic favicon. Mapping `type`/`category` to an icon is the
410
+ * consumer's job; the extension stays brand-neutral on purpose.
411
+ */
412
+ declare const getSpecialUrlInfo: (url: string) => SpecialUrlInfo | null;
413
+
414
+ export { type AdoptMetadata, type BuildPreviewOptionsFromAnchorArgs, type ControllerState, Copy, type CreateHyperlinkOptions, DANGEROUS_SCHEME_RE, DEFAULT_OFFSET, type EditHyperlinkAttributes, type EditHyperlinkOptions, Hyperlink, type HyperlinkAttributes, type HyperlinkOptions, type HyperlinkPublicCommands, type HyperlinkStorage, type IconProps, type IsAllowedUriContext, LinkOff, type LinkProtocolOptions, type LinkifyMatchLike, Pencil, type Popover, type PopoverController, type PopoverKind, type PopoverOptions, type PreviewHyperlinkOptions, SAFE_WINDOW_FEATURES, type SetHyperlinkAttributes, type SpecialUrlInfo, type SpecialUrlType, type ValidateURLOptions, type VirtualCoordinates, attachTooltip, buildPreviewOptionsFromAnchor, copyToClipboard, createHTMLElement, createHyperlinkPopover, createPopover, Hyperlink as default, editHyperlinkPopover, getDefaultController, getSpecialUrlInfo, hideTooltip, isSafeHref, normalizeHref, openCreateHyperlink, openEditHyperlink, openPreviewHyperlink, previewHyperlinkPopover, validateURL };