@tiptap/extension-link 2.9.1 → 2.10.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/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: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => ReturnType
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: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => ReturnType
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[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp']
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 = (typeof protocol === 'string' ? protocol : protocol.scheme)
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
- // eslint-disable-next-line no-useless-escape
126
- return !uri || uri.replace(ATTR_WHITESPACE, '').match(new RegExp(`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`, 'i'))
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
- tag: 'a[href]',
199
- getAttrs: dom => {
200
- const href = (dom as HTMLElement).getAttribute('href')
201
-
202
- // prevent XSS attacks
203
- if (!href || !isAllowedUri(href, this.options.protocols)) {
204
- return false
205
- }
206
- return null
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 (!isAllowedUri(HTMLAttributes.href, this.options.protocols)) {
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 ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
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 { validate } = this.options
254
- const links = find(text).filter(item => item.isLink && validate(item.value))
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 => (foundLinks.push({
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.validate,
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
  }