@tiptap/extension-link 2.9.1 → 2.10.1
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/helpers/autolink.d.ts +1 -0
- package/dist/helpers/autolink.d.ts.map +1 -1
- package/dist/index.cjs +64 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +64 -14
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +64 -14
- package/dist/index.umd.js.map +1 -1
- package/dist/link.d.ts +37 -0
- package/dist/link.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/helpers/autolink.ts +3 -0
- package/src/link.ts +146 -33
package/src/link.ts
CHANGED
|
@@ -38,46 +38,87 @@ export interface LinkOptions {
|
|
|
38
38
|
* @default true
|
|
39
39
|
* @example false
|
|
40
40
|
*/
|
|
41
|
-
autolink: boolean
|
|
41
|
+
autolink: boolean;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* An array of custom protocols to be registered with linkifyjs.
|
|
45
45
|
* @default []
|
|
46
46
|
* @example ['ftp', 'git']
|
|
47
47
|
*/
|
|
48
|
-
protocols: Array<LinkProtocolOptions | string
|
|
48
|
+
protocols: Array<LinkProtocolOptions | string>;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Default protocol to use when no protocol is specified.
|
|
52
52
|
* @default 'http'
|
|
53
53
|
*/
|
|
54
|
-
defaultProtocol: string
|
|
54
|
+
defaultProtocol: string;
|
|
55
55
|
/**
|
|
56
56
|
* If enabled, links will be opened on click.
|
|
57
57
|
* @default true
|
|
58
58
|
* @example false
|
|
59
59
|
*/
|
|
60
|
-
openOnClick: boolean | DeprecatedOpenWhenNotEditable
|
|
60
|
+
openOnClick: boolean | DeprecatedOpenWhenNotEditable;
|
|
61
61
|
/**
|
|
62
62
|
* Adds a link to the current selection if the pasted content only contains an url.
|
|
63
63
|
* @default true
|
|
64
64
|
* @example false
|
|
65
65
|
*/
|
|
66
|
-
linkOnPaste: boolean
|
|
66
|
+
linkOnPaste: boolean;
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* HTML attributes to add to the link element.
|
|
70
70
|
* @default {}
|
|
71
71
|
* @example { class: 'foo' }
|
|
72
72
|
*/
|
|
73
|
-
HTMLAttributes: Record<string, any
|
|
73
|
+
HTMLAttributes: Record<string, any>;
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
+
* @deprecated Use the `shouldAutoLink` option instead.
|
|
76
77
|
* A validation function that modifies link verification for the auto linker.
|
|
77
78
|
* @param url - The url to be validated.
|
|
78
79
|
* @returns - True if the url is valid, false otherwise.
|
|
79
80
|
*/
|
|
80
|
-
validate: (url: string) => boolean
|
|
81
|
+
validate: (url: string) => boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
|
85
|
+
* Only modify this if you know what you're doing.
|
|
86
|
+
*
|
|
87
|
+
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
|
91
|
+
* return url.startsWith('./') || defaultValidate(url)
|
|
92
|
+
* }
|
|
93
|
+
*/
|
|
94
|
+
isAllowedUri: (
|
|
95
|
+
/**
|
|
96
|
+
* The URL to be validated.
|
|
97
|
+
*/
|
|
98
|
+
url: string,
|
|
99
|
+
ctx: {
|
|
100
|
+
/**
|
|
101
|
+
* The default validation function.
|
|
102
|
+
*/
|
|
103
|
+
defaultValidate: (url: string) => boolean;
|
|
104
|
+
/**
|
|
105
|
+
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
|
106
|
+
*/
|
|
107
|
+
protocols: Array<LinkProtocolOptions | string>;
|
|
108
|
+
/**
|
|
109
|
+
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
|
110
|
+
*/
|
|
111
|
+
defaultProtocol: string;
|
|
112
|
+
}
|
|
113
|
+
) => boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Determines whether a valid link should be automatically linked in the content.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} url - The URL that has already been validated.
|
|
119
|
+
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
|
120
|
+
*/
|
|
121
|
+
shouldAutoLink: (url: string) => boolean;
|
|
81
122
|
}
|
|
82
123
|
|
|
83
124
|
declare module '@tiptap/core' {
|
|
@@ -88,19 +129,29 @@ declare module '@tiptap/core' {
|
|
|
88
129
|
* @param attributes The link attributes
|
|
89
130
|
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
|
90
131
|
*/
|
|
91
|
-
setLink: (attributes: {
|
|
132
|
+
setLink: (attributes: {
|
|
133
|
+
href: string;
|
|
134
|
+
target?: string | null;
|
|
135
|
+
rel?: string | null;
|
|
136
|
+
class?: string | null;
|
|
137
|
+
}) => ReturnType;
|
|
92
138
|
/**
|
|
93
139
|
* Toggle a link mark
|
|
94
140
|
* @param attributes The link attributes
|
|
95
141
|
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
|
96
142
|
*/
|
|
97
|
-
toggleLink: (attributes: {
|
|
143
|
+
toggleLink: (attributes: {
|
|
144
|
+
href: string;
|
|
145
|
+
target?: string | null;
|
|
146
|
+
rel?: string | null;
|
|
147
|
+
class?: string | null;
|
|
148
|
+
}) => ReturnType;
|
|
98
149
|
/**
|
|
99
150
|
* Unset a link mark
|
|
100
151
|
* @example editor.commands.unsetLink()
|
|
101
152
|
*/
|
|
102
|
-
unsetLink: () => ReturnType
|
|
103
|
-
}
|
|
153
|
+
unsetLink: () => ReturnType;
|
|
154
|
+
};
|
|
104
155
|
}
|
|
105
156
|
}
|
|
106
157
|
|
|
@@ -110,11 +161,22 @@ declare module '@tiptap/core' {
|
|
|
110
161
|
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
|
|
111
162
|
|
|
112
163
|
function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
|
|
113
|
-
const allowedProtocols: string[] = [
|
|
164
|
+
const allowedProtocols: string[] = [
|
|
165
|
+
'http',
|
|
166
|
+
'https',
|
|
167
|
+
'ftp',
|
|
168
|
+
'ftps',
|
|
169
|
+
'mailto',
|
|
170
|
+
'tel',
|
|
171
|
+
'callto',
|
|
172
|
+
'sms',
|
|
173
|
+
'cid',
|
|
174
|
+
'xmpp',
|
|
175
|
+
]
|
|
114
176
|
|
|
115
177
|
if (protocols) {
|
|
116
178
|
protocols.forEach(protocol => {
|
|
117
|
-
const nextProtocol =
|
|
179
|
+
const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
|
|
118
180
|
|
|
119
181
|
if (nextProtocol) {
|
|
120
182
|
allowedProtocols.push(nextProtocol)
|
|
@@ -122,8 +184,18 @@ function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocol
|
|
|
122
184
|
})
|
|
123
185
|
}
|
|
124
186
|
|
|
125
|
-
|
|
126
|
-
|
|
187
|
+
return (
|
|
188
|
+
!uri
|
|
189
|
+
|| uri
|
|
190
|
+
.replace(ATTR_WHITESPACE, '')
|
|
191
|
+
.match(
|
|
192
|
+
new RegExp(
|
|
193
|
+
// eslint-disable-next-line no-useless-escape
|
|
194
|
+
`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`,
|
|
195
|
+
'i',
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
127
199
|
}
|
|
128
200
|
|
|
129
201
|
/**
|
|
@@ -140,6 +212,13 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
140
212
|
exitable: true,
|
|
141
213
|
|
|
142
214
|
onCreate() {
|
|
215
|
+
if (this.options.validate && !this.options.shouldAutoLink) {
|
|
216
|
+
// Copy the validate function to the shouldAutoLink option
|
|
217
|
+
this.options.shouldAutoLink = this.options.validate
|
|
218
|
+
console.warn(
|
|
219
|
+
'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',
|
|
220
|
+
)
|
|
221
|
+
}
|
|
143
222
|
this.options.protocols.forEach(protocol => {
|
|
144
223
|
if (typeof protocol === 'string') {
|
|
145
224
|
registerCustomProtocol(protocol)
|
|
@@ -169,7 +248,9 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
169
248
|
rel: 'noopener noreferrer nofollow',
|
|
170
249
|
class: null,
|
|
171
250
|
},
|
|
251
|
+
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
|
172
252
|
validate: url => !!url,
|
|
253
|
+
shouldAutoLink: url => !!url,
|
|
173
254
|
}
|
|
174
255
|
},
|
|
175
256
|
|
|
@@ -194,25 +275,44 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
194
275
|
},
|
|
195
276
|
|
|
196
277
|
parseHTML() {
|
|
197
|
-
return [
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
tag: 'a[href]',
|
|
281
|
+
getAttrs: dom => {
|
|
282
|
+
const href = (dom as HTMLElement).getAttribute('href')
|
|
283
|
+
|
|
284
|
+
// prevent XSS attacks
|
|
285
|
+
if (
|
|
286
|
+
!href
|
|
287
|
+
|| !this.options.isAllowedUri(href, {
|
|
288
|
+
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
|
289
|
+
protocols: this.options.protocols,
|
|
290
|
+
defaultProtocol: this.options.defaultProtocol,
|
|
291
|
+
})
|
|
292
|
+
) {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
return null
|
|
296
|
+
},
|
|
207
297
|
},
|
|
208
|
-
|
|
298
|
+
]
|
|
209
299
|
},
|
|
210
300
|
|
|
211
301
|
renderHTML({ HTMLAttributes }) {
|
|
212
302
|
// prevent XSS attacks
|
|
213
|
-
if (
|
|
303
|
+
if (
|
|
304
|
+
!this.options.isAllowedUri(HTMLAttributes.href, {
|
|
305
|
+
defaultValidate: href => !!isAllowedUri(href, this.options.protocols),
|
|
306
|
+
protocols: this.options.protocols,
|
|
307
|
+
defaultProtocol: this.options.defaultProtocol,
|
|
308
|
+
})
|
|
309
|
+
) {
|
|
214
310
|
// strip out the href
|
|
215
|
-
return [
|
|
311
|
+
return [
|
|
312
|
+
'a',
|
|
313
|
+
mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }),
|
|
314
|
+
0,
|
|
315
|
+
]
|
|
216
316
|
}
|
|
217
317
|
|
|
218
318
|
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
|
@@ -250,17 +350,24 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
250
350
|
const foundLinks: PasteRuleMatch[] = []
|
|
251
351
|
|
|
252
352
|
if (text) {
|
|
253
|
-
const {
|
|
254
|
-
const links = find(text).filter(
|
|
353
|
+
const { protocols, defaultProtocol } = this.options
|
|
354
|
+
const links = find(text).filter(
|
|
355
|
+
item => item.isLink
|
|
356
|
+
&& this.options.isAllowedUri(item.value, {
|
|
357
|
+
defaultValidate: href => !!isAllowedUri(href, protocols),
|
|
358
|
+
protocols,
|
|
359
|
+
defaultProtocol,
|
|
360
|
+
}),
|
|
361
|
+
)
|
|
255
362
|
|
|
256
363
|
if (links.length) {
|
|
257
|
-
links.forEach(link =>
|
|
364
|
+
links.forEach(link => foundLinks.push({
|
|
258
365
|
text: link.value,
|
|
259
366
|
data: {
|
|
260
367
|
href: link.href,
|
|
261
368
|
},
|
|
262
369
|
index: link.start,
|
|
263
|
-
}))
|
|
370
|
+
}))
|
|
264
371
|
}
|
|
265
372
|
}
|
|
266
373
|
|
|
@@ -278,13 +385,19 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
278
385
|
|
|
279
386
|
addProseMirrorPlugins() {
|
|
280
387
|
const plugins: Plugin[] = []
|
|
388
|
+
const { protocols, defaultProtocol } = this.options
|
|
281
389
|
|
|
282
390
|
if (this.options.autolink) {
|
|
283
391
|
plugins.push(
|
|
284
392
|
autolink({
|
|
285
393
|
type: this.type,
|
|
286
394
|
defaultProtocol: this.options.defaultProtocol,
|
|
287
|
-
validate: this.options.
|
|
395
|
+
validate: url => this.options.isAllowedUri(url, {
|
|
396
|
+
defaultValidate: href => !!isAllowedUri(href, protocols),
|
|
397
|
+
protocols,
|
|
398
|
+
defaultProtocol,
|
|
399
|
+
}),
|
|
400
|
+
shouldAutoLink: this.options.shouldAutoLink,
|
|
288
401
|
}),
|
|
289
402
|
)
|
|
290
403
|
}
|