@docs.plus/extension-hyperlink 1.5.1 → 4.3.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,362 @@
1
+ <!-- markdownlint-disable MD024 -->
2
+
3
+ # Changelog
4
+
5
+ All notable changes to `@docs.plus/extension-hyperlink` are documented here.
6
+ This project follows [Semantic Versioning](https://semver.org/) and [Conventional Commits](https://conventionalcommits.org). Section headings (`Added` / `Changed` / `Fixed` / `Security`) intentionally repeat per version, per the [Keep a Changelog](https://keepachangelog.com/) convention.
7
+
8
+ ---
9
+
10
+ ## Migrating from v1.x to v4.x
11
+
12
+ The last npm-published v1 release was `1.5.2`. Versions `2.x` and `3.x` were internal monorepo refactors and were never published. If you are upgrading from `^1.5.2` directly to `4.x`, the API has been substantially redesigned. This section is the short, actionable diff; the full per-version detail lives under [4.0.0](#400--2026-04-15) (renames + popover contract), [4.1.0](#410--2026-04-15) (XSS hardening + bug fixes), [4.2.0](#420--2026-04-15) (stylesheet + clean-room test harness), and [4.3.0](#430--2026-04-16) (unified href canonicalization).
13
+
14
+ ### What changed at a glance
15
+
16
+ - **Options renamed** to align with Tiptap conventions
17
+ (`autoHyperlink` → `autolink`, `hyperlinkOnPaste` → `linkOnPaste`).
18
+ - **Commands renamed** with consistent casing
19
+ (`editHyperLinkText` → `editHyperlinkText`, `editHyperLinkHref` → `editHyperlinkHref`).
20
+ - **CSS classes renamed** from camelCase to kebab-case
21
+ (`.hyperlinkCreatePopover` → `.hyperlink-create-popover`, …).
22
+ - **Default stylesheet ships separately** — `import '@docs.plus/extension-hyperlink/styles.css'`.
23
+ v1 inlined CSS via JS; v4 ships a small opt-in CSS file so fully-custom UIs pay zero cost.
24
+ - **Popover contract changed** — `createHyperlink` callback now returns
25
+ `HTMLElement | null` (was `void`); the extension owns positioning, you own content.
26
+ - **Meta key renamed** — `tr.setMeta('preventAutoHyperlink', …)` → `tr.setMeta('preventAutolink', …)`.
27
+ - **Type augmentation fixed** — commands are augmented under `hyperlink:` (was `link:`).
28
+ - **Hardened XSS guards** — `javascript:`, `data:`, and `vbscript:` are now blocked
29
+ at every entry point (parse, paste, input rule, click, popover open). If you
30
+ intentionally stored such URLs, they will now be rejected.
31
+ - **Plausible-host validation** — `validateURL` now rejects typo URLs like
32
+ `https://googlecom` (no TLD dot, not localhost, not an IP literal).
33
+
34
+ ### Code diff
35
+
36
+ ```diff
37
+ Hyperlink.configure({
38
+ - autoHyperlink: true,
39
+ - hyperlinkOnPaste: true,
40
+ + autolink: true,
41
+ + linkOnPaste: true,
42
+ popovers: {
43
+ previewHyperlink: myPreviewFn,
44
+ - createHyperlink: myCreateFn, // was (options) => void
45
+ + createHyperlink: myCreateFn, // now (options) => HTMLElement | null
46
+ }
47
+ })
48
+
49
+ // Commands
50
+ -editor.commands.editHyperLinkText('New Text')
51
+ -editor.commands.editHyperLinkHref('https://example.com')
52
+ +editor.commands.editHyperlinkText('New Text')
53
+ +editor.commands.editHyperlinkHref('https://example.com')
54
+
55
+ // Meta key in transactions
56
+ -tr.setMeta('preventAutoHyperlink', true)
57
+ +tr.setMeta('preventAutolink', true)
58
+ ```
59
+
60
+ ```css
61
+ /* CSS selectors */
62
+ - .hyperlinkCreatePopover { … }
63
+ - .hyperlinkPreviewPopover { … }
64
+ - .hyperlinkEditPopover { … }
65
+ - .buttonsWrapper { … }
66
+ - .inputsWrapper { … }
67
+ - .textWrapper { … }
68
+ - .hrefWrapper { … }
69
+ - .backButton { … }
70
+ - .btn_applyModal { … }
71
+ + .hyperlink-create-popover { … }
72
+ + .hyperlink-preview-popover { … }
73
+ + .hyperlink-edit-popover { … }
74
+ + .buttons-wrapper { … }
75
+ + .inputs-wrapper { … }
76
+ + .text-wrapper { … }
77
+ + .href-wrapper { … }
78
+ + .back-button { … }
79
+ + .apply-button { … }
80
+ ```
81
+
82
+ ```ts
83
+ // Popover types — no more hand-rolled types
84
+ import type {
85
+ PreviewHyperlinkOptions,
86
+ CreateHyperlinkOptions
87
+ } from '@docs.plus/extension-hyperlink'
88
+ ```
89
+
90
+ ### One-shot rename script
91
+
92
+ For the rename-only changes, run this in your project root and commit the diff:
93
+
94
+ ```bash
95
+ rg -l "autoHyperlink|hyperlinkOnPaste|editHyperLinkText|editHyperLinkHref|preventAutoHyperlink|hyperlinkCreatePopover|hyperlinkPreviewPopover|hyperlinkEditPopover|buttonsWrapper|inputsWrapper|textWrapper|hrefWrapper|backButton|btn_applyModal" \
96
+ | xargs sed -i.bak \
97
+ -e 's/autoHyperlink/autolink/g' \
98
+ -e 's/hyperlinkOnPaste/linkOnPaste/g' \
99
+ -e 's/editHyperLinkText/editHyperlinkText/g' \
100
+ -e 's/editHyperLinkHref/editHyperlinkHref/g' \
101
+ -e 's/preventAutoHyperlink/preventAutolink/g' \
102
+ -e 's/hyperlinkCreatePopover/hyperlink-create-popover/g' \
103
+ -e 's/hyperlinkPreviewPopover/hyperlink-preview-popover/g' \
104
+ -e 's/hyperlinkEditPopover/hyperlink-edit-popover/g' \
105
+ -e 's/buttonsWrapper/buttons-wrapper/g' \
106
+ -e 's/inputsWrapper/inputs-wrapper/g' \
107
+ -e 's/textWrapper/text-wrapper/g' \
108
+ -e 's/hrefWrapper/href-wrapper/g' \
109
+ -e 's/backButton/back-button/g' \
110
+ -e 's/btn_applyModal/apply-button/g'
111
+ ```
112
+
113
+ The rename script handles the mechanical changes. The semantic changes that
114
+ require code review — popover contract returning `HTMLElement | null`, the
115
+ new `styles.css` import, and the stricter URL validation — are not safely
116
+ automatable. Read the [4.0.0](#400--2026-04-15) and [4.1.0](#410--2026-04-15)
117
+ sections to confirm those before shipping the upgrade.
118
+
119
+ ### Need help?
120
+
121
+ Open an issue at <https://github.com/docs-plus/docs.plus/issues> with the
122
+ label `extension-hyperlink` + `migration` and a snippet of the v1 config
123
+ you're upgrading from.
124
+
125
+ ---
126
+
127
+ ## [4.3.0] — 2026-04-16
128
+
129
+ ### Added
130
+
131
+ - **`normalizeHref(raw)` helper** (re-exported from the package root) for canonicalizing user-typed hrefs: prepends `https://` to bare domains (`google.com` → `https://google.com`), preserves explicit schemes, passes protocol-relative URLs (`//example.com`) through untouched.
132
+ - **`normalizeLinkifyHref(match)` helper** — same canonical form for `linkifyjs` matches. Routes URL matches through `normalizeHref(match.value)` to upgrade linkifyjs's `http://` default to `https://`; trusts linkifyjs's `href` for non-URL matches (emails → `mailto:`, custom schemes).
133
+ - **`LinkifyMatchLike` type** exported for consumers building BYO popovers that interact with linkifyjs matches directly.
134
+ - **Plausible-host validation** in `validateURL`: web-scheme URLs must have a host with a TLD dot, `localhost`, or an IPv4/IPv6 literal. Rejects typos like `https://googlecom` that linkifyjs would otherwise accept.
135
+ - **`autolink.cy.ts`** E2E spec (5 tests) pinning the unified-href contract across autolink-on-space, paste-over-selection, and email-preservation paths.
136
+
137
+ ### Changed
138
+
139
+ - **Unified write-boundary href canonicalization**: every path that stores a hyperlink mark now produces the same canonical `href` for the same input — create popover, edit popover, `setHyperlink` command, markdown input rule, autolink plugin, paste handler, and paste rule all route through `normalizeHref` or `normalizeLinkifyHref`. Previously, autolink stored bare domains, paste stored `http://`, and only the create popover upgraded to `https://`.
140
+ - **Read-side origin-leak defence**: click handler and preview popover now prefer the stored mark attribute (`attrs.href`) over the DOM `link.href` property, which resolves relative URLs against `document.baseURI`. Prevents a `<a href="google.com">` injected via `setContent` from rendering/opening as `http://<host-origin>/google.com`.
141
+
142
+ ### Fixed
143
+
144
+ - **Autolink email href clobber**: `findLinks`'s trailing-punctuation cleanup was overwriting linkifyjs's canonical `href` (including the `mailto:` prefix) with the punctuation-stripped `value`. Emails now correctly store `mailto:user@example.com` on whitespace autolink.
145
+
146
+ ## [4.2.0] — 2026-04-15
147
+
148
+ ### Added
149
+
150
+ - **Shipped default stylesheet** (`@docs.plus/extension-hyperlink/styles.css`) for the prebuilt create / preview / edit popovers. Small, framework-agnostic, and opt-in — the extension's JavaScript never imports it, so fully-custom UIs pay zero CSS cost. Previously consumers had to write popover styles from scratch or copy them out of the monorepo.
151
+ - **Theming via CSS custom properties**: every visual token (colors, radii, shadow, font, transitions, z-index) exposed as `--hl-*` variables. Colors use CSS `light-dark()` so the popover tracks the nearest ancestor's `color-scheme` (and falls back to `prefers-color-scheme` when no `color-scheme` is set), instead of being locked to the OS preference.
152
+ - **README quick-start** for the prebuilt popovers, plus two runnable vanilla-JS DOM examples (under `<details>`) showing how to build a custom `previewHyperlink` / `createHyperlink` from scratch — restored from the v3 docs and wired to the current `PreviewHyperlinkOptions` / `CreateHyperlinkOptions` shapes.
153
+ - **Clean-room E2E test harness** (Cypress specs across `create`, `preview-edit`, `xss-guards`, `styling`, `custom-popover`) that runs against the **built `dist/` loaded via the published `exports` map** — i.e. exercises exactly what an npm consumer installs, not the monorepo source. Replaces the manual "scratch Tiptap install + click around" ritual done before every release. Run with `bun run test` (~20s headless).
154
+ - **Bun-native playground** (`test/playground/server.ts`, ~15 lines): `Bun.serve({ routes: { '/': index } })` with an HTML import bundles the playground's TypeScript + CSS on demand. Replaces the previous Vite dev server; no bundler config, no dedupe knobs, one less dev dep.
155
+
156
+ ### Security
157
+
158
+ - **`parseHTML` now rejects every dangerous URL scheme**, not only `javascript:`. Previously used a CSS selector (`a[href]:not([href^="javascript:"])`) that silently let `data:` and `vbscript:` through on document load / paste of third-party HTML; replaced with a `getAttrs` callback that runs the full `DANGEROUS_SCHEME_RE` check. Aligns `parseHTML` with the input rule, click handler, and preview popover so all four entry points enforce the same XSS invariant.
159
+
160
+ ### Fixed
161
+
162
+ - **`editHyperlink` "mismatched transaction" error** when editing text + href together via the edit popover. The command helper used to dispatch its own nested `editor.chain()...run()`, which collided with the outer `.extendMarkRange(...).editHyperlink(...).run()` in the popover. Refactored into a composable Tiptap command (`editHyperlinkCommand`) that reads positions + marks off `tr.doc` inside the caller's single transaction. Discovered by the new `preview-edit.cy.ts` spec.
163
+
164
+ ### Changed
165
+
166
+ - **`sideEffects`** in `package.json` is now `["**/*.css"]` (was `false`) so bundlers preserve `import '@docs.plus/extension-hyperlink/styles.css'` while still tree-shaking JS.
167
+ - **Package `exports`** now advertises `./styles.css` and `./package.json` as public subpaths.
168
+ - **`DANGEROUS_SCHEME_RE`** is now part of the public API (re-exported from the package root) so custom popovers can apply the same XSS check as the prebuilt ones.
169
+
170
+ ---
171
+
172
+ ## [4.1.0] — 2026-04-15
173
+
174
+ ### Security
175
+
176
+ - **Blocked dangerous URL schemes** (`javascript:`, `data:`, `vbscript:`) in the markdown input rule, click handler `window.open`, and preview popover link open. Previously, `parseHTML` blocked `javascript:` on import but the input rule allowed it on creation — a stored XSS vector in collaborative editors. Shared `DANGEROUS_SCHEME_RE` regex exported from `utils/validateURL`.
177
+
178
+ ### Fixed
179
+
180
+ - **Click handler attrs mismatch**: `showPopover` now reads mark attributes from the **clicked link's document position** (`view.posAtDOM`) instead of from the current selection (`getAttributes`). Previously, clicking a link when the cursor was on a different link returned wrong `attrs` (href, title, image) to the popover.
181
+ - **Global `linkifyjs.reset()` removed**: `onDestroy` no longer calls `reset()`, which cleared the global linkify protocol registry and broke other editors on the same page. Registered protocols are now additive for the page lifetime.
182
+ - **Edit popover back button**: replaced fragile `setTimeout(() => link.click(), 1)` with an explicit `onBack` callback that re-creates the preview toolbar without simulating DOM clicks.
183
+ - **Floating toolbar async race**: `updatePosition` now checks `visible` after `await computePosition()` to avoid writing to a detached toolbar element if `hide()` ran mid-computation.
184
+ - **Silent error swallowing**: `editHyperlink` helper now logs a `console.warn` on `catch` instead of silently returning `false`.
185
+
186
+ ### Changed
187
+
188
+ - **Hardcoded mark name eliminated**: `editHyperlink` helper and edit popover now accept `markName` parameter (default `'hyperlink'`) instead of hardcoding `state.schema.marks.hyperlink` and `.extendMarkRange('hyperlink')`. All callers pass `this.name`.
189
+ - **Production build preserves `console.warn`/`console.error`**: tsup config changed from `drop: ['console']` to `pure: ['console.log', 'console.debug']` — library errors are now visible in production.
190
+ - **Deduplicated toolbar offset**: `TOOLBAR_OFFSET` in `clickHandler.ts` replaced with shared `DEFAULT_OFFSET` exported from `floatingToolbar.ts`.
191
+ - **`EditHyperlinkModalOptions` exported**: consumers can now type-check edit popover configuration.
192
+
193
+ ---
194
+
195
+ ## [4.0.0] — 2026-04-15
196
+
197
+ ### Breaking Changes
198
+
199
+ - **Renamed options** to align with Tiptap conventions:
200
+ - `autoHyperlink` → `autolink`
201
+ - `hyperlinkOnPaste` → `linkOnPaste`
202
+ - **Renamed commands** to fix mixed casing:
203
+ - `editHyperLinkText` → `editHyperlinkText`
204
+ - `editHyperLinkHref` → `editHyperlinkHref`
205
+ - **Renamed CSS classes** from camelCase to kebab-case:
206
+ - `.hyperlinkCreatePopover` → `.hyperlink-create-popover`
207
+ - `.hyperlinkPreviewPopover` → `.hyperlink-preview-popover`
208
+ - `.hyperlinkEditPopover` → `.hyperlink-edit-popover`
209
+ - `.buttonsWrapper` → `.buttons-wrapper`
210
+ - `.inputsWrapper` → `.inputs-wrapper`
211
+ - `.textWrapper` → `.text-wrapper`
212
+ - `.hrefWrapper` → `.href-wrapper`
213
+ - `.backButton` → `.back-button`
214
+ - `.btn_applyModal` → `.apply-button`
215
+ - **Unified popover contract**: `createHyperlink` callback now returns `HTMLElement | null` (was `void`). The extension handles positioning — consumers only build content.
216
+ - **Renamed meta key**: `preventAutoHyperlink` → `preventAutolink`.
217
+ - **Fixed type augmentation**: commands are now augmented under the correct `hyperlink:` key (was `link:`).
218
+
219
+ ### Added
220
+
221
+ - **Exported popover types**: `PreviewHyperlinkOptions` and `CreateHyperlinkOptions` — consumers no longer need to hand-roll option types.
222
+ - **`updateReference()`** on `FloatingToolbarInstance` — allows consumers to reposition the toolbar after async updates (e.g., metadata fetch).
223
+ - **`hide` middleware** from `@floating-ui/dom` — toolbar auto-hides when the reference element scrolls out of view.
224
+
225
+ ### Fixed
226
+
227
+ - **Floating-ui strategy**: changed from `position: absolute` to `strategy: 'fixed'` for body-appended elements, eliminating scroll-lag.
228
+ - **Stateful `/g` regex**: split `SPECIAL_SCHEME_REGEX` into global and non-global variants to prevent intermittent `test()` failures.
229
+ - **`editHyperlink` helper**: rewritten to use ProseMirror model (`getMarkRange`) instead of fragile DOM traversal (`domAtPos` → `closest('a')`).
230
+ - **Missing `.run()`**: edit popover now correctly executes the `editHyperlink` command chain.
231
+ - **Stale `view` capture**: click handler passes `view` directly from `handleDOMEvents` instead of capturing at plugin creation time.
232
+ - **Memory leaks**: proper cleanup of `autoUpdate` subscriptions and event listeners on `hide()` and `destroy()`.
233
+
234
+ ### Changed
235
+
236
+ - **`floating-toolbar.ts` rewritten**: replaced over-engineered singleton class (`FloatingToolbarManager`, ~800 LOC) with a lean functional module (~270 LOC).
237
+ - **`createHyperlinkPopover` simplified**: no longer manages its own floating toolbar — just returns DOM content.
238
+ - **Reference type**: `any` → `ReferenceElement | VirtualElement` for the floating toolbar reference element.
239
+ - Plugin factory functions renamed to camelCase: `autolinkPlugin`, `clickHandlerPlugin`, `pasteHandlerPlugin`.
240
+ - Plugin key strings standardized to camelCase: `hyperlinkAutolink`, `hyperlinkClickHandler`, `hyperlinkPasteHandler`.
241
+ - File renames: `copy2Clipboard.ts` → `copyToClipboard.ts`, `floating-toolbar.ts` → `floatingToolbar.ts`, `autoHyperlink.ts` → `autolink.ts`.
242
+
243
+ ### Removed
244
+
245
+ - Internal `Logger`, `HTMLSanitizer`, `CleanupTracker` classes (replaced by direct, simpler implementations).
246
+ - Redundant default export from `specialUrls.ts`.
247
+ - Empty `types.ts` file.
248
+
249
+ ### Migration Guide
250
+
251
+ ```diff
252
+ Hyperlink.configure({
253
+ - autoHyperlink: true,
254
+ - hyperlinkOnPaste: true,
255
+ + autolink: true,
256
+ + linkOnPaste: true,
257
+ popovers: {
258
+ previewHyperlink: myPreviewFn,
259
+ - createHyperlink: myCreateFn, // was (options) => void
260
+ + createHyperlink: myCreateFn, // now (options) => HTMLElement | null
261
+ }
262
+ })
263
+
264
+ // Commands
265
+ -editor.commands.editHyperLinkText('New Text')
266
+ -editor.commands.editHyperLinkHref('https://example.com')
267
+ +editor.commands.editHyperlinkText('New Text')
268
+ +editor.commands.editHyperlinkHref('https://example.com')
269
+
270
+ // CSS selectors
271
+ -.hyperlinkCreatePopover { ... }
272
+ -.buttonsWrapper { ... }
273
+ +.hyperlink-create-popover { ... }
274
+ +.buttons-wrapper { ... }
275
+
276
+ // Meta key in transactions
277
+ -tr.setMeta('preventAutoHyperlink', true)
278
+ +tr.setMeta('preventAutolink', true)
279
+
280
+ // Popover types (no more hand-rolled types)
281
+ +import type { PreviewHyperlinkOptions, CreateHyperlinkOptions } from '@docs.plus/extension-hyperlink'
282
+ ```
283
+
284
+ ---
285
+
286
+ ## [3.0.0] — 2024
287
+
288
+ Internal release. Migrated build to `tsup` (ESM + CJS + DTS). Added special URL scheme support (50+ protocols), markdown link input rules (`[text](url)`), and image syntax protection for auto-linking.
289
+
290
+ ## [2.0.0] — 2024
291
+
292
+ Internal release. Moved from standalone repository to `docs.plus` monorepo under `packages/extension-hyperlink`. Updated peer dependencies to use monorepo `catalog:` pins.
293
+
294
+ ---
295
+
296
+ ## [1.3.7] — 2023-07-26
297
+
298
+ Version bump only.
299
+
300
+ ## [1.3.6] — 2023-07-26
301
+
302
+ Version bump only.
303
+
304
+ ## [1.3.5] — 2023-07-26
305
+
306
+ Version bump only.
307
+
308
+ ## [1.3.4] — 2023-07-25
309
+
310
+ ### Bug Fixes
311
+
312
+ - Hide popover on outside click.
313
+ - Prevent popover from displaying in the wrong position.
314
+ - Fix typo.
315
+
316
+ ## [1.3.3] — 2023-07-23
317
+
318
+ Version bump only.
319
+
320
+ ## [1.3.2] — 2023-07-21
321
+
322
+ ### Bug Fixes
323
+
324
+ - Fix interfaces and types.
325
+ - Fix replacing hyperlink with new text.
326
+ - Fix selecting closest anchor element.
327
+ - Fix wrong selection of the anchor node.
328
+
329
+ ## [1.3.1] — 2023-07-20
330
+
331
+ ### Bug Fixes
332
+
333
+ - Fix interfaces and types.
334
+ - Fix replacing hyperlink with new text.
335
+ - Fix selecting closest anchor element.
336
+ - Fix wrong selection of the anchor node.
337
+
338
+ ## [1.3.0] — 2023-07-20
339
+
340
+ ### Bug Fixes
341
+
342
+ - Fix interfaces and types.
343
+ - Fix replacing hyperlink with new text.
344
+ - Fix selecting closest anchor element.
345
+
346
+ ## [1.2.1] — 2023-07-19
347
+
348
+ ### Bug Fixes
349
+
350
+ - Fix interfaces and types.
351
+
352
+ ## [1.2.0] — 2023-07-19
353
+
354
+ Version bump only.
355
+
356
+ ## [1.1.0] — 2023-07-18
357
+
358
+ Version bump only.
359
+
360
+ ## [1.0.4] — 2023-07-17
361
+
362
+ Version bump only.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2026 Hossein Marzban
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.