@tiptap/extension-link 3.28.0 → 3.29.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/index.cjs CHANGED
@@ -28,7 +28,7 @@ __export(index_exports, {
28
28
  module.exports = __toCommonJS(index_exports);
29
29
 
30
30
  // src/link.ts
31
- var import_core3 = require("@tiptap/core");
31
+ var import_core4 = require("@tiptap/core");
32
32
  var import_linkifyjs3 = require("linkifyjs");
33
33
 
34
34
  // src/helpers/autolink.ts
@@ -196,6 +196,134 @@ function clickHandler(options) {
196
196
  });
197
197
  }
198
198
 
199
+ // src/helpers/markdownLink.ts
200
+ var import_core3 = require("@tiptap/core");
201
+ var MARKDOWN_LINK_INPUT_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)$/;
202
+ var MARKDOWN_LINK_PASTE_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)/g;
203
+ function isEscaped(text, index) {
204
+ let backslashes = 0;
205
+ for (let position = index - 1; position >= 0 && text[position] === "\\"; position -= 1) {
206
+ backslashes += 1;
207
+ }
208
+ return backslashes % 2 === 1;
209
+ }
210
+ function isInsideCodeSpan(text, matchIndex) {
211
+ let openRunLength = 0;
212
+ let index = 0;
213
+ while (index < matchIndex) {
214
+ if (text[index] !== "`") {
215
+ index += 1;
216
+ continue;
217
+ }
218
+ if (openRunLength === 0 && isEscaped(text, index)) {
219
+ index += 1;
220
+ continue;
221
+ }
222
+ let runLength = 0;
223
+ while (index < matchIndex && text[index] === "`") {
224
+ runLength += 1;
225
+ index += 1;
226
+ }
227
+ if (openRunLength === 0) {
228
+ openRunLength = runLength;
229
+ } else if (runLength === openRunLength) {
230
+ openRunLength = 0;
231
+ }
232
+ }
233
+ return openRunLength > 0;
234
+ }
235
+ function isConvertibleLink(text, match, isAllowedHref) {
236
+ var _a, _b;
237
+ const [, linkText, href] = match;
238
+ const characterBefore = match.index ? text[match.index - 1] : void 0;
239
+ if (characterBefore === "!" || isEscaped(text, (_a = match.index) != null ? _a : 0)) {
240
+ return false;
241
+ }
242
+ if (isInsideCodeSpan(text, (_b = match.index) != null ? _b : 0)) {
243
+ return false;
244
+ }
245
+ return !!linkText.trim() && isAllowedHref(href);
246
+ }
247
+ function toRuleMatch(match) {
248
+ var _a, _b;
249
+ const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] = match;
250
+ const title = (_a = straightQuotedTitle != null ? straightQuotedTitle : curlyDoubleTitle) != null ? _a : curlySingleTitle;
251
+ return {
252
+ index: (_b = match.index) != null ? _b : 0,
253
+ text: linkSyntax,
254
+ replaceWith: linkText,
255
+ data: {
256
+ href,
257
+ // an empty title ("") counts as no title, as in CommonMark
258
+ title: title || null,
259
+ markdown: true
260
+ }
261
+ };
262
+ }
263
+ function matchesOverlap(a, b) {
264
+ return a.index < b.index + b.text.length && b.index < a.index + a.text.length;
265
+ }
266
+ function getMarkdownLinkAttributes(match) {
267
+ var _a, _b, _c;
268
+ return {
269
+ href: (_a = match.data) == null ? void 0 : _a.href,
270
+ title: (_c = (_b = match.data) == null ? void 0 : _b.title) != null ? _c : null
271
+ };
272
+ }
273
+ function markdownLinkInputRule(config) {
274
+ const rule = (0, import_core3.markInputRule)({
275
+ find: (text) => {
276
+ const match = MARKDOWN_LINK_INPUT_REGEX.exec(text);
277
+ if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) {
278
+ return null;
279
+ }
280
+ return toRuleMatch(match);
281
+ },
282
+ type: config.type,
283
+ getAttributes: getMarkdownLinkAttributes
284
+ });
285
+ return new import_core3.InputRule({
286
+ find: rule.find,
287
+ handler: (props) => {
288
+ const result = rule.handler(props);
289
+ if (result !== null && props.state.tr.steps.length) {
290
+ props.state.tr.setMeta("preventAutolink", true);
291
+ }
292
+ return result;
293
+ }
294
+ });
295
+ }
296
+ function markdownLinkPasteRule(config) {
297
+ const rule = (0, import_core3.markPasteRule)({
298
+ find: (text) => {
299
+ var _a, _b;
300
+ const markdownMatches = [];
301
+ for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) {
302
+ if (isConvertibleLink(text, match, config.isAllowedHref)) {
303
+ markdownMatches.push(toRuleMatch(match));
304
+ }
305
+ }
306
+ const plainUrlMatches = ((_b = (_a = config.findPlainUrls) == null ? void 0 : _a.call(config, text)) != null ? _b : []).filter(
307
+ (urlMatch) => !markdownMatches.some((markdownMatch) => matchesOverlap(markdownMatch, urlMatch))
308
+ );
309
+ return [...markdownMatches, ...plainUrlMatches];
310
+ },
311
+ type: config.type,
312
+ getAttributes: getMarkdownLinkAttributes
313
+ });
314
+ return new import_core3.PasteRule({
315
+ find: rule.find,
316
+ handler: (props) => {
317
+ var _a;
318
+ const result = rule.handler(props);
319
+ if (result !== null && props.state.tr.steps.length && ((_a = props.match.data) == null ? void 0 : _a.markdown)) {
320
+ props.state.tr.setMeta("preventAutolink", true);
321
+ }
322
+ return result;
323
+ }
324
+ });
325
+ }
326
+
199
327
  // src/helpers/pasteHandler.ts
200
328
  var import_state3 = require("@tiptap/pm/state");
201
329
  var import_linkifyjs2 = require("linkifyjs");
@@ -259,7 +387,7 @@ function isAllowedUri(uri, protocols) {
259
387
  )
260
388
  );
261
389
  }
262
- var Link = import_core3.Mark.create({
390
+ var Link = import_core4.Mark.create({
263
391
  name: "link",
264
392
  priority: 1e3,
265
393
  keepOnSplit: false,
@@ -290,6 +418,8 @@ var Link = import_core3.Mark.create({
290
418
  openOnClick: true,
291
419
  enableClickSelection: false,
292
420
  linkOnPaste: true,
421
+ markdownLinks: false,
422
+ // TODO (major) - default to true on next major version
293
423
  autolink: true,
294
424
  protocols: [],
295
425
  defaultProtocol: "http",
@@ -368,9 +498,9 @@ var Link = import_core3.Mark.create({
368
498
  protocols: this.options.protocols,
369
499
  defaultProtocol: this.options.defaultProtocol
370
500
  })) {
371
- return ["a", (0, import_core3.mergeAttributes)(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
501
+ return ["a", (0, import_core4.mergeAttributes)(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
372
502
  }
373
- return ["a", (0, import_core3.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes), 0];
503
+ return ["a", (0, import_core4.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes), 0];
374
504
  },
375
505
  markdownTokenName: "link",
376
506
  parseMarkdown: (token, helpers) => {
@@ -415,37 +545,64 @@ var Link = import_core3.Mark.create({
415
545
  }
416
546
  };
417
547
  },
418
- addPasteRules() {
548
+ addInputRules() {
549
+ if (!this.options.markdownLinks) {
550
+ return [];
551
+ }
419
552
  return [
420
- (0, import_core3.markPasteRule)({
421
- find: (text) => {
422
- const foundLinks = [];
423
- if (text) {
424
- const { protocols, defaultProtocol } = this.options;
425
- const links = (0, import_linkifyjs3.find)(text).filter(
426
- (item) => item.isLink && this.options.isAllowedUri(item.value, {
427
- defaultValidate: (href) => !!isAllowedUri(href, protocols),
428
- protocols,
429
- defaultProtocol
430
- })
431
- );
432
- if (links.length) {
433
- links.forEach((link) => {
434
- if (!this.options.shouldAutoLink(link.value)) {
435
- return;
436
- }
437
- foundLinks.push({
438
- text: link.value,
439
- data: {
440
- href: link.href
441
- },
442
- index: link.start
443
- });
444
- });
445
- }
553
+ markdownLinkInputRule({
554
+ type: this.type,
555
+ isAllowedHref: (href) => this.options.isAllowedUri(href, {
556
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
557
+ protocols: this.options.protocols,
558
+ defaultProtocol: this.options.defaultProtocol
559
+ })
560
+ })
561
+ ];
562
+ },
563
+ addPasteRules() {
564
+ const findPlainUrls = (text) => {
565
+ const foundLinks = [];
566
+ if (text) {
567
+ const { protocols, defaultProtocol } = this.options;
568
+ const links = (0, import_linkifyjs3.find)(text).filter(
569
+ (item) => item.isLink && this.options.isAllowedUri(item.value, {
570
+ defaultValidate: (href) => !!isAllowedUri(href, protocols),
571
+ protocols,
572
+ defaultProtocol
573
+ })
574
+ );
575
+ links.forEach((link) => {
576
+ if (!this.options.shouldAutoLink(link.value)) {
577
+ return;
446
578
  }
447
- return foundLinks;
448
- },
579
+ foundLinks.push({
580
+ text: link.value,
581
+ data: {
582
+ href: link.href
583
+ },
584
+ index: link.start
585
+ });
586
+ });
587
+ }
588
+ return foundLinks;
589
+ };
590
+ if (this.options.markdownLinks) {
591
+ return [
592
+ markdownLinkPasteRule({
593
+ type: this.type,
594
+ isAllowedHref: (href) => this.options.isAllowedUri(href, {
595
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
596
+ protocols: this.options.protocols,
597
+ defaultProtocol: this.options.defaultProtocol
598
+ }),
599
+ findPlainUrls
600
+ })
601
+ ];
602
+ }
603
+ return [
604
+ (0, import_core4.markPasteRule)({
605
+ find: findPlainUrls,
449
606
  type: this.type,
450
607
  getAttributes: (match) => {
451
608
  var _a;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/link.ts","../src/helpers/autolink.ts","../src/helpers/whitespace.ts","../src/helpers/clickHandler.ts","../src/helpers/pasteHandler.ts"],"sourcesContent":["import { Link } from './link.js'\n\nexport * from './link.js'\n\nexport default Link\n","import type { PasteRuleMatch } from '@tiptap/core'\nimport { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport type { Plugin } from '@tiptap/pm/state'\nimport { find, registerCustomProtocol, reset } from 'linkifyjs'\n\nimport { autolink } from './helpers/autolink.js'\nimport { clickHandler } from './helpers/clickHandler.js'\nimport { pasteHandler } from './helpers/pasteHandler.js'\nimport { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'\n\nexport interface LinkProtocolOptions {\n /**\n * The protocol scheme to be registered.\n * @default '''\n * @example 'ftp'\n * @example 'git'\n */\n scheme: string\n\n /**\n * If enabled, it allows optional slashes after the protocol.\n * @default false\n * @example true\n */\n optionalSlashes?: boolean\n}\n\nexport const pasteRegex =\n /https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z]{2,}\\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi\n\n/**\n * @deprecated The default behavior is now to open links when the editor is not editable.\n */\ntype DeprecatedOpenWhenNotEditable = 'whenNotEditable'\n\nexport interface LinkOptions {\n /**\n * If enabled, the extension will automatically add links as you type.\n * @default true\n * @example false\n */\n autolink: boolean\n\n /**\n * An array of custom protocols to be registered with linkifyjs.\n * @default []\n * @example ['ftp', 'git']\n */\n protocols: Array<LinkProtocolOptions | string>\n\n /**\n * Default protocol to use when no protocol is specified.\n * @default 'http'\n */\n defaultProtocol: string\n /**\n * If enabled, links will be opened on click.\n * @default true\n * @example false\n */\n openOnClick: boolean | DeprecatedOpenWhenNotEditable\n /**\n * If enabled, the link will be selected when clicked.\n * @default false\n * @example true\n */\n enableClickSelection: boolean\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n * @default true\n * @example false\n */\n linkOnPaste: boolean\n\n /**\n * HTML attributes to add to the link element.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * @deprecated Use the `shouldAutoLink` option instead.\n * A validation function that modifies link verification for the auto linker.\n * @param url - The url to be validated.\n * @returns - True if the url is valid, false otherwise.\n */\n validate: (url: string) => boolean\n\n /**\n * A validation function which is used for configuring link verification for preventing XSS attacks.\n * Only modify this if you know what you're doing.\n *\n * @returns {boolean} `true` if the URL is valid, `false` otherwise.\n *\n * @example\n * isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {\n * return url.startsWith('./') || defaultValidate(url)\n * }\n */\n isAllowedUri: (\n /**\n * The URL to be validated.\n */\n url: string,\n ctx: {\n /**\n * The default validation function.\n */\n defaultValidate: (url: string) => boolean\n /**\n * An array of allowed protocols for the URL (e.g., \"http\", \"https\"). As defined in the `protocols` option.\n */\n protocols: Array<LinkProtocolOptions | string>\n /**\n * A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.\n */\n defaultProtocol: string\n },\n ) => boolean\n\n /**\n * Determines whether a valid link should be automatically linked in the content.\n *\n * @param {string} url - The URL that has already been validated.\n * @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.\n */\n shouldAutoLink: (url: string) => boolean\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n * @param attributes The link attributes\n * @example editor.commands.setLink({ href: 'https://tiptap.dev' })\n */\n setLink: (attributes: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Toggle a link mark\n * @param attributes The link attributes\n * @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })\n */\n toggleLink: (attributes?: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType\n }\n }\n}\n\nexport function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {\n const allowedProtocols: string[] = [\n 'http',\n 'https',\n 'ftp',\n 'ftps',\n 'mailto',\n 'tel',\n 'callto',\n 'sms',\n 'cid',\n 'xmpp',\n ]\n\n if (protocols) {\n protocols.forEach(protocol => {\n const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme\n\n if (nextProtocol) {\n allowedProtocols.push(nextProtocol)\n }\n })\n }\n\n return (\n !uri ||\n uri\n .replace(UNICODE_WHITESPACE_REGEX_GLOBAL, '')\n .match(\n new RegExp(\n `^(?:(?:${allowedProtocols\n .map(protocol => protocol.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&'))\n .join('|')}):|[^a-z]|[a-z0-9+.\\\\-]+(?:[^a-z+.\\\\-:]|$))`,\n 'i',\n ),\n )\n )\n}\n\n/**\n * This extension allows you to create links.\n * @see https://www.tiptap.dev/api/marks/link\n */\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n exitable: true,\n\n onCreate() {\n // TODO: v4 - remove validate option\n if (this.options.validate && !this.options.shouldAutoLink) {\n // Copy the validate function to the shouldAutoLink option\n this.options.shouldAutoLink = this.options.validate\n console.warn(\n 'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',\n )\n }\n this.options.protocols.forEach(protocol => {\n if (typeof protocol === 'string') {\n registerCustomProtocol(protocol)\n return\n }\n registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)\n })\n },\n\n onDestroy() {\n reset()\n },\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n enableClickSelection: false,\n linkOnPaste: true,\n autolink: true,\n protocols: [],\n defaultProtocol: 'http',\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n class: null,\n },\n isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),\n validate: url => !!url,\n shouldAutoLink: url => {\n // URLs with explicit protocols (e.g., https://) should be auto-linked\n // But not if @ appears before :// (that would be userinfo like user:pass@host)\n const hasProtocol = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url)\n const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url)\n\n if (hasProtocol || (hasMaybeProtocol && !url.includes('@'))) {\n return true\n }\n // Strip userinfo (user:pass@) if present, then extract hostname\n const urlWithoutUserinfo = url.includes('@') ? url.split('@').pop()! : url\n const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]\n\n // Don't auto-link IP addresses without protocol\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(hostname)) {\n return false\n }\n // Don't auto-link single-word hostnames without TLD (e.g., \"localhost\")\n if (!/\\./.test(hostname)) {\n return false\n }\n return true\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n parseHTML(element) {\n return element.getAttribute('href')\n },\n },\n target: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.target ?? null,\n },\n rel: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.rel ?? null,\n },\n class: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.class ?? null,\n },\n title: {\n default: null,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: 'a[href]',\n getAttrs: dom => {\n const href = (dom as HTMLElement).getAttribute('href')\n\n // prevent XSS attacks\n if (\n !href ||\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n return null\n },\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n // prevent XSS attacks\n if (\n !this.options.isAllowedUri(HTMLAttributes.href, {\n defaultValidate: href => !!isAllowedUri(href, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n // strip out the href\n return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]\n }\n\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n markdownTokenName: 'link',\n\n parseMarkdown: (token, helpers) => {\n return helpers.applyMark('link', helpers.parseInline(token.tokens || []), {\n href: token.href,\n title: token.title || null,\n })\n },\n\n renderMarkdown: (node, h) => {\n const href = node.attrs?.href ?? ''\n const title = node.attrs?.title ?? ''\n const text = h.renderChildren(node)\n\n return title ? `[${text}](${href} \"${title}\")` : `[${text}](${href})`\n },\n\n addCommands() {\n return {\n setLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes\n\n if (\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes || {}\n\n if (\n href &&\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\n () =>\n ({ chain }) => {\n return chain()\n .unsetMark(this.name, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => {\n const foundLinks: PasteRuleMatch[] = []\n\n if (text) {\n const { protocols, defaultProtocol } = this.options\n const links = find(text).filter(\n item =>\n item.isLink &&\n this.options.isAllowedUri(item.value, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n )\n\n if (links.length) {\n links.forEach(link => {\n if (!this.options.shouldAutoLink(link.value)) {\n return\n }\n\n foundLinks.push({\n text: link.value,\n data: {\n href: link.href,\n },\n index: link.start,\n })\n })\n }\n }\n\n return foundLinks\n },\n type: this.type,\n getAttributes: match => {\n return {\n href: match.data?.href,\n }\n },\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins: Plugin[] = []\n const { protocols, defaultProtocol } = this.options\n\n if (this.options.autolink) {\n plugins.push(\n autolink({\n type: this.type,\n defaultProtocol: this.options.defaultProtocol,\n validate: url =>\n this.options.isAllowedUri(url, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n plugins.push(\n clickHandler({\n type: this.type,\n editor: this.editor,\n openOnClick:\n this.options.openOnClick === 'whenNotEditable' ? true : this.options.openOnClick,\n enableClickSelection: this.options.enableClickSelection,\n }),\n )\n\n if (this.options.linkOnPaste) {\n plugins.push(\n pasteHandler({\n editor: this.editor,\n defaultProtocol: this.options.defaultProtocol,\n type: this.type,\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n return plugins\n },\n})\n","import type { NodeWithPos } from '@tiptap/core'\nimport {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n} from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { MultiToken } from 'linkifyjs'\nimport { tokenize } from 'linkifyjs'\n\nimport { UNICODE_WHITESPACE_REGEX, UNICODE_WHITESPACE_REGEX_END } from './whitespace.js'\n\n/**\n * Check if the provided tokens form a valid link structure, which can either be a single link token\n * or a link token surrounded by parentheses or square brackets.\n *\n * This ensures that only complete and valid text is hyperlinked, preventing cases where a valid\n * top-level domain (TLD) is immediately followed by an invalid character, like a number. For\n * example, with the `find` method from Linkify, entering `example.com1` would result in\n * `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`\n * method, we can perform more comprehensive validation on the input text.\n */\nfunction isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>) {\n if (tokens.length === 1) {\n return tokens[0].isLink\n }\n\n if (tokens.length === 3 && tokens[1].isLink) {\n return ['()', '[]'].includes(tokens[0].value + tokens[2].value)\n }\n\n return false\n}\n\ntype AutolinkOptions = {\n type: MarkType\n defaultProtocol: string\n validate: (url: string) => boolean\n shouldAutoLink: (url: string) => boolean\n}\n\n/**\n * This plugin allows you to automatically add links to your editor.\n * @param options The plugin options\n * @returns The plugin instance\n */\nexport function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n /**\n * Does the transaction change the document?\n */\n const docChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n\n /**\n * Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.\n */\n const preventAutolink = transactions.some(transaction =>\n transaction.getMeta('preventAutolink'),\n )\n\n /**\n * Prevent autolink if the transaction is not a document change\n * or if the transaction has the meta `preventAutolink`.\n */\n if (!docChanges || preventAutolink) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, [...transactions])\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n // Now let’s see if we can add new links.\n const nodesInChangedRanges = findChildrenInRange(\n newState.doc,\n newRange,\n node => node.isTextblock,\n )\n\n let textBlock: NodeWithPos | undefined\n let textBeforeWhitespace: string | undefined\n\n if (nodesInChangedRanges.length > 1) {\n // Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n } else if (nodesInChangedRanges.length) {\n const endText = newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ')\n if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {\n return\n }\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n newRange.to,\n undefined,\n ' ',\n )\n }\n\n if (textBlock && textBeforeWhitespace) {\n const wordsBeforeWhitespace = textBeforeWhitespace\n .split(UNICODE_WHITESPACE_REGEX)\n .filter(Boolean)\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset =\n textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t =>\n t.toObject(options.defaultProtocol),\n )\n\n if (!isValidLinkStructure(linksBeforeSpace)) {\n return false\n }\n\n linksBeforeSpace\n .filter(link => link.isLink)\n // Calculate link position.\n .map(link => ({\n ...link,\n from: lastWordAndBlockOffset + link.start + 1,\n to: lastWordAndBlockOffset + link.end + 1,\n }))\n // ignore link inside code mark\n .filter(link => {\n if (!newState.schema.marks.code) {\n return true\n }\n\n return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code)\n })\n // validate link\n .filter(link => options.validate(link.value))\n // check whether should autolink\n .filter(link => options.shouldAutoLink(link.value))\n // Add link mark.\n .forEach(link => {\n if (\n getMarksBetween(link.from, link.to, newState.doc).some(\n item => item.mark.type === options.type,\n )\n ) {\n return\n }\n\n tr.addMark(\n link.from,\n link.to,\n options.type.create({\n href: link.href,\n }),\n )\n })\n }\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts\nexport const UNICODE_WHITESPACE_PATTERN =\n '[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]'\n\nexport const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN)\nexport const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`)\nexport const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, 'g')\n","import type { Editor } from '@tiptap/core'\nimport { getAttributes } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType\n editor: Editor\n openOnClick?: boolean\n enableClickSelection?: boolean\n}\n\nexport function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n if (event.button !== 0) {\n return false\n }\n\n if (!view.editable) {\n return false\n }\n\n let link: HTMLAnchorElement | null = null\n\n if (event.target instanceof HTMLAnchorElement) {\n link = event.target\n } else {\n const target = event.target as HTMLElement | null\n if (!target) {\n return false\n }\n\n const root = options.editor.view.dom\n\n // Tntentionally limit the lookup to the editor root.\n // Using tag names like DIV as boundaries breaks with custom NodeViews,\n link = target.closest<HTMLAnchorElement>('a')\n\n if (link && !root.contains(link)) {\n link = null\n }\n }\n\n if (!link) {\n return false\n }\n\n let handled = false\n\n if (options.enableClickSelection) {\n const commandResult = options.editor.commands.extendMarkRange(options.type.name)\n handled = commandResult\n }\n\n if (options.openOnClick) {\n const attrs = getAttributes(view.state, options.type.name)\n const href = link.href ?? attrs.href\n const target = link.target ?? attrs.target\n\n if (href) {\n window.open(href, target)\n handled = true\n }\n }\n\n return handled\n },\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\nimport type { LinkOptions } from '../link.js'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\n shouldAutoLink?: LinkOptions['shouldAutoLink']\n}\n\nexport function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, _event, slice) => {\n const { shouldAutoLink } = options\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(\n item => item.isLink && item.value === textContent,\n )\n\n if (\n !textContent ||\n !link ||\n (shouldAutoLink !== undefined && !shouldAutoLink(link.value))\n ) {\n return false\n }\n\n return options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n },\n },\n })\n}\n"],"mappings":"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","names":["import_core","import_linkifyjs","import_core","import_state","import_state","import_linkifyjs"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/link.ts","../src/helpers/autolink.ts","../src/helpers/whitespace.ts","../src/helpers/clickHandler.ts","../src/helpers/markdownLink.ts","../src/helpers/pasteHandler.ts"],"sourcesContent":["import { Link } from './link.js'\n\nexport * from './link.js'\n\nexport default Link\n","import type { PasteRuleMatch } from '@tiptap/core'\nimport { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport type { Plugin } from '@tiptap/pm/state'\nimport { find, registerCustomProtocol, reset } from 'linkifyjs'\n\nimport { autolink } from './helpers/autolink.js'\nimport { clickHandler } from './helpers/clickHandler.js'\nimport { markdownLinkInputRule, markdownLinkPasteRule } from './helpers/markdownLink.js'\nimport { pasteHandler } from './helpers/pasteHandler.js'\nimport { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'\n\nexport interface LinkProtocolOptions {\n /**\n * The protocol scheme to be registered.\n * @default '''\n * @example 'ftp'\n * @example 'git'\n */\n scheme: string\n\n /**\n * If enabled, it allows optional slashes after the protocol.\n * @default false\n * @example true\n */\n optionalSlashes?: boolean\n}\n\nexport const pasteRegex =\n /https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z]{2,}\\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi\n\n/**\n * @deprecated The default behavior is now to open links when the editor is not editable.\n */\ntype DeprecatedOpenWhenNotEditable = 'whenNotEditable'\n\nexport interface LinkOptions {\n /**\n * If enabled, the extension will automatically add links as you type.\n * @default true\n * @example false\n */\n autolink: boolean\n\n /**\n * An array of custom protocols to be registered with linkifyjs.\n * @default []\n * @example ['ftp', 'git']\n */\n protocols: Array<LinkProtocolOptions | string>\n\n /**\n * Default protocol to use when no protocol is specified.\n * @default 'http'\n */\n defaultProtocol: string\n /**\n * If enabled, links will be opened on click.\n * @default true\n * @example false\n */\n openOnClick: boolean | DeprecatedOpenWhenNotEditable\n /**\n * If enabled, the link will be selected when clicked.\n * @default false\n * @example true\n */\n enableClickSelection: boolean\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n * @default true\n * @example false\n */\n linkOnPaste: boolean\n\n /**\n * If enabled, typing or pasting the Markdown link syntax, e.g. `[Tiptap](https://tiptap.dev)`\n * or `[Tiptap](https://tiptap.dev \"Rich text editor\")`, converts it into a link.\n * @default false\n * @example true\n */\n markdownLinks: boolean\n\n /**\n * HTML attributes to add to the link element.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * @deprecated Use the `shouldAutoLink` option instead.\n * A validation function that modifies link verification for the auto linker.\n * @param url - The url to be validated.\n * @returns - True if the url is valid, false otherwise.\n */\n validate: (url: string) => boolean\n\n /**\n * A validation function which is used for configuring link verification for preventing XSS attacks.\n * Only modify this if you know what you're doing.\n *\n * @returns {boolean} `true` if the URL is valid, `false` otherwise.\n *\n * @example\n * isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {\n * return url.startsWith('./') || defaultValidate(url)\n * }\n */\n isAllowedUri: (\n /**\n * The URL to be validated.\n */\n url: string,\n ctx: {\n /**\n * The default validation function.\n */\n defaultValidate: (url: string) => boolean\n /**\n * An array of allowed protocols for the URL (e.g., \"http\", \"https\"). As defined in the `protocols` option.\n */\n protocols: Array<LinkProtocolOptions | string>\n /**\n * A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.\n */\n defaultProtocol: string\n },\n ) => boolean\n\n /**\n * Determines whether a valid link should be automatically linked in the content.\n *\n * @param {string} url - The URL that has already been validated.\n * @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.\n */\n shouldAutoLink: (url: string) => boolean\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n * @param attributes The link attributes\n * @example editor.commands.setLink({ href: 'https://tiptap.dev' })\n */\n setLink: (attributes: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Toggle a link mark\n * @param attributes The link attributes\n * @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })\n */\n toggleLink: (attributes?: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType\n }\n }\n}\n\nexport function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {\n const allowedProtocols: string[] = [\n 'http',\n 'https',\n 'ftp',\n 'ftps',\n 'mailto',\n 'tel',\n 'callto',\n 'sms',\n 'cid',\n 'xmpp',\n ]\n\n if (protocols) {\n protocols.forEach(protocol => {\n const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme\n\n if (nextProtocol) {\n allowedProtocols.push(nextProtocol)\n }\n })\n }\n\n return (\n !uri ||\n uri\n .replace(UNICODE_WHITESPACE_REGEX_GLOBAL, '')\n .match(\n new RegExp(\n `^(?:(?:${allowedProtocols\n .map(protocol => protocol.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&'))\n .join('|')}):|[^a-z]|[a-z0-9+.\\\\-]+(?:[^a-z+.\\\\-:]|$))`,\n 'i',\n ),\n )\n )\n}\n\n/**\n * This extension allows you to create links.\n * @see https://www.tiptap.dev/api/marks/link\n */\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n exitable: true,\n\n onCreate() {\n // TODO: v4 - remove validate option\n if (this.options.validate && !this.options.shouldAutoLink) {\n // Copy the validate function to the shouldAutoLink option\n this.options.shouldAutoLink = this.options.validate\n console.warn(\n 'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',\n )\n }\n this.options.protocols.forEach(protocol => {\n if (typeof protocol === 'string') {\n registerCustomProtocol(protocol)\n return\n }\n registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)\n })\n },\n\n onDestroy() {\n reset()\n },\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n enableClickSelection: false,\n linkOnPaste: true,\n markdownLinks: false, // TODO (major) - default to true on next major version\n autolink: true,\n protocols: [],\n defaultProtocol: 'http',\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n class: null,\n },\n isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),\n validate: url => !!url,\n shouldAutoLink: url => {\n // URLs with explicit protocols (e.g., https://) should be auto-linked\n // But not if @ appears before :// (that would be userinfo like user:pass@host)\n const hasProtocol = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url)\n const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url)\n\n if (hasProtocol || (hasMaybeProtocol && !url.includes('@'))) {\n return true\n }\n // Strip userinfo (user:pass@) if present, then extract hostname\n const urlWithoutUserinfo = url.includes('@') ? url.split('@').pop()! : url\n const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]\n\n // Don't auto-link IP addresses without protocol\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(hostname)) {\n return false\n }\n // Don't auto-link single-word hostnames without TLD (e.g., \"localhost\")\n if (!/\\./.test(hostname)) {\n return false\n }\n return true\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n parseHTML(element) {\n return element.getAttribute('href')\n },\n },\n target: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.target ?? null,\n },\n rel: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.rel ?? null,\n },\n class: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.class ?? null,\n },\n title: {\n default: null,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: 'a[href]',\n getAttrs: dom => {\n const href = (dom as HTMLElement).getAttribute('href')\n\n // prevent XSS attacks\n if (\n !href ||\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n return null\n },\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n // prevent XSS attacks\n if (\n !this.options.isAllowedUri(HTMLAttributes.href, {\n defaultValidate: href => !!isAllowedUri(href, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n // strip out the href\n return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]\n }\n\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n markdownTokenName: 'link',\n\n parseMarkdown: (token, helpers) => {\n return helpers.applyMark('link', helpers.parseInline(token.tokens || []), {\n href: token.href,\n title: token.title || null,\n })\n },\n\n renderMarkdown: (node, h) => {\n const href = node.attrs?.href ?? ''\n const title = node.attrs?.title ?? ''\n const text = h.renderChildren(node)\n\n return title ? `[${text}](${href} \"${title}\")` : `[${text}](${href})`\n },\n\n addCommands() {\n return {\n setLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes\n\n if (\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes || {}\n\n if (\n href &&\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\n () =>\n ({ chain }) => {\n return chain()\n .unsetMark(this.name, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n }\n },\n\n addInputRules() {\n if (!this.options.markdownLinks) {\n return []\n }\n\n return [\n markdownLinkInputRule({\n type: this.type,\n isAllowedHref: href =>\n this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n }),\n }),\n ]\n },\n\n addPasteRules() {\n const findPlainUrls = (text: string): PasteRuleMatch[] => {\n const foundLinks: PasteRuleMatch[] = []\n\n if (text) {\n const { protocols, defaultProtocol } = this.options\n const links = find(text).filter(\n item =>\n item.isLink &&\n this.options.isAllowedUri(item.value, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n )\n\n links.forEach(link => {\n if (!this.options.shouldAutoLink(link.value)) {\n return\n }\n\n foundLinks.push({\n text: link.value,\n data: {\n href: link.href,\n },\n index: link.start,\n })\n })\n }\n\n return foundLinks\n }\n\n if (this.options.markdownLinks) {\n return [\n markdownLinkPasteRule({\n type: this.type,\n isAllowedHref: href =>\n this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n }),\n findPlainUrls,\n }),\n ]\n }\n\n return [\n markPasteRule({\n find: findPlainUrls,\n type: this.type,\n getAttributes: match => {\n return {\n href: match.data?.href,\n }\n },\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins: Plugin[] = []\n const { protocols, defaultProtocol } = this.options\n\n if (this.options.autolink) {\n plugins.push(\n autolink({\n type: this.type,\n defaultProtocol: this.options.defaultProtocol,\n validate: url =>\n this.options.isAllowedUri(url, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n plugins.push(\n clickHandler({\n type: this.type,\n editor: this.editor,\n openOnClick:\n this.options.openOnClick === 'whenNotEditable' ? true : this.options.openOnClick,\n enableClickSelection: this.options.enableClickSelection,\n }),\n )\n\n if (this.options.linkOnPaste) {\n plugins.push(\n pasteHandler({\n editor: this.editor,\n defaultProtocol: this.options.defaultProtocol,\n type: this.type,\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n return plugins\n },\n})\n","import type { NodeWithPos } from '@tiptap/core'\nimport {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n} from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { MultiToken } from 'linkifyjs'\nimport { tokenize } from 'linkifyjs'\n\nimport { UNICODE_WHITESPACE_REGEX, UNICODE_WHITESPACE_REGEX_END } from './whitespace.js'\n\n/**\n * Check if the provided tokens form a valid link structure, which can either be a single link token\n * or a link token surrounded by parentheses or square brackets.\n *\n * This ensures that only complete and valid text is hyperlinked, preventing cases where a valid\n * top-level domain (TLD) is immediately followed by an invalid character, like a number. For\n * example, with the `find` method from Linkify, entering `example.com1` would result in\n * `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`\n * method, we can perform more comprehensive validation on the input text.\n */\nfunction isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>) {\n if (tokens.length === 1) {\n return tokens[0].isLink\n }\n\n if (tokens.length === 3 && tokens[1].isLink) {\n return ['()', '[]'].includes(tokens[0].value + tokens[2].value)\n }\n\n return false\n}\n\ntype AutolinkOptions = {\n type: MarkType\n defaultProtocol: string\n validate: (url: string) => boolean\n shouldAutoLink: (url: string) => boolean\n}\n\n/**\n * This plugin allows you to automatically add links to your editor.\n * @param options The plugin options\n * @returns The plugin instance\n */\nexport function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n /**\n * Does the transaction change the document?\n */\n const docChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n\n /**\n * Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.\n */\n const preventAutolink = transactions.some(transaction =>\n transaction.getMeta('preventAutolink'),\n )\n\n /**\n * Prevent autolink if the transaction is not a document change\n * or if the transaction has the meta `preventAutolink`.\n */\n if (!docChanges || preventAutolink) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, [...transactions])\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n // Now let’s see if we can add new links.\n const nodesInChangedRanges = findChildrenInRange(\n newState.doc,\n newRange,\n node => node.isTextblock,\n )\n\n let textBlock: NodeWithPos | undefined\n let textBeforeWhitespace: string | undefined\n\n if (nodesInChangedRanges.length > 1) {\n // Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n } else if (nodesInChangedRanges.length) {\n const endText = newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ')\n if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {\n return\n }\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n newRange.to,\n undefined,\n ' ',\n )\n }\n\n if (textBlock && textBeforeWhitespace) {\n const wordsBeforeWhitespace = textBeforeWhitespace\n .split(UNICODE_WHITESPACE_REGEX)\n .filter(Boolean)\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset =\n textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t =>\n t.toObject(options.defaultProtocol),\n )\n\n if (!isValidLinkStructure(linksBeforeSpace)) {\n return false\n }\n\n linksBeforeSpace\n .filter(link => link.isLink)\n // Calculate link position.\n .map(link => ({\n ...link,\n from: lastWordAndBlockOffset + link.start + 1,\n to: lastWordAndBlockOffset + link.end + 1,\n }))\n // ignore link inside code mark\n .filter(link => {\n if (!newState.schema.marks.code) {\n return true\n }\n\n return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code)\n })\n // validate link\n .filter(link => options.validate(link.value))\n // check whether should autolink\n .filter(link => options.shouldAutoLink(link.value))\n // Add link mark.\n .forEach(link => {\n if (\n getMarksBetween(link.from, link.to, newState.doc).some(\n item => item.mark.type === options.type,\n )\n ) {\n return\n }\n\n tr.addMark(\n link.from,\n link.to,\n options.type.create({\n href: link.href,\n }),\n )\n })\n }\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts\nexport const UNICODE_WHITESPACE_PATTERN =\n '[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]'\n\nexport const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN)\nexport const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`)\nexport const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, 'g')\n","import type { Editor } from '@tiptap/core'\nimport { getAttributes } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType\n editor: Editor\n openOnClick?: boolean\n enableClickSelection?: boolean\n}\n\nexport function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n if (event.button !== 0) {\n return false\n }\n\n if (!view.editable) {\n return false\n }\n\n let link: HTMLAnchorElement | null = null\n\n if (event.target instanceof HTMLAnchorElement) {\n link = event.target\n } else {\n const target = event.target as HTMLElement | null\n if (!target) {\n return false\n }\n\n const root = options.editor.view.dom\n\n // Tntentionally limit the lookup to the editor root.\n // Using tag names like DIV as boundaries breaks with custom NodeViews,\n link = target.closest<HTMLAnchorElement>('a')\n\n if (link && !root.contains(link)) {\n link = null\n }\n }\n\n if (!link) {\n return false\n }\n\n let handled = false\n\n if (options.enableClickSelection) {\n const commandResult = options.editor.commands.extendMarkRange(options.type.name)\n handled = commandResult\n }\n\n if (options.openOnClick) {\n const attrs = getAttributes(view.state, options.type.name)\n const href = link.href ?? attrs.href\n const target = link.target ?? attrs.target\n\n if (href) {\n window.open(href, target)\n handled = true\n }\n }\n\n return handled\n },\n },\n })\n}\n","import type { InputRuleMatch, PasteRuleMatch } from '@tiptap/core'\nimport { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\n\n/**\n * Matches a Markdown link with an optional quoted title.\n * for ex: [Tiptap](https://tiptap.dev) or [Tiptap](https://tiptap.dev \"some title\")\n * the URL may also contain one level of balanced parentheses, as in CommonMark\n * (titles accept curly quotes too, the Typography extension swaps them in while typing)\n * the title delimiters must come in matching pairs\n */\nconst MARKDOWN_LINK_INPUT_REGEX =\n /\\[([^[\\]]+)\\]\\(((?:[^\\s()]|\\([^\\s()]*\\))+)(?:\\s+(?:([\"'])(.*?)\\3|“(.*?)”|‘(.*?)’))?\\)$/\n\n/**\n * Same as the input regex but global, to find every Markdown link in pasted text.\n */\nconst MARKDOWN_LINK_PASTE_REGEX =\n /\\[([^[\\]]+)\\]\\(((?:[^\\s()]|\\([^\\s()]*\\))+)(?:\\s+(?:([\"'])(.*?)\\3|“(.*?)”|‘(.*?)’))?\\)/g\n\nexport interface MarkdownLinkRuleConfig {\n type: MarkType\n\n /**\n * Return `false` to leave the Markdown syntax untouched.\n */\n isAllowedHref: (href: string) => boolean\n}\n\nexport interface MarkdownLinkPasteRuleConfig extends MarkdownLinkRuleConfig {\n /**\n * Finds plain URLs to link in the same pass. Matches overlapping a\n * converted Markdown link are dropped so its href is kept.\n */\n findPlainUrls?: (text: string) => PasteRuleMatch[]\n}\n\nfunction isEscaped(text: string, index: number): boolean {\n let backslashes = 0\n\n for (let position = index - 1; position >= 0 && text[position] === '\\\\'; position -= 1) {\n backslashes += 1\n }\n\n return backslashes % 2 === 1\n}\n\n/**\n * Pairs the backtick runs before the match by length, as CommonMark does.\n * A run left open means the match sits in an unfinished code span.\n */\nfunction isInsideCodeSpan(text: string, matchIndex: number): boolean {\n let openRunLength = 0\n let index = 0\n\n while (index < matchIndex) {\n if (text[index] !== '`') {\n index += 1\n continue\n }\n\n // escapes only apply outside code spans\n if (openRunLength === 0 && isEscaped(text, index)) {\n index += 1\n continue\n }\n\n let runLength = 0\n\n while (index < matchIndex && text[index] === '`') {\n runLength += 1\n index += 1\n }\n\n if (openRunLength === 0) {\n openRunLength = runLength\n } else if (runLength === openRunLength) {\n openRunLength = 0\n }\n }\n\n return openRunLength > 0\n}\n\nfunction isConvertibleLink(\n text: string,\n match: RegExpMatchArray,\n isAllowedHref: MarkdownLinkRuleConfig['isAllowedHref'],\n): boolean {\n const [, linkText, href] = match\n const characterBefore = match.index ? text[match.index - 1] : undefined\n\n // `!` is the Markdown image syntax, `\\` may escape the opening bracket\n if (characterBefore === '!' || isEscaped(text, match.index ?? 0)) {\n return false\n }\n\n if (isInsideCodeSpan(text, match.index ?? 0)) {\n return false\n }\n\n return !!linkText.trim() && isAllowedHref(href)\n}\n\nfunction toRuleMatch(match: RegExpMatchArray): InputRuleMatch & PasteRuleMatch {\n const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] =\n match\n const title = straightQuotedTitle ?? curlyDoubleTitle ?? curlySingleTitle\n\n return {\n index: match.index ?? 0,\n text: linkSyntax,\n replaceWith: linkText,\n data: {\n href,\n // an empty title (\"\") counts as no title, as in CommonMark\n title: title || null,\n markdown: true,\n },\n }\n}\n\nfunction matchesOverlap(a: PasteRuleMatch, b: PasteRuleMatch): boolean {\n return a.index < b.index + b.text.length && b.index < a.index + a.text.length\n}\n\nfunction getMarkdownLinkAttributes(match: { data?: Record<string, any> }) {\n return {\n href: match.data?.href,\n title: match.data?.title ?? null,\n }\n}\n\n/**\n * Turns typed Markdown link syntax into a link mark as soon as the closing `)` comes in.\n * The transaction gets flagged so autolink doesn't touch the converted text again.\n */\nexport function markdownLinkInputRule(config: MarkdownLinkRuleConfig): InputRule {\n const rule = markInputRule({\n find: text => {\n const match = MARKDOWN_LINK_INPUT_REGEX.exec(text)\n\n if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) {\n return null\n }\n\n return toRuleMatch(match)\n },\n type: config.type,\n getAttributes: getMarkdownLinkAttributes,\n })\n\n return new InputRule({\n find: rule.find,\n handler: props => {\n const result = rule.handler(props)\n\n if (result !== null && props.state.tr.steps.length) {\n props.state.tr.setMeta('preventAutolink', true)\n }\n\n return result\n },\n })\n}\n\n/**\n * Same for pasting, converts every Markdown link found in the pasted text\n * and links the plain URLs from `findPlainUrls`.\n */\nexport function markdownLinkPasteRule(config: MarkdownLinkPasteRuleConfig): PasteRule {\n const rule = markPasteRule({\n find: text => {\n const markdownMatches: PasteRuleMatch[] = []\n\n for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) {\n if (isConvertibleLink(text, match, config.isAllowedHref)) {\n markdownMatches.push(toRuleMatch(match))\n }\n }\n\n const plainUrlMatches = (config.findPlainUrls?.(text) ?? []).filter(\n urlMatch => !markdownMatches.some(markdownMatch => matchesOverlap(markdownMatch, urlMatch)),\n )\n\n return [...markdownMatches, ...plainUrlMatches]\n },\n type: config.type,\n getAttributes: getMarkdownLinkAttributes,\n })\n\n return new PasteRule({\n find: rule.find,\n handler: props => {\n const result = rule.handler(props)\n\n // only Markdown conversions suppress autolink\n if (result !== null && props.state.tr.steps.length && props.match.data?.markdown) {\n props.state.tr.setMeta('preventAutolink', true)\n }\n\n return result\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\nimport type { LinkOptions } from '../link.js'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\n shouldAutoLink?: LinkOptions['shouldAutoLink']\n}\n\nexport function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, _event, slice) => {\n const { shouldAutoLink } = options\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(\n item => item.isLink && item.value === textContent,\n )\n\n if (\n !textContent ||\n !link ||\n (shouldAutoLink !== undefined && !shouldAutoLink(link.value))\n ) {\n return false\n }\n\n return options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n },\n },\n })\n}\n"],"mappings":"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","names":["import_core","import_linkifyjs","import_core","import_state","import_core","import_state","import_linkifyjs"]}
package/dist/index.d.cts CHANGED
@@ -56,6 +56,13 @@ interface LinkOptions {
56
56
  * @example false
57
57
  */
58
58
  linkOnPaste: boolean;
59
+ /**
60
+ * If enabled, typing or pasting the Markdown link syntax, e.g. `[Tiptap](https://tiptap.dev)`
61
+ * or `[Tiptap](https://tiptap.dev "Rich text editor")`, converts it into a link.
62
+ * @default false
63
+ * @example true
64
+ */
65
+ markdownLinks: boolean;
59
66
  /**
60
67
  * HTML attributes to add to the link element.
61
68
  * @default {}
package/dist/index.d.ts CHANGED
@@ -56,6 +56,13 @@ interface LinkOptions {
56
56
  * @example false
57
57
  */
58
58
  linkOnPaste: boolean;
59
+ /**
60
+ * If enabled, typing or pasting the Markdown link syntax, e.g. `[Tiptap](https://tiptap.dev)`
61
+ * or `[Tiptap](https://tiptap.dev "Rich text editor")`, converts it into a link.
62
+ * @default false
63
+ * @example true
64
+ */
65
+ markdownLinks: boolean;
59
66
  /**
60
67
  * HTML attributes to add to the link element.
61
68
  * @default {}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/link.ts
2
- import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
2
+ import { Mark, markPasteRule as markPasteRule2, mergeAttributes } from "@tiptap/core";
3
3
  import { find as find2, registerCustomProtocol, reset } from "linkifyjs";
4
4
 
5
5
  // src/helpers/autolink.ts
@@ -172,6 +172,134 @@ function clickHandler(options) {
172
172
  });
173
173
  }
174
174
 
175
+ // src/helpers/markdownLink.ts
176
+ import { InputRule, markInputRule, markPasteRule, PasteRule } from "@tiptap/core";
177
+ var MARKDOWN_LINK_INPUT_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)$/;
178
+ var MARKDOWN_LINK_PASTE_REGEX = /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)/g;
179
+ function isEscaped(text, index) {
180
+ let backslashes = 0;
181
+ for (let position = index - 1; position >= 0 && text[position] === "\\"; position -= 1) {
182
+ backslashes += 1;
183
+ }
184
+ return backslashes % 2 === 1;
185
+ }
186
+ function isInsideCodeSpan(text, matchIndex) {
187
+ let openRunLength = 0;
188
+ let index = 0;
189
+ while (index < matchIndex) {
190
+ if (text[index] !== "`") {
191
+ index += 1;
192
+ continue;
193
+ }
194
+ if (openRunLength === 0 && isEscaped(text, index)) {
195
+ index += 1;
196
+ continue;
197
+ }
198
+ let runLength = 0;
199
+ while (index < matchIndex && text[index] === "`") {
200
+ runLength += 1;
201
+ index += 1;
202
+ }
203
+ if (openRunLength === 0) {
204
+ openRunLength = runLength;
205
+ } else if (runLength === openRunLength) {
206
+ openRunLength = 0;
207
+ }
208
+ }
209
+ return openRunLength > 0;
210
+ }
211
+ function isConvertibleLink(text, match, isAllowedHref) {
212
+ var _a, _b;
213
+ const [, linkText, href] = match;
214
+ const characterBefore = match.index ? text[match.index - 1] : void 0;
215
+ if (characterBefore === "!" || isEscaped(text, (_a = match.index) != null ? _a : 0)) {
216
+ return false;
217
+ }
218
+ if (isInsideCodeSpan(text, (_b = match.index) != null ? _b : 0)) {
219
+ return false;
220
+ }
221
+ return !!linkText.trim() && isAllowedHref(href);
222
+ }
223
+ function toRuleMatch(match) {
224
+ var _a, _b;
225
+ const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] = match;
226
+ const title = (_a = straightQuotedTitle != null ? straightQuotedTitle : curlyDoubleTitle) != null ? _a : curlySingleTitle;
227
+ return {
228
+ index: (_b = match.index) != null ? _b : 0,
229
+ text: linkSyntax,
230
+ replaceWith: linkText,
231
+ data: {
232
+ href,
233
+ // an empty title ("") counts as no title, as in CommonMark
234
+ title: title || null,
235
+ markdown: true
236
+ }
237
+ };
238
+ }
239
+ function matchesOverlap(a, b) {
240
+ return a.index < b.index + b.text.length && b.index < a.index + a.text.length;
241
+ }
242
+ function getMarkdownLinkAttributes(match) {
243
+ var _a, _b, _c;
244
+ return {
245
+ href: (_a = match.data) == null ? void 0 : _a.href,
246
+ title: (_c = (_b = match.data) == null ? void 0 : _b.title) != null ? _c : null
247
+ };
248
+ }
249
+ function markdownLinkInputRule(config) {
250
+ const rule = markInputRule({
251
+ find: (text) => {
252
+ const match = MARKDOWN_LINK_INPUT_REGEX.exec(text);
253
+ if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) {
254
+ return null;
255
+ }
256
+ return toRuleMatch(match);
257
+ },
258
+ type: config.type,
259
+ getAttributes: getMarkdownLinkAttributes
260
+ });
261
+ return new InputRule({
262
+ find: rule.find,
263
+ handler: (props) => {
264
+ const result = rule.handler(props);
265
+ if (result !== null && props.state.tr.steps.length) {
266
+ props.state.tr.setMeta("preventAutolink", true);
267
+ }
268
+ return result;
269
+ }
270
+ });
271
+ }
272
+ function markdownLinkPasteRule(config) {
273
+ const rule = markPasteRule({
274
+ find: (text) => {
275
+ var _a, _b;
276
+ const markdownMatches = [];
277
+ for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) {
278
+ if (isConvertibleLink(text, match, config.isAllowedHref)) {
279
+ markdownMatches.push(toRuleMatch(match));
280
+ }
281
+ }
282
+ const plainUrlMatches = ((_b = (_a = config.findPlainUrls) == null ? void 0 : _a.call(config, text)) != null ? _b : []).filter(
283
+ (urlMatch) => !markdownMatches.some((markdownMatch) => matchesOverlap(markdownMatch, urlMatch))
284
+ );
285
+ return [...markdownMatches, ...plainUrlMatches];
286
+ },
287
+ type: config.type,
288
+ getAttributes: getMarkdownLinkAttributes
289
+ });
290
+ return new PasteRule({
291
+ find: rule.find,
292
+ handler: (props) => {
293
+ var _a;
294
+ const result = rule.handler(props);
295
+ if (result !== null && props.state.tr.steps.length && ((_a = props.match.data) == null ? void 0 : _a.markdown)) {
296
+ props.state.tr.setMeta("preventAutolink", true);
297
+ }
298
+ return result;
299
+ }
300
+ });
301
+ }
302
+
175
303
  // src/helpers/pasteHandler.ts
176
304
  import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@tiptap/pm/state";
177
305
  import { find } from "linkifyjs";
@@ -266,6 +394,8 @@ var Link = Mark.create({
266
394
  openOnClick: true,
267
395
  enableClickSelection: false,
268
396
  linkOnPaste: true,
397
+ markdownLinks: false,
398
+ // TODO (major) - default to true on next major version
269
399
  autolink: true,
270
400
  protocols: [],
271
401
  defaultProtocol: "http",
@@ -391,37 +521,64 @@ var Link = Mark.create({
391
521
  }
392
522
  };
393
523
  },
394
- addPasteRules() {
524
+ addInputRules() {
525
+ if (!this.options.markdownLinks) {
526
+ return [];
527
+ }
395
528
  return [
396
- markPasteRule({
397
- find: (text) => {
398
- const foundLinks = [];
399
- if (text) {
400
- const { protocols, defaultProtocol } = this.options;
401
- const links = find2(text).filter(
402
- (item) => item.isLink && this.options.isAllowedUri(item.value, {
403
- defaultValidate: (href) => !!isAllowedUri(href, protocols),
404
- protocols,
405
- defaultProtocol
406
- })
407
- );
408
- if (links.length) {
409
- links.forEach((link) => {
410
- if (!this.options.shouldAutoLink(link.value)) {
411
- return;
412
- }
413
- foundLinks.push({
414
- text: link.value,
415
- data: {
416
- href: link.href
417
- },
418
- index: link.start
419
- });
420
- });
421
- }
529
+ markdownLinkInputRule({
530
+ type: this.type,
531
+ isAllowedHref: (href) => this.options.isAllowedUri(href, {
532
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
533
+ protocols: this.options.protocols,
534
+ defaultProtocol: this.options.defaultProtocol
535
+ })
536
+ })
537
+ ];
538
+ },
539
+ addPasteRules() {
540
+ const findPlainUrls = (text) => {
541
+ const foundLinks = [];
542
+ if (text) {
543
+ const { protocols, defaultProtocol } = this.options;
544
+ const links = find2(text).filter(
545
+ (item) => item.isLink && this.options.isAllowedUri(item.value, {
546
+ defaultValidate: (href) => !!isAllowedUri(href, protocols),
547
+ protocols,
548
+ defaultProtocol
549
+ })
550
+ );
551
+ links.forEach((link) => {
552
+ if (!this.options.shouldAutoLink(link.value)) {
553
+ return;
422
554
  }
423
- return foundLinks;
424
- },
555
+ foundLinks.push({
556
+ text: link.value,
557
+ data: {
558
+ href: link.href
559
+ },
560
+ index: link.start
561
+ });
562
+ });
563
+ }
564
+ return foundLinks;
565
+ };
566
+ if (this.options.markdownLinks) {
567
+ return [
568
+ markdownLinkPasteRule({
569
+ type: this.type,
570
+ isAllowedHref: (href) => this.options.isAllowedUri(href, {
571
+ defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
572
+ protocols: this.options.protocols,
573
+ defaultProtocol: this.options.defaultProtocol
574
+ }),
575
+ findPlainUrls
576
+ })
577
+ ];
578
+ }
579
+ return [
580
+ markPasteRule2({
581
+ find: findPlainUrls,
425
582
  type: this.type,
426
583
  getAttributes: (match) => {
427
584
  var _a;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/link.ts","../src/helpers/autolink.ts","../src/helpers/whitespace.ts","../src/helpers/clickHandler.ts","../src/helpers/pasteHandler.ts","../src/index.ts"],"sourcesContent":["import type { PasteRuleMatch } from '@tiptap/core'\nimport { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport type { Plugin } from '@tiptap/pm/state'\nimport { find, registerCustomProtocol, reset } from 'linkifyjs'\n\nimport { autolink } from './helpers/autolink.js'\nimport { clickHandler } from './helpers/clickHandler.js'\nimport { pasteHandler } from './helpers/pasteHandler.js'\nimport { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'\n\nexport interface LinkProtocolOptions {\n /**\n * The protocol scheme to be registered.\n * @default '''\n * @example 'ftp'\n * @example 'git'\n */\n scheme: string\n\n /**\n * If enabled, it allows optional slashes after the protocol.\n * @default false\n * @example true\n */\n optionalSlashes?: boolean\n}\n\nexport const pasteRegex =\n /https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z]{2,}\\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi\n\n/**\n * @deprecated The default behavior is now to open links when the editor is not editable.\n */\ntype DeprecatedOpenWhenNotEditable = 'whenNotEditable'\n\nexport interface LinkOptions {\n /**\n * If enabled, the extension will automatically add links as you type.\n * @default true\n * @example false\n */\n autolink: boolean\n\n /**\n * An array of custom protocols to be registered with linkifyjs.\n * @default []\n * @example ['ftp', 'git']\n */\n protocols: Array<LinkProtocolOptions | string>\n\n /**\n * Default protocol to use when no protocol is specified.\n * @default 'http'\n */\n defaultProtocol: string\n /**\n * If enabled, links will be opened on click.\n * @default true\n * @example false\n */\n openOnClick: boolean | DeprecatedOpenWhenNotEditable\n /**\n * If enabled, the link will be selected when clicked.\n * @default false\n * @example true\n */\n enableClickSelection: boolean\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n * @default true\n * @example false\n */\n linkOnPaste: boolean\n\n /**\n * HTML attributes to add to the link element.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * @deprecated Use the `shouldAutoLink` option instead.\n * A validation function that modifies link verification for the auto linker.\n * @param url - The url to be validated.\n * @returns - True if the url is valid, false otherwise.\n */\n validate: (url: string) => boolean\n\n /**\n * A validation function which is used for configuring link verification for preventing XSS attacks.\n * Only modify this if you know what you're doing.\n *\n * @returns {boolean} `true` if the URL is valid, `false` otherwise.\n *\n * @example\n * isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {\n * return url.startsWith('./') || defaultValidate(url)\n * }\n */\n isAllowedUri: (\n /**\n * The URL to be validated.\n */\n url: string,\n ctx: {\n /**\n * The default validation function.\n */\n defaultValidate: (url: string) => boolean\n /**\n * An array of allowed protocols for the URL (e.g., \"http\", \"https\"). As defined in the `protocols` option.\n */\n protocols: Array<LinkProtocolOptions | string>\n /**\n * A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.\n */\n defaultProtocol: string\n },\n ) => boolean\n\n /**\n * Determines whether a valid link should be automatically linked in the content.\n *\n * @param {string} url - The URL that has already been validated.\n * @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.\n */\n shouldAutoLink: (url: string) => boolean\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n * @param attributes The link attributes\n * @example editor.commands.setLink({ href: 'https://tiptap.dev' })\n */\n setLink: (attributes: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Toggle a link mark\n * @param attributes The link attributes\n * @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })\n */\n toggleLink: (attributes?: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType\n }\n }\n}\n\nexport function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {\n const allowedProtocols: string[] = [\n 'http',\n 'https',\n 'ftp',\n 'ftps',\n 'mailto',\n 'tel',\n 'callto',\n 'sms',\n 'cid',\n 'xmpp',\n ]\n\n if (protocols) {\n protocols.forEach(protocol => {\n const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme\n\n if (nextProtocol) {\n allowedProtocols.push(nextProtocol)\n }\n })\n }\n\n return (\n !uri ||\n uri\n .replace(UNICODE_WHITESPACE_REGEX_GLOBAL, '')\n .match(\n new RegExp(\n `^(?:(?:${allowedProtocols\n .map(protocol => protocol.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&'))\n .join('|')}):|[^a-z]|[a-z0-9+.\\\\-]+(?:[^a-z+.\\\\-:]|$))`,\n 'i',\n ),\n )\n )\n}\n\n/**\n * This extension allows you to create links.\n * @see https://www.tiptap.dev/api/marks/link\n */\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n exitable: true,\n\n onCreate() {\n // TODO: v4 - remove validate option\n if (this.options.validate && !this.options.shouldAutoLink) {\n // Copy the validate function to the shouldAutoLink option\n this.options.shouldAutoLink = this.options.validate\n console.warn(\n 'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',\n )\n }\n this.options.protocols.forEach(protocol => {\n if (typeof protocol === 'string') {\n registerCustomProtocol(protocol)\n return\n }\n registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)\n })\n },\n\n onDestroy() {\n reset()\n },\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n enableClickSelection: false,\n linkOnPaste: true,\n autolink: true,\n protocols: [],\n defaultProtocol: 'http',\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n class: null,\n },\n isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),\n validate: url => !!url,\n shouldAutoLink: url => {\n // URLs with explicit protocols (e.g., https://) should be auto-linked\n // But not if @ appears before :// (that would be userinfo like user:pass@host)\n const hasProtocol = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url)\n const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url)\n\n if (hasProtocol || (hasMaybeProtocol && !url.includes('@'))) {\n return true\n }\n // Strip userinfo (user:pass@) if present, then extract hostname\n const urlWithoutUserinfo = url.includes('@') ? url.split('@').pop()! : url\n const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]\n\n // Don't auto-link IP addresses without protocol\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(hostname)) {\n return false\n }\n // Don't auto-link single-word hostnames without TLD (e.g., \"localhost\")\n if (!/\\./.test(hostname)) {\n return false\n }\n return true\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n parseHTML(element) {\n return element.getAttribute('href')\n },\n },\n target: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.target ?? null,\n },\n rel: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.rel ?? null,\n },\n class: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.class ?? null,\n },\n title: {\n default: null,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: 'a[href]',\n getAttrs: dom => {\n const href = (dom as HTMLElement).getAttribute('href')\n\n // prevent XSS attacks\n if (\n !href ||\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n return null\n },\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n // prevent XSS attacks\n if (\n !this.options.isAllowedUri(HTMLAttributes.href, {\n defaultValidate: href => !!isAllowedUri(href, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n // strip out the href\n return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]\n }\n\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n markdownTokenName: 'link',\n\n parseMarkdown: (token, helpers) => {\n return helpers.applyMark('link', helpers.parseInline(token.tokens || []), {\n href: token.href,\n title: token.title || null,\n })\n },\n\n renderMarkdown: (node, h) => {\n const href = node.attrs?.href ?? ''\n const title = node.attrs?.title ?? ''\n const text = h.renderChildren(node)\n\n return title ? `[${text}](${href} \"${title}\")` : `[${text}](${href})`\n },\n\n addCommands() {\n return {\n setLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes\n\n if (\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes || {}\n\n if (\n href &&\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\n () =>\n ({ chain }) => {\n return chain()\n .unsetMark(this.name, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => {\n const foundLinks: PasteRuleMatch[] = []\n\n if (text) {\n const { protocols, defaultProtocol } = this.options\n const links = find(text).filter(\n item =>\n item.isLink &&\n this.options.isAllowedUri(item.value, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n )\n\n if (links.length) {\n links.forEach(link => {\n if (!this.options.shouldAutoLink(link.value)) {\n return\n }\n\n foundLinks.push({\n text: link.value,\n data: {\n href: link.href,\n },\n index: link.start,\n })\n })\n }\n }\n\n return foundLinks\n },\n type: this.type,\n getAttributes: match => {\n return {\n href: match.data?.href,\n }\n },\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins: Plugin[] = []\n const { protocols, defaultProtocol } = this.options\n\n if (this.options.autolink) {\n plugins.push(\n autolink({\n type: this.type,\n defaultProtocol: this.options.defaultProtocol,\n validate: url =>\n this.options.isAllowedUri(url, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n plugins.push(\n clickHandler({\n type: this.type,\n editor: this.editor,\n openOnClick:\n this.options.openOnClick === 'whenNotEditable' ? true : this.options.openOnClick,\n enableClickSelection: this.options.enableClickSelection,\n }),\n )\n\n if (this.options.linkOnPaste) {\n plugins.push(\n pasteHandler({\n editor: this.editor,\n defaultProtocol: this.options.defaultProtocol,\n type: this.type,\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n return plugins\n },\n})\n","import type { NodeWithPos } from '@tiptap/core'\nimport {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n} from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { MultiToken } from 'linkifyjs'\nimport { tokenize } from 'linkifyjs'\n\nimport { UNICODE_WHITESPACE_REGEX, UNICODE_WHITESPACE_REGEX_END } from './whitespace.js'\n\n/**\n * Check if the provided tokens form a valid link structure, which can either be a single link token\n * or a link token surrounded by parentheses or square brackets.\n *\n * This ensures that only complete and valid text is hyperlinked, preventing cases where a valid\n * top-level domain (TLD) is immediately followed by an invalid character, like a number. For\n * example, with the `find` method from Linkify, entering `example.com1` would result in\n * `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`\n * method, we can perform more comprehensive validation on the input text.\n */\nfunction isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>) {\n if (tokens.length === 1) {\n return tokens[0].isLink\n }\n\n if (tokens.length === 3 && tokens[1].isLink) {\n return ['()', '[]'].includes(tokens[0].value + tokens[2].value)\n }\n\n return false\n}\n\ntype AutolinkOptions = {\n type: MarkType\n defaultProtocol: string\n validate: (url: string) => boolean\n shouldAutoLink: (url: string) => boolean\n}\n\n/**\n * This plugin allows you to automatically add links to your editor.\n * @param options The plugin options\n * @returns The plugin instance\n */\nexport function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n /**\n * Does the transaction change the document?\n */\n const docChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n\n /**\n * Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.\n */\n const preventAutolink = transactions.some(transaction =>\n transaction.getMeta('preventAutolink'),\n )\n\n /**\n * Prevent autolink if the transaction is not a document change\n * or if the transaction has the meta `preventAutolink`.\n */\n if (!docChanges || preventAutolink) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, [...transactions])\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n // Now let’s see if we can add new links.\n const nodesInChangedRanges = findChildrenInRange(\n newState.doc,\n newRange,\n node => node.isTextblock,\n )\n\n let textBlock: NodeWithPos | undefined\n let textBeforeWhitespace: string | undefined\n\n if (nodesInChangedRanges.length > 1) {\n // Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n } else if (nodesInChangedRanges.length) {\n const endText = newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ')\n if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {\n return\n }\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n newRange.to,\n undefined,\n ' ',\n )\n }\n\n if (textBlock && textBeforeWhitespace) {\n const wordsBeforeWhitespace = textBeforeWhitespace\n .split(UNICODE_WHITESPACE_REGEX)\n .filter(Boolean)\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset =\n textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t =>\n t.toObject(options.defaultProtocol),\n )\n\n if (!isValidLinkStructure(linksBeforeSpace)) {\n return false\n }\n\n linksBeforeSpace\n .filter(link => link.isLink)\n // Calculate link position.\n .map(link => ({\n ...link,\n from: lastWordAndBlockOffset + link.start + 1,\n to: lastWordAndBlockOffset + link.end + 1,\n }))\n // ignore link inside code mark\n .filter(link => {\n if (!newState.schema.marks.code) {\n return true\n }\n\n return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code)\n })\n // validate link\n .filter(link => options.validate(link.value))\n // check whether should autolink\n .filter(link => options.shouldAutoLink(link.value))\n // Add link mark.\n .forEach(link => {\n if (\n getMarksBetween(link.from, link.to, newState.doc).some(\n item => item.mark.type === options.type,\n )\n ) {\n return\n }\n\n tr.addMark(\n link.from,\n link.to,\n options.type.create({\n href: link.href,\n }),\n )\n })\n }\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts\nexport const UNICODE_WHITESPACE_PATTERN =\n '[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]'\n\nexport const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN)\nexport const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`)\nexport const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, 'g')\n","import type { Editor } from '@tiptap/core'\nimport { getAttributes } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType\n editor: Editor\n openOnClick?: boolean\n enableClickSelection?: boolean\n}\n\nexport function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n if (event.button !== 0) {\n return false\n }\n\n if (!view.editable) {\n return false\n }\n\n let link: HTMLAnchorElement | null = null\n\n if (event.target instanceof HTMLAnchorElement) {\n link = event.target\n } else {\n const target = event.target as HTMLElement | null\n if (!target) {\n return false\n }\n\n const root = options.editor.view.dom\n\n // Tntentionally limit the lookup to the editor root.\n // Using tag names like DIV as boundaries breaks with custom NodeViews,\n link = target.closest<HTMLAnchorElement>('a')\n\n if (link && !root.contains(link)) {\n link = null\n }\n }\n\n if (!link) {\n return false\n }\n\n let handled = false\n\n if (options.enableClickSelection) {\n const commandResult = options.editor.commands.extendMarkRange(options.type.name)\n handled = commandResult\n }\n\n if (options.openOnClick) {\n const attrs = getAttributes(view.state, options.type.name)\n const href = link.href ?? attrs.href\n const target = link.target ?? attrs.target\n\n if (href) {\n window.open(href, target)\n handled = true\n }\n }\n\n return handled\n },\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\nimport type { LinkOptions } from '../link.js'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\n shouldAutoLink?: LinkOptions['shouldAutoLink']\n}\n\nexport function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, _event, slice) => {\n const { shouldAutoLink } = options\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(\n item => item.isLink && item.value === textContent,\n )\n\n if (\n !textContent ||\n !link ||\n (shouldAutoLink !== undefined && !shouldAutoLink(link.value))\n ) {\n return false\n }\n\n return options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n },\n },\n })\n}\n","import { Link } from './link.js'\n\nexport * from './link.js'\n\nexport default Link\n"],"mappings":"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","names":["find","Plugin","PluginKey","Plugin","PluginKey","find"]}
1
+ {"version":3,"sources":["../src/link.ts","../src/helpers/autolink.ts","../src/helpers/whitespace.ts","../src/helpers/clickHandler.ts","../src/helpers/markdownLink.ts","../src/helpers/pasteHandler.ts","../src/index.ts"],"sourcesContent":["import type { PasteRuleMatch } from '@tiptap/core'\nimport { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport type { Plugin } from '@tiptap/pm/state'\nimport { find, registerCustomProtocol, reset } from 'linkifyjs'\n\nimport { autolink } from './helpers/autolink.js'\nimport { clickHandler } from './helpers/clickHandler.js'\nimport { markdownLinkInputRule, markdownLinkPasteRule } from './helpers/markdownLink.js'\nimport { pasteHandler } from './helpers/pasteHandler.js'\nimport { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'\n\nexport interface LinkProtocolOptions {\n /**\n * The protocol scheme to be registered.\n * @default '''\n * @example 'ftp'\n * @example 'git'\n */\n scheme: string\n\n /**\n * If enabled, it allows optional slashes after the protocol.\n * @default false\n * @example true\n */\n optionalSlashes?: boolean\n}\n\nexport const pasteRegex =\n /https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z]{2,}\\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi\n\n/**\n * @deprecated The default behavior is now to open links when the editor is not editable.\n */\ntype DeprecatedOpenWhenNotEditable = 'whenNotEditable'\n\nexport interface LinkOptions {\n /**\n * If enabled, the extension will automatically add links as you type.\n * @default true\n * @example false\n */\n autolink: boolean\n\n /**\n * An array of custom protocols to be registered with linkifyjs.\n * @default []\n * @example ['ftp', 'git']\n */\n protocols: Array<LinkProtocolOptions | string>\n\n /**\n * Default protocol to use when no protocol is specified.\n * @default 'http'\n */\n defaultProtocol: string\n /**\n * If enabled, links will be opened on click.\n * @default true\n * @example false\n */\n openOnClick: boolean | DeprecatedOpenWhenNotEditable\n /**\n * If enabled, the link will be selected when clicked.\n * @default false\n * @example true\n */\n enableClickSelection: boolean\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n * @default true\n * @example false\n */\n linkOnPaste: boolean\n\n /**\n * If enabled, typing or pasting the Markdown link syntax, e.g. `[Tiptap](https://tiptap.dev)`\n * or `[Tiptap](https://tiptap.dev \"Rich text editor\")`, converts it into a link.\n * @default false\n * @example true\n */\n markdownLinks: boolean\n\n /**\n * HTML attributes to add to the link element.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * @deprecated Use the `shouldAutoLink` option instead.\n * A validation function that modifies link verification for the auto linker.\n * @param url - The url to be validated.\n * @returns - True if the url is valid, false otherwise.\n */\n validate: (url: string) => boolean\n\n /**\n * A validation function which is used for configuring link verification for preventing XSS attacks.\n * Only modify this if you know what you're doing.\n *\n * @returns {boolean} `true` if the URL is valid, `false` otherwise.\n *\n * @example\n * isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {\n * return url.startsWith('./') || defaultValidate(url)\n * }\n */\n isAllowedUri: (\n /**\n * The URL to be validated.\n */\n url: string,\n ctx: {\n /**\n * The default validation function.\n */\n defaultValidate: (url: string) => boolean\n /**\n * An array of allowed protocols for the URL (e.g., \"http\", \"https\"). As defined in the `protocols` option.\n */\n protocols: Array<LinkProtocolOptions | string>\n /**\n * A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.\n */\n defaultProtocol: string\n },\n ) => boolean\n\n /**\n * Determines whether a valid link should be automatically linked in the content.\n *\n * @param {string} url - The URL that has already been validated.\n * @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.\n */\n shouldAutoLink: (url: string) => boolean\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n * @param attributes The link attributes\n * @example editor.commands.setLink({ href: 'https://tiptap.dev' })\n */\n setLink: (attributes: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Toggle a link mark\n * @param attributes The link attributes\n * @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })\n */\n toggleLink: (attributes?: {\n href: string\n target?: string | null\n rel?: string | null\n class?: string | null\n title?: string | null\n }) => ReturnType\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType\n }\n }\n}\n\nexport function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {\n const allowedProtocols: string[] = [\n 'http',\n 'https',\n 'ftp',\n 'ftps',\n 'mailto',\n 'tel',\n 'callto',\n 'sms',\n 'cid',\n 'xmpp',\n ]\n\n if (protocols) {\n protocols.forEach(protocol => {\n const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme\n\n if (nextProtocol) {\n allowedProtocols.push(nextProtocol)\n }\n })\n }\n\n return (\n !uri ||\n uri\n .replace(UNICODE_WHITESPACE_REGEX_GLOBAL, '')\n .match(\n new RegExp(\n `^(?:(?:${allowedProtocols\n .map(protocol => protocol.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&'))\n .join('|')}):|[^a-z]|[a-z0-9+.\\\\-]+(?:[^a-z+.\\\\-:]|$))`,\n 'i',\n ),\n )\n )\n}\n\n/**\n * This extension allows you to create links.\n * @see https://www.tiptap.dev/api/marks/link\n */\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n exitable: true,\n\n onCreate() {\n // TODO: v4 - remove validate option\n if (this.options.validate && !this.options.shouldAutoLink) {\n // Copy the validate function to the shouldAutoLink option\n this.options.shouldAutoLink = this.options.validate\n console.warn(\n 'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',\n )\n }\n this.options.protocols.forEach(protocol => {\n if (typeof protocol === 'string') {\n registerCustomProtocol(protocol)\n return\n }\n registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)\n })\n },\n\n onDestroy() {\n reset()\n },\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n enableClickSelection: false,\n linkOnPaste: true,\n markdownLinks: false, // TODO (major) - default to true on next major version\n autolink: true,\n protocols: [],\n defaultProtocol: 'http',\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n class: null,\n },\n isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),\n validate: url => !!url,\n shouldAutoLink: url => {\n // URLs with explicit protocols (e.g., https://) should be auto-linked\n // But not if @ appears before :// (that would be userinfo like user:pass@host)\n const hasProtocol = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url)\n const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url)\n\n if (hasProtocol || (hasMaybeProtocol && !url.includes('@'))) {\n return true\n }\n // Strip userinfo (user:pass@) if present, then extract hostname\n const urlWithoutUserinfo = url.includes('@') ? url.split('@').pop()! : url\n const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]\n\n // Don't auto-link IP addresses without protocol\n if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(hostname)) {\n return false\n }\n // Don't auto-link single-word hostnames without TLD (e.g., \"localhost\")\n if (!/\\./.test(hostname)) {\n return false\n }\n return true\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n parseHTML(element) {\n return element.getAttribute('href')\n },\n },\n target: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.target ?? null,\n },\n rel: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.rel ?? null,\n },\n class: {\n // Coerce `undefined` to `null` because `undefined` is an invalid attribute value\n default: this.options.HTMLAttributes.class ?? null,\n },\n title: {\n default: null,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: 'a[href]',\n getAttrs: dom => {\n const href = (dom as HTMLElement).getAttribute('href')\n\n // prevent XSS attacks\n if (\n !href ||\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n return null\n },\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n // prevent XSS attacks\n if (\n !this.options.isAllowedUri(HTMLAttributes.href, {\n defaultValidate: href => !!isAllowedUri(href, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n // strip out the href\n return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]\n }\n\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n markdownTokenName: 'link',\n\n parseMarkdown: (token, helpers) => {\n return helpers.applyMark('link', helpers.parseInline(token.tokens || []), {\n href: token.href,\n title: token.title || null,\n })\n },\n\n renderMarkdown: (node, h) => {\n const href = node.attrs?.href ?? ''\n const title = node.attrs?.title ?? ''\n const text = h.renderChildren(node)\n\n return title ? `[${text}](${href} \"${title}\")` : `[${text}](${href})`\n },\n\n addCommands() {\n return {\n setLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes\n\n if (\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes =>\n ({ chain }) => {\n const { href } = attributes || {}\n\n if (\n href &&\n !this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n })\n ) {\n return false\n }\n\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\n () =>\n ({ chain }) => {\n return chain()\n .unsetMark(this.name, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n }\n },\n\n addInputRules() {\n if (!this.options.markdownLinks) {\n return []\n }\n\n return [\n markdownLinkInputRule({\n type: this.type,\n isAllowedHref: href =>\n this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n }),\n }),\n ]\n },\n\n addPasteRules() {\n const findPlainUrls = (text: string): PasteRuleMatch[] => {\n const foundLinks: PasteRuleMatch[] = []\n\n if (text) {\n const { protocols, defaultProtocol } = this.options\n const links = find(text).filter(\n item =>\n item.isLink &&\n this.options.isAllowedUri(item.value, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n )\n\n links.forEach(link => {\n if (!this.options.shouldAutoLink(link.value)) {\n return\n }\n\n foundLinks.push({\n text: link.value,\n data: {\n href: link.href,\n },\n index: link.start,\n })\n })\n }\n\n return foundLinks\n }\n\n if (this.options.markdownLinks) {\n return [\n markdownLinkPasteRule({\n type: this.type,\n isAllowedHref: href =>\n this.options.isAllowedUri(href, {\n defaultValidate: url => !!isAllowedUri(url, this.options.protocols),\n protocols: this.options.protocols,\n defaultProtocol: this.options.defaultProtocol,\n }),\n findPlainUrls,\n }),\n ]\n }\n\n return [\n markPasteRule({\n find: findPlainUrls,\n type: this.type,\n getAttributes: match => {\n return {\n href: match.data?.href,\n }\n },\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins: Plugin[] = []\n const { protocols, defaultProtocol } = this.options\n\n if (this.options.autolink) {\n plugins.push(\n autolink({\n type: this.type,\n defaultProtocol: this.options.defaultProtocol,\n validate: url =>\n this.options.isAllowedUri(url, {\n defaultValidate: href => !!isAllowedUri(href, protocols),\n protocols,\n defaultProtocol,\n }),\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n plugins.push(\n clickHandler({\n type: this.type,\n editor: this.editor,\n openOnClick:\n this.options.openOnClick === 'whenNotEditable' ? true : this.options.openOnClick,\n enableClickSelection: this.options.enableClickSelection,\n }),\n )\n\n if (this.options.linkOnPaste) {\n plugins.push(\n pasteHandler({\n editor: this.editor,\n defaultProtocol: this.options.defaultProtocol,\n type: this.type,\n shouldAutoLink: this.options.shouldAutoLink,\n }),\n )\n }\n\n return plugins\n },\n})\n","import type { NodeWithPos } from '@tiptap/core'\nimport {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n} from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { MultiToken } from 'linkifyjs'\nimport { tokenize } from 'linkifyjs'\n\nimport { UNICODE_WHITESPACE_REGEX, UNICODE_WHITESPACE_REGEX_END } from './whitespace.js'\n\n/**\n * Check if the provided tokens form a valid link structure, which can either be a single link token\n * or a link token surrounded by parentheses or square brackets.\n *\n * This ensures that only complete and valid text is hyperlinked, preventing cases where a valid\n * top-level domain (TLD) is immediately followed by an invalid character, like a number. For\n * example, with the `find` method from Linkify, entering `example.com1` would result in\n * `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`\n * method, we can perform more comprehensive validation on the input text.\n */\nfunction isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>) {\n if (tokens.length === 1) {\n return tokens[0].isLink\n }\n\n if (tokens.length === 3 && tokens[1].isLink) {\n return ['()', '[]'].includes(tokens[0].value + tokens[2].value)\n }\n\n return false\n}\n\ntype AutolinkOptions = {\n type: MarkType\n defaultProtocol: string\n validate: (url: string) => boolean\n shouldAutoLink: (url: string) => boolean\n}\n\n/**\n * This plugin allows you to automatically add links to your editor.\n * @param options The plugin options\n * @returns The plugin instance\n */\nexport function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n /**\n * Does the transaction change the document?\n */\n const docChanges =\n transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)\n\n /**\n * Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.\n */\n const preventAutolink = transactions.some(transaction =>\n transaction.getMeta('preventAutolink'),\n )\n\n /**\n * Prevent autolink if the transaction is not a document change\n * or if the transaction has the meta `preventAutolink`.\n */\n if (!docChanges || preventAutolink) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, [...transactions])\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ newRange }) => {\n // Now let’s see if we can add new links.\n const nodesInChangedRanges = findChildrenInRange(\n newState.doc,\n newRange,\n node => node.isTextblock,\n )\n\n let textBlock: NodeWithPos | undefined\n let textBeforeWhitespace: string | undefined\n\n if (nodesInChangedRanges.length > 1) {\n // Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n } else if (nodesInChangedRanges.length) {\n const endText = newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ')\n if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {\n return\n }\n textBlock = nodesInChangedRanges[0]\n textBeforeWhitespace = newState.doc.textBetween(\n textBlock.pos,\n newRange.to,\n undefined,\n ' ',\n )\n }\n\n if (textBlock && textBeforeWhitespace) {\n const wordsBeforeWhitespace = textBeforeWhitespace\n .split(UNICODE_WHITESPACE_REGEX)\n .filter(Boolean)\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset =\n textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t =>\n t.toObject(options.defaultProtocol),\n )\n\n if (!isValidLinkStructure(linksBeforeSpace)) {\n return false\n }\n\n linksBeforeSpace\n .filter(link => link.isLink)\n // Calculate link position.\n .map(link => ({\n ...link,\n from: lastWordAndBlockOffset + link.start + 1,\n to: lastWordAndBlockOffset + link.end + 1,\n }))\n // ignore link inside code mark\n .filter(link => {\n if (!newState.schema.marks.code) {\n return true\n }\n\n return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code)\n })\n // validate link\n .filter(link => options.validate(link.value))\n // check whether should autolink\n .filter(link => options.shouldAutoLink(link.value))\n // Add link mark.\n .forEach(link => {\n if (\n getMarksBetween(link.from, link.to, newState.doc).some(\n item => item.mark.type === options.type,\n )\n ) {\n return\n }\n\n tr.addMark(\n link.from,\n link.to,\n options.type.create({\n href: link.href,\n }),\n )\n })\n }\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts\nexport const UNICODE_WHITESPACE_PATTERN =\n '[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]'\n\nexport const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN)\nexport const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`)\nexport const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, 'g')\n","import type { Editor } from '@tiptap/core'\nimport { getAttributes } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType\n editor: Editor\n openOnClick?: boolean\n enableClickSelection?: boolean\n}\n\nexport function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n if (event.button !== 0) {\n return false\n }\n\n if (!view.editable) {\n return false\n }\n\n let link: HTMLAnchorElement | null = null\n\n if (event.target instanceof HTMLAnchorElement) {\n link = event.target\n } else {\n const target = event.target as HTMLElement | null\n if (!target) {\n return false\n }\n\n const root = options.editor.view.dom\n\n // Tntentionally limit the lookup to the editor root.\n // Using tag names like DIV as boundaries breaks with custom NodeViews,\n link = target.closest<HTMLAnchorElement>('a')\n\n if (link && !root.contains(link)) {\n link = null\n }\n }\n\n if (!link) {\n return false\n }\n\n let handled = false\n\n if (options.enableClickSelection) {\n const commandResult = options.editor.commands.extendMarkRange(options.type.name)\n handled = commandResult\n }\n\n if (options.openOnClick) {\n const attrs = getAttributes(view.state, options.type.name)\n const href = link.href ?? attrs.href\n const target = link.target ?? attrs.target\n\n if (href) {\n window.open(href, target)\n handled = true\n }\n }\n\n return handled\n },\n },\n })\n}\n","import type { InputRuleMatch, PasteRuleMatch } from '@tiptap/core'\nimport { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\n\n/**\n * Matches a Markdown link with an optional quoted title.\n * for ex: [Tiptap](https://tiptap.dev) or [Tiptap](https://tiptap.dev \"some title\")\n * the URL may also contain one level of balanced parentheses, as in CommonMark\n * (titles accept curly quotes too, the Typography extension swaps them in while typing)\n * the title delimiters must come in matching pairs\n */\nconst MARKDOWN_LINK_INPUT_REGEX =\n /\\[([^[\\]]+)\\]\\(((?:[^\\s()]|\\([^\\s()]*\\))+)(?:\\s+(?:([\"'])(.*?)\\3|“(.*?)”|‘(.*?)’))?\\)$/\n\n/**\n * Same as the input regex but global, to find every Markdown link in pasted text.\n */\nconst MARKDOWN_LINK_PASTE_REGEX =\n /\\[([^[\\]]+)\\]\\(((?:[^\\s()]|\\([^\\s()]*\\))+)(?:\\s+(?:([\"'])(.*?)\\3|“(.*?)”|‘(.*?)’))?\\)/g\n\nexport interface MarkdownLinkRuleConfig {\n type: MarkType\n\n /**\n * Return `false` to leave the Markdown syntax untouched.\n */\n isAllowedHref: (href: string) => boolean\n}\n\nexport interface MarkdownLinkPasteRuleConfig extends MarkdownLinkRuleConfig {\n /**\n * Finds plain URLs to link in the same pass. Matches overlapping a\n * converted Markdown link are dropped so its href is kept.\n */\n findPlainUrls?: (text: string) => PasteRuleMatch[]\n}\n\nfunction isEscaped(text: string, index: number): boolean {\n let backslashes = 0\n\n for (let position = index - 1; position >= 0 && text[position] === '\\\\'; position -= 1) {\n backslashes += 1\n }\n\n return backslashes % 2 === 1\n}\n\n/**\n * Pairs the backtick runs before the match by length, as CommonMark does.\n * A run left open means the match sits in an unfinished code span.\n */\nfunction isInsideCodeSpan(text: string, matchIndex: number): boolean {\n let openRunLength = 0\n let index = 0\n\n while (index < matchIndex) {\n if (text[index] !== '`') {\n index += 1\n continue\n }\n\n // escapes only apply outside code spans\n if (openRunLength === 0 && isEscaped(text, index)) {\n index += 1\n continue\n }\n\n let runLength = 0\n\n while (index < matchIndex && text[index] === '`') {\n runLength += 1\n index += 1\n }\n\n if (openRunLength === 0) {\n openRunLength = runLength\n } else if (runLength === openRunLength) {\n openRunLength = 0\n }\n }\n\n return openRunLength > 0\n}\n\nfunction isConvertibleLink(\n text: string,\n match: RegExpMatchArray,\n isAllowedHref: MarkdownLinkRuleConfig['isAllowedHref'],\n): boolean {\n const [, linkText, href] = match\n const characterBefore = match.index ? text[match.index - 1] : undefined\n\n // `!` is the Markdown image syntax, `\\` may escape the opening bracket\n if (characterBefore === '!' || isEscaped(text, match.index ?? 0)) {\n return false\n }\n\n if (isInsideCodeSpan(text, match.index ?? 0)) {\n return false\n }\n\n return !!linkText.trim() && isAllowedHref(href)\n}\n\nfunction toRuleMatch(match: RegExpMatchArray): InputRuleMatch & PasteRuleMatch {\n const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] =\n match\n const title = straightQuotedTitle ?? curlyDoubleTitle ?? curlySingleTitle\n\n return {\n index: match.index ?? 0,\n text: linkSyntax,\n replaceWith: linkText,\n data: {\n href,\n // an empty title (\"\") counts as no title, as in CommonMark\n title: title || null,\n markdown: true,\n },\n }\n}\n\nfunction matchesOverlap(a: PasteRuleMatch, b: PasteRuleMatch): boolean {\n return a.index < b.index + b.text.length && b.index < a.index + a.text.length\n}\n\nfunction getMarkdownLinkAttributes(match: { data?: Record<string, any> }) {\n return {\n href: match.data?.href,\n title: match.data?.title ?? null,\n }\n}\n\n/**\n * Turns typed Markdown link syntax into a link mark as soon as the closing `)` comes in.\n * The transaction gets flagged so autolink doesn't touch the converted text again.\n */\nexport function markdownLinkInputRule(config: MarkdownLinkRuleConfig): InputRule {\n const rule = markInputRule({\n find: text => {\n const match = MARKDOWN_LINK_INPUT_REGEX.exec(text)\n\n if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) {\n return null\n }\n\n return toRuleMatch(match)\n },\n type: config.type,\n getAttributes: getMarkdownLinkAttributes,\n })\n\n return new InputRule({\n find: rule.find,\n handler: props => {\n const result = rule.handler(props)\n\n if (result !== null && props.state.tr.steps.length) {\n props.state.tr.setMeta('preventAutolink', true)\n }\n\n return result\n },\n })\n}\n\n/**\n * Same for pasting, converts every Markdown link found in the pasted text\n * and links the plain URLs from `findPlainUrls`.\n */\nexport function markdownLinkPasteRule(config: MarkdownLinkPasteRuleConfig): PasteRule {\n const rule = markPasteRule({\n find: text => {\n const markdownMatches: PasteRuleMatch[] = []\n\n for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) {\n if (isConvertibleLink(text, match, config.isAllowedHref)) {\n markdownMatches.push(toRuleMatch(match))\n }\n }\n\n const plainUrlMatches = (config.findPlainUrls?.(text) ?? []).filter(\n urlMatch => !markdownMatches.some(markdownMatch => matchesOverlap(markdownMatch, urlMatch)),\n )\n\n return [...markdownMatches, ...plainUrlMatches]\n },\n type: config.type,\n getAttributes: getMarkdownLinkAttributes,\n })\n\n return new PasteRule({\n find: rule.find,\n handler: props => {\n const result = rule.handler(props)\n\n // only Markdown conversions suppress autolink\n if (result !== null && props.state.tr.steps.length && props.match.data?.markdown) {\n props.state.tr.setMeta('preventAutolink', true)\n }\n\n return result\n },\n })\n}\n","import type { Editor } from '@tiptap/core'\nimport type { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\nimport type { LinkOptions } from '../link.js'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\n shouldAutoLink?: LinkOptions['shouldAutoLink']\n}\n\nexport function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, _event, slice) => {\n const { shouldAutoLink } = options\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(\n item => item.isLink && item.value === textContent,\n )\n\n if (\n !textContent ||\n !link ||\n (shouldAutoLink !== undefined && !shouldAutoLink(link.value))\n ) {\n return false\n }\n\n return options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n },\n },\n })\n}\n","import { Link } from './link.js'\n\nexport * from './link.js'\n\nexport default Link\n"],"mappings":";AACA,SAAS,MAAM,iBAAAA,gBAAe,uBAAuB;AAErD,SAAS,QAAAC,OAAM,wBAAwB,aAAa;;;ACFpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,QAAQ,iBAAiB;AAElC,SAAS,gBAAgB;;;ACRlB,IAAM,6BACX;AAEK,IAAM,2BAA2B,IAAI,OAAO,0BAA0B;AACtE,IAAM,+BAA+B,IAAI,OAAO,GAAG,0BAA0B,GAAG;AAChF,IAAM,kCAAkC,IAAI,OAAO,4BAA4B,GAAG;;;ADiBzF,SAAS,qBAAqB,QAAmD;AAC/E,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO,OAAO,CAAC,EAAE;AAAA,EACnB;AAEA,MAAI,OAAO,WAAW,KAAK,OAAO,CAAC,EAAE,QAAQ;AAC3C,WAAO,CAAC,MAAM,IAAI,EAAE,SAAS,OAAO,CAAC,EAAE,QAAQ,OAAO,CAAC,EAAE,KAAK;AAAA,EAChE;AAEA,SAAO;AACT;AAcO,SAAS,SAAS,SAAkC;AACzD,SAAO,IAAI,OAAO;AAAA,IAChB,KAAK,IAAI,UAAU,UAAU;AAAA,IAC7B,mBAAmB,CAAC,cAAc,UAAU,aAAa;AAIvD,YAAM,aACJ,aAAa,KAAK,iBAAe,YAAY,UAAU,KAAK,CAAC,SAAS,IAAI,GAAG,SAAS,GAAG;AAK3F,YAAM,kBAAkB,aAAa;AAAA,QAAK,iBACxC,YAAY,QAAQ,iBAAiB;AAAA,MACvC;AAMA,UAAI,CAAC,cAAc,iBAAiB;AAClC;AAAA,MACF;AAEA,YAAM,EAAE,GAAG,IAAI;AACf,YAAM,YAAY,wBAAwB,SAAS,KAAK,CAAC,GAAG,YAAY,CAAC;AACzE,YAAM,UAAU,iBAAiB,SAAS;AAE1C,cAAQ,QAAQ,CAAC,EAAE,SAAS,MAAM;AAEhC,cAAM,uBAAuB;AAAA,UAC3B,SAAS;AAAA,UACT;AAAA,UACA,UAAQ,KAAK;AAAA,QACf;AAEA,YAAI;AACJ,YAAI;AAEJ,YAAI,qBAAqB,SAAS,GAAG;AAEnC,sBAAY,qBAAqB,CAAC;AAClC,iCAAuB,SAAS,IAAI;AAAA,YAClC,UAAU;AAAA,YACV,UAAU,MAAM,UAAU,KAAK;AAAA,YAC/B;AAAA,YACA;AAAA,UACF;AAAA,QACF,WAAW,qBAAqB,QAAQ;AACtC,gBAAM,UAAU,SAAS,IAAI,YAAY,SAAS,MAAM,SAAS,IAAI,KAAK,GAAG;AAC7E,cAAI,CAAC,6BAA6B,KAAK,OAAO,GAAG;AAC/C;AAAA,UACF;AACA,sBAAY,qBAAqB,CAAC;AAClC,iCAAuB,SAAS,IAAI;AAAA,YAClC,UAAU;AAAA,YACV,SAAS;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAEA,YAAI,aAAa,sBAAsB;AACrC,gBAAM,wBAAwB,qBAC3B,MAAM,wBAAwB,EAC9B,OAAO,OAAO;AAEjB,cAAI,sBAAsB,UAAU,GAAG;AACrC,mBAAO;AAAA,UACT;AAEA,gBAAM,sBAAsB,sBAAsB,sBAAsB,SAAS,CAAC;AAClF,gBAAM,yBACJ,UAAU,MAAM,qBAAqB,YAAY,mBAAmB;AAEtE,cAAI,CAAC,qBAAqB;AACxB,mBAAO;AAAA,UACT;AAEA,gBAAM,mBAAmB,SAAS,mBAAmB,EAAE;AAAA,YAAI,OACzD,EAAE,SAAS,QAAQ,eAAe;AAAA,UACpC;AAEA,cAAI,CAAC,qBAAqB,gBAAgB,GAAG;AAC3C,mBAAO;AAAA,UACT;AAEA,2BACG,OAAO,UAAQ,KAAK,MAAM,EAE1B,IAAI,WAAS;AAAA,YACZ,GAAG;AAAA,YACH,MAAM,yBAAyB,KAAK,QAAQ;AAAA,YAC5C,IAAI,yBAAyB,KAAK,MAAM;AAAA,UAC1C,EAAE,EAED,OAAO,UAAQ;AACd,gBAAI,CAAC,SAAS,OAAO,MAAM,MAAM;AAC/B,qBAAO;AAAA,YACT;AAEA,mBAAO,CAAC,SAAS,IAAI,aAAa,KAAK,MAAM,KAAK,IAAI,SAAS,OAAO,MAAM,IAAI;AAAA,UAClF,CAAC,EAEA,OAAO,UAAQ,QAAQ,SAAS,KAAK,KAAK,CAAC,EAE3C,OAAO,UAAQ,QAAQ,eAAe,KAAK,KAAK,CAAC,EAEjD,QAAQ,UAAQ;AACf,gBACE,gBAAgB,KAAK,MAAM,KAAK,IAAI,SAAS,GAAG,EAAE;AAAA,cAChD,UAAQ,KAAK,KAAK,SAAS,QAAQ;AAAA,YACrC,GACA;AACA;AAAA,YACF;AAEA,eAAG;AAAA,cACD,KAAK;AAAA,cACL,KAAK;AAAA,cACL,QAAQ,KAAK,OAAO;AAAA,gBAClB,MAAM,KAAK;AAAA,cACb,CAAC;AAAA,YACH;AAAA,UACF,CAAC;AAAA,QACL;AAAA,MACF,CAAC;AAED,UAAI,CAAC,GAAG,MAAM,QAAQ;AACpB;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;;;AEvLA,SAAS,qBAAqB;AAE9B,SAAS,UAAAC,SAAQ,aAAAC,kBAAiB;AAS3B,SAAS,aAAa,SAAsC;AACjE,SAAO,IAAID,QAAO;AAAA,IAChB,KAAK,IAAIC,WAAU,iBAAiB;AAAA,IACpC,OAAO;AAAA,MACL,aAAa,CAAC,MAAM,KAAK,UAAU;AAhBzC;AAiBQ,YAAI,MAAM,WAAW,GAAG;AACtB,iBAAO;AAAA,QACT;AAEA,YAAI,CAAC,KAAK,UAAU;AAClB,iBAAO;AAAA,QACT;AAEA,YAAI,OAAiC;AAErC,YAAI,MAAM,kBAAkB,mBAAmB;AAC7C,iBAAO,MAAM;AAAA,QACf,OAAO;AACL,gBAAM,SAAS,MAAM;AACrB,cAAI,CAAC,QAAQ;AACX,mBAAO;AAAA,UACT;AAEA,gBAAM,OAAO,QAAQ,OAAO,KAAK;AAIjC,iBAAO,OAAO,QAA2B,GAAG;AAE5C,cAAI,QAAQ,CAAC,KAAK,SAAS,IAAI,GAAG;AAChC,mBAAO;AAAA,UACT;AAAA,QACF;AAEA,YAAI,CAAC,MAAM;AACT,iBAAO;AAAA,QACT;AAEA,YAAI,UAAU;AAEd,YAAI,QAAQ,sBAAsB;AAChC,gBAAM,gBAAgB,QAAQ,OAAO,SAAS,gBAAgB,QAAQ,KAAK,IAAI;AAC/E,oBAAU;AAAA,QACZ;AAEA,YAAI,QAAQ,aAAa;AACvB,gBAAM,QAAQ,cAAc,KAAK,OAAO,QAAQ,KAAK,IAAI;AACzD,gBAAM,QAAO,UAAK,SAAL,YAAa,MAAM;AAChC,gBAAM,UAAS,UAAK,WAAL,YAAe,MAAM;AAEpC,cAAI,MAAM;AACR,mBAAO,KAAK,MAAM,MAAM;AACxB,sBAAU;AAAA,UACZ;AAAA,QACF;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ACvEA,SAAS,WAAW,eAAe,eAAe,iBAAiB;AAUnE,IAAM,4BACJ;AAKF,IAAM,4BACJ;AAmBF,SAAS,UAAU,MAAc,OAAwB;AACvD,MAAI,cAAc;AAElB,WAAS,WAAW,QAAQ,GAAG,YAAY,KAAK,KAAK,QAAQ,MAAM,MAAM,YAAY,GAAG;AACtF,mBAAe;AAAA,EACjB;AAEA,SAAO,cAAc,MAAM;AAC7B;AAMA,SAAS,iBAAiB,MAAc,YAA6B;AACnE,MAAI,gBAAgB;AACpB,MAAI,QAAQ;AAEZ,SAAO,QAAQ,YAAY;AACzB,QAAI,KAAK,KAAK,MAAM,KAAK;AACvB,eAAS;AACT;AAAA,IACF;AAGA,QAAI,kBAAkB,KAAK,UAAU,MAAM,KAAK,GAAG;AACjD,eAAS;AACT;AAAA,IACF;AAEA,QAAI,YAAY;AAEhB,WAAO,QAAQ,cAAc,KAAK,KAAK,MAAM,KAAK;AAChD,mBAAa;AACb,eAAS;AAAA,IACX;AAEA,QAAI,kBAAkB,GAAG;AACvB,sBAAgB;AAAA,IAClB,WAAW,cAAc,eAAe;AACtC,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,gBAAgB;AACzB;AAEA,SAAS,kBACP,MACA,OACA,eACS;AAxFX;AAyFE,QAAM,CAAC,EAAE,UAAU,IAAI,IAAI;AAC3B,QAAM,kBAAkB,MAAM,QAAQ,KAAK,MAAM,QAAQ,CAAC,IAAI;AAG9D,MAAI,oBAAoB,OAAO,UAAU,OAAM,WAAM,UAAN,YAAe,CAAC,GAAG;AAChE,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,OAAM,WAAM,UAAN,YAAe,CAAC,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,SAAO,CAAC,CAAC,SAAS,KAAK,KAAK,cAAc,IAAI;AAChD;AAEA,SAAS,YAAY,OAA0D;AAxG/E;AAyGE,QAAM,CAAC,YAAY,UAAU,MAAM,EAAE,qBAAqB,kBAAkB,gBAAgB,IAC1F;AACF,QAAM,SAAQ,yDAAuB,qBAAvB,YAA2C;AAEzD,SAAO;AAAA,IACL,QAAO,WAAM,UAAN,YAAe;AAAA,IACtB,MAAM;AAAA,IACN,aAAa;AAAA,IACb,MAAM;AAAA,MACJ;AAAA;AAAA,MAEA,OAAO,SAAS;AAAA,MAChB,UAAU;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAAS,eAAe,GAAmB,GAA4B;AACrE,SAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK;AACzE;AAEA,SAAS,0BAA0B,OAAuC;AA9H1E;AA+HE,SAAO;AAAA,IACL,OAAM,WAAM,SAAN,mBAAY;AAAA,IAClB,QAAO,iBAAM,SAAN,mBAAY,UAAZ,YAAqB;AAAA,EAC9B;AACF;AAMO,SAAS,sBAAsB,QAA2C;AAC/E,QAAM,OAAO,cAAc;AAAA,IACzB,MAAM,UAAQ;AACZ,YAAM,QAAQ,0BAA0B,KAAK,IAAI;AAEjD,UAAI,CAAC,SAAS,CAAC,kBAAkB,MAAM,OAAO,OAAO,aAAa,GAAG;AACnE,eAAO;AAAA,MACT;AAEA,aAAO,YAAY,KAAK;AAAA,IAC1B;AAAA,IACA,MAAM,OAAO;AAAA,IACb,eAAe;AAAA,EACjB,CAAC;AAED,SAAO,IAAI,UAAU;AAAA,IACnB,MAAM,KAAK;AAAA,IACX,SAAS,WAAS;AAChB,YAAM,SAAS,KAAK,QAAQ,KAAK;AAEjC,UAAI,WAAW,QAAQ,MAAM,MAAM,GAAG,MAAM,QAAQ;AAClD,cAAM,MAAM,GAAG,QAAQ,mBAAmB,IAAI;AAAA,MAChD;AAEA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAMO,SAAS,sBAAsB,QAAgD;AACpF,QAAM,OAAO,cAAc;AAAA,IACzB,MAAM,UAAQ;AA5KlB;AA6KM,YAAM,kBAAoC,CAAC;AAE3C,iBAAW,SAAS,KAAK,SAAS,yBAAyB,GAAG;AAC5D,YAAI,kBAAkB,MAAM,OAAO,OAAO,aAAa,GAAG;AACxD,0BAAgB,KAAK,YAAY,KAAK,CAAC;AAAA,QACzC;AAAA,MACF;AAEA,YAAM,oBAAmB,kBAAO,kBAAP,gCAAuB,UAAvB,YAAgC,CAAC,GAAG;AAAA,QAC3D,cAAY,CAAC,gBAAgB,KAAK,mBAAiB,eAAe,eAAe,QAAQ,CAAC;AAAA,MAC5F;AAEA,aAAO,CAAC,GAAG,iBAAiB,GAAG,eAAe;AAAA,IAChD;AAAA,IACA,MAAM,OAAO;AAAA,IACb,eAAe;AAAA,EACjB,CAAC;AAED,SAAO,IAAI,UAAU;AAAA,IACnB,MAAM,KAAK;AAAA,IACX,SAAS,WAAS;AAjMtB;AAkMM,YAAM,SAAS,KAAK,QAAQ,KAAK;AAGjC,UAAI,WAAW,QAAQ,MAAM,MAAM,GAAG,MAAM,YAAU,WAAM,MAAM,SAAZ,mBAAkB,WAAU;AAChF,cAAM,MAAM,GAAG,QAAQ,mBAAmB,IAAI;AAAA,MAChD;AAEA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;;;AC1MA,SAAS,UAAAC,SAAQ,aAAAC,kBAAiB;AAClC,SAAS,YAAY;AAWd,SAAS,aAAa,SAAsC;AACjE,SAAO,IAAID,QAAO;AAAA,IAChB,KAAK,IAAIC,WAAU,iBAAiB;AAAA,IACpC,OAAO;AAAA,MACL,aAAa,CAAC,MAAM,QAAQ,UAAU;AACpC,cAAM,EAAE,eAAe,IAAI;AAC3B,cAAM,EAAE,MAAM,IAAI;AAClB,cAAM,EAAE,UAAU,IAAI;AACtB,cAAM,EAAE,MAAM,IAAI;AAElB,YAAI,OAAO;AACT,iBAAO;AAAA,QACT;AAEA,YAAI,cAAc;AAElB,cAAM,QAAQ,QAAQ,UAAQ;AAC5B,yBAAe,KAAK;AAAA,QACtB,CAAC;AAED,cAAM,OAAO,KAAK,aAAa,EAAE,iBAAiB,QAAQ,gBAAgB,CAAC,EAAE;AAAA,UAC3E,UAAQ,KAAK,UAAU,KAAK,UAAU;AAAA,QACxC;AAEA,YACE,CAAC,eACD,CAAC,QACA,mBAAmB,UAAa,CAAC,eAAe,KAAK,KAAK,GAC3D;AACA,iBAAO;AAAA,QACT;AAEA,eAAO,QAAQ,OAAO,SAAS,QAAQ,QAAQ,MAAM;AAAA,UACnD,MAAM,KAAK;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ALxBO,IAAM,aACX;AAkJK,SAAS,aAAa,KAAyB,WAAsC;AAC1F,QAAM,mBAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,WAAW;AACb,cAAU,QAAQ,cAAY;AAC5B,YAAM,eAAe,OAAO,aAAa,WAAW,WAAW,SAAS;AAExE,UAAI,cAAc;AAChB,yBAAiB,KAAK,YAAY;AAAA,MACpC;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SACE,CAAC,OACD,IACG,QAAQ,iCAAiC,EAAE,EAC3C;AAAA,IACC,IAAI;AAAA,MACF,UAAU,iBACP,IAAI,cAAY,SAAS,QAAQ,yBAAyB,MAAM,CAAC,EACjE,KAAK,GAAG,CAAC;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEN;AAMO,IAAM,OAAO,KAAK,OAAoB;AAAA,EAC3C,MAAM;AAAA,EAEN,UAAU;AAAA,EAEV,aAAa;AAAA,EAEb,UAAU;AAAA,EAEV,WAAW;AAET,QAAI,KAAK,QAAQ,YAAY,CAAC,KAAK,QAAQ,gBAAgB;AAEzD,WAAK,QAAQ,iBAAiB,KAAK,QAAQ;AAC3C,cAAQ;AAAA,QACN;AAAA,MACF;AAAA,IACF;AACA,SAAK,QAAQ,UAAU,QAAQ,cAAY;AACzC,UAAI,OAAO,aAAa,UAAU;AAChC,+BAAuB,QAAQ;AAC/B;AAAA,MACF;AACA,6BAAuB,SAAS,QAAQ,SAAS,eAAe;AAAA,IAClE,CAAC;AAAA,EACH;AAAA,EAEA,YAAY;AACV,UAAM;AAAA,EACR;AAAA,EAEA,YAAY;AACV,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,aAAa;AACX,WAAO;AAAA,MACL,aAAa;AAAA,MACb,sBAAsB;AAAA,MACtB,aAAa;AAAA,MACb,eAAe;AAAA;AAAA,MACf,UAAU;AAAA,MACV,WAAW,CAAC;AAAA,MACZ,iBAAiB;AAAA,MACjB,gBAAgB;AAAA,QACd,QAAQ;AAAA,QACR,KAAK;AAAA,QACL,OAAO;AAAA,MACT;AAAA,MACA,cAAc,CAAC,KAAK,QAAQ,CAAC,CAAC,aAAa,KAAK,IAAI,SAAS;AAAA,MAC7D,UAAU,SAAO,CAAC,CAAC;AAAA,MACnB,gBAAgB,SAAO;AAGrB,cAAM,cAAc,2BAA2B,KAAK,GAAG;AACvD,cAAM,mBAAmB,uBAAuB,KAAK,GAAG;AAExD,YAAI,eAAgB,oBAAoB,CAAC,IAAI,SAAS,GAAG,GAAI;AAC3D,iBAAO;AAAA,QACT;AAEA,cAAM,qBAAqB,IAAI,SAAS,GAAG,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI,IAAK;AACvE,cAAM,WAAW,mBAAmB,MAAM,QAAQ,EAAE,CAAC;AAGrD,YAAI,0BAA0B,KAAK,QAAQ,GAAG;AAC5C,iBAAO;AAAA,QACT;AAEA,YAAI,CAAC,KAAK,KAAK,QAAQ,GAAG;AACxB,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EAEA,gBAAgB;AAvSlB;AAwSI,WAAO;AAAA,MACL,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,UAAU,SAAS;AACjB,iBAAO,QAAQ,aAAa,MAAM;AAAA,QACpC;AAAA,MACF;AAAA,MACA,QAAQ;AAAA;AAAA,QAEN,UAAS,UAAK,QAAQ,eAAe,WAA5B,YAAsC;AAAA,MACjD;AAAA,MACA,KAAK;AAAA;AAAA,QAEH,UAAS,UAAK,QAAQ,eAAe,QAA5B,YAAmC;AAAA,MAC9C;AAAA,MACA,OAAO;AAAA;AAAA,QAEL,UAAS,UAAK,QAAQ,eAAe,UAA5B,YAAqC;AAAA,MAChD;AAAA,MACA,OAAO;AAAA,QACL,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EAEA,YAAY;AACV,WAAO;AAAA,MACL;AAAA,QACE,KAAK;AAAA,QACL,UAAU,SAAO;AACf,gBAAM,OAAQ,IAAoB,aAAa,MAAM;AAGrD,cACE,CAAC,QACD,CAAC,KAAK,QAAQ,aAAa,MAAM;AAAA,YAC/B,iBAAiB,SAAO,CAAC,CAAC,aAAa,KAAK,KAAK,QAAQ,SAAS;AAAA,YAClE,WAAW,KAAK,QAAQ;AAAA,YACxB,iBAAiB,KAAK,QAAQ;AAAA,UAChC,CAAC,GACD;AACA,mBAAO;AAAA,UACT;AACA,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,WAAW,EAAE,eAAe,GAAG;AAE7B,QACE,CAAC,KAAK,QAAQ,aAAa,eAAe,MAAM;AAAA,MAC9C,iBAAiB,UAAQ,CAAC,CAAC,aAAa,MAAM,KAAK,QAAQ,SAAS;AAAA,MACpE,WAAW,KAAK,QAAQ;AAAA,MACxB,iBAAiB,KAAK,QAAQ;AAAA,IAChC,CAAC,GACD;AAEA,aAAO,CAAC,KAAK,gBAAgB,KAAK,QAAQ,gBAAgB,EAAE,GAAG,gBAAgB,MAAM,GAAG,CAAC,GAAG,CAAC;AAAA,IAC/F;AAEA,WAAO,CAAC,KAAK,gBAAgB,KAAK,QAAQ,gBAAgB,cAAc,GAAG,CAAC;AAAA,EAC9E;AAAA,EAEA,mBAAmB;AAAA,EAEnB,eAAe,CAAC,OAAO,YAAY;AACjC,WAAO,QAAQ,UAAU,QAAQ,QAAQ,YAAY,MAAM,UAAU,CAAC,CAAC,GAAG;AAAA,MACxE,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM,SAAS;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,gBAAgB,CAAC,MAAM,MAAM;AAlX/B;AAmXI,UAAM,QAAO,gBAAK,UAAL,mBAAY,SAAZ,YAAoB;AACjC,UAAM,SAAQ,gBAAK,UAAL,mBAAY,UAAZ,YAAqB;AACnC,UAAM,OAAO,EAAE,eAAe,IAAI;AAElC,WAAO,QAAQ,IAAI,IAAI,KAAK,IAAI,KAAK,KAAK,OAAO,IAAI,IAAI,KAAK,IAAI;AAAA,EACpE;AAAA,EAEA,cAAc;AACZ,WAAO;AAAA,MACL,SACE,gBACA,CAAC,EAAE,MAAM,MAAM;AACb,cAAM,EAAE,KAAK,IAAI;AAEjB,YACE,CAAC,KAAK,QAAQ,aAAa,MAAM;AAAA,UAC/B,iBAAiB,SAAO,CAAC,CAAC,aAAa,KAAK,KAAK,QAAQ,SAAS;AAAA,UAClE,WAAW,KAAK,QAAQ;AAAA,UACxB,iBAAiB,KAAK,QAAQ;AAAA,QAChC,CAAC,GACD;AACA,iBAAO;AAAA,QACT;AAEA,eAAO,MAAM,EAAE,QAAQ,KAAK,MAAM,UAAU,EAAE,QAAQ,mBAAmB,IAAI,EAAE,IAAI;AAAA,MACrF;AAAA,MAEF,YACE,gBACA,CAAC,EAAE,MAAM,MAAM;AACb,cAAM,EAAE,KAAK,IAAI,cAAc,CAAC;AAEhC,YACE,QACA,CAAC,KAAK,QAAQ,aAAa,MAAM;AAAA,UAC/B,iBAAiB,SAAO,CAAC,CAAC,aAAa,KAAK,KAAK,QAAQ,SAAS;AAAA,UAClE,WAAW,KAAK,QAAQ;AAAA,UACxB,iBAAiB,KAAK,QAAQ;AAAA,QAChC,CAAC,GACD;AACA,iBAAO;AAAA,QACT;AAEA,eAAO,MAAM,EACV,WAAW,KAAK,MAAM,YAAY,EAAE,sBAAsB,KAAK,CAAC,EAChE,QAAQ,mBAAmB,IAAI,EAC/B,IAAI;AAAA,MACT;AAAA,MAEF,WACE,MACA,CAAC,EAAE,MAAM,MAAM;AACb,eAAO,MAAM,EACV,UAAU,KAAK,MAAM,EAAE,sBAAsB,KAAK,CAAC,EACnD,QAAQ,mBAAmB,IAAI,EAC/B,IAAI;AAAA,MACT;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,gBAAgB;AACd,QAAI,CAAC,KAAK,QAAQ,eAAe;AAC/B,aAAO,CAAC;AAAA,IACV;AAEA,WAAO;AAAA,MACL,sBAAsB;AAAA,QACpB,MAAM,KAAK;AAAA,QACX,eAAe,UACb,KAAK,QAAQ,aAAa,MAAM;AAAA,UAC9B,iBAAiB,SAAO,CAAC,CAAC,aAAa,KAAK,KAAK,QAAQ,SAAS;AAAA,UAClE,WAAW,KAAK,QAAQ;AAAA,UACxB,iBAAiB,KAAK,QAAQ;AAAA,QAChC,CAAC;AAAA,MACL,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,gBAAgB;AACd,UAAM,gBAAgB,CAAC,SAAmC;AACxD,YAAM,aAA+B,CAAC;AAEtC,UAAI,MAAM;AACR,cAAM,EAAE,WAAW,gBAAgB,IAAI,KAAK;AAC5C,cAAM,QAAQC,MAAK,IAAI,EAAE;AAAA,UACvB,UACE,KAAK,UACL,KAAK,QAAQ,aAAa,KAAK,OAAO;AAAA,YACpC,iBAAiB,UAAQ,CAAC,CAAC,aAAa,MAAM,SAAS;AAAA,YACvD;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACL;AAEA,cAAM,QAAQ,UAAQ;AACpB,cAAI,CAAC,KAAK,QAAQ,eAAe,KAAK,KAAK,GAAG;AAC5C;AAAA,UACF;AAEA,qBAAW,KAAK;AAAA,YACd,MAAM,KAAK;AAAA,YACX,MAAM;AAAA,cACJ,MAAM,KAAK;AAAA,YACb;AAAA,YACA,OAAO,KAAK;AAAA,UACd,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,QAAQ,eAAe;AAC9B,aAAO;AAAA,QACL,sBAAsB;AAAA,UACpB,MAAM,KAAK;AAAA,UACX,eAAe,UACb,KAAK,QAAQ,aAAa,MAAM;AAAA,YAC9B,iBAAiB,SAAO,CAAC,CAAC,aAAa,KAAK,KAAK,QAAQ,SAAS;AAAA,YAClE,WAAW,KAAK,QAAQ;AAAA,YACxB,iBAAiB,KAAK,QAAQ;AAAA,UAChC,CAAC;AAAA,UACH;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,MACLC,eAAc;AAAA,QACZ,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,QACX,eAAe,WAAS;AAtfhC;AAufU,iBAAO;AAAA,YACL,OAAM,WAAM,SAAN,mBAAY;AAAA,UACpB;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,wBAAwB;AACtB,UAAM,UAAoB,CAAC;AAC3B,UAAM,EAAE,WAAW,gBAAgB,IAAI,KAAK;AAE5C,QAAI,KAAK,QAAQ,UAAU;AACzB,cAAQ;AAAA,QACN,SAAS;AAAA,UACP,MAAM,KAAK;AAAA,UACX,iBAAiB,KAAK,QAAQ;AAAA,UAC9B,UAAU,SACR,KAAK,QAAQ,aAAa,KAAK;AAAA,YAC7B,iBAAiB,UAAQ,CAAC,CAAC,aAAa,MAAM,SAAS;AAAA,YACvD;AAAA,YACA;AAAA,UACF,CAAC;AAAA,UACH,gBAAgB,KAAK,QAAQ;AAAA,QAC/B,CAAC;AAAA,MACH;AAAA,IACF;AAEA,YAAQ;AAAA,MACN,aAAa;AAAA,QACX,MAAM,KAAK;AAAA,QACX,QAAQ,KAAK;AAAA,QACb,aACE,KAAK,QAAQ,gBAAgB,oBAAoB,OAAO,KAAK,QAAQ;AAAA,QACvE,sBAAsB,KAAK,QAAQ;AAAA,MACrC,CAAC;AAAA,IACH;AAEA,QAAI,KAAK,QAAQ,aAAa;AAC5B,cAAQ;AAAA,QACN,aAAa;AAAA,UACX,QAAQ,KAAK;AAAA,UACb,iBAAiB,KAAK,QAAQ;AAAA,UAC9B,MAAM,KAAK;AAAA,UACX,gBAAgB,KAAK,QAAQ;AAAA,QAC/B,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF,CAAC;;;AMtiBD,IAAO,gBAAQ;","names":["markPasteRule","find","Plugin","PluginKey","Plugin","PluginKey","find","markPasteRule"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiptap/extension-link",
3
- "version": "3.28.0",
3
+ "version": "3.29.1",
4
4
  "description": "link extension for tiptap",
5
5
  "keywords": [
6
6
  "tiptap",
@@ -42,12 +42,12 @@
42
42
  "linkifyjs": "^4.3.3"
43
43
  },
44
44
  "devDependencies": {
45
- "@tiptap/core": "^3.28.0",
46
- "@tiptap/pm": "^3.28.0"
45
+ "@tiptap/pm": "^3.29.1",
46
+ "@tiptap/core": "^3.29.1"
47
47
  },
48
48
  "peerDependencies": {
49
- "@tiptap/core": "3.28.0",
50
- "@tiptap/pm": "3.28.0"
49
+ "@tiptap/core": "3.29.1",
50
+ "@tiptap/pm": "3.29.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup"
@@ -0,0 +1,205 @@
1
+ import type { InputRuleMatch, PasteRuleMatch } from '@tiptap/core'
2
+ import { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'
3
+ import type { MarkType } from '@tiptap/pm/model'
4
+
5
+ /**
6
+ * Matches a Markdown link with an optional quoted title.
7
+ * for ex: [Tiptap](https://tiptap.dev) or [Tiptap](https://tiptap.dev "some title")
8
+ * the URL may also contain one level of balanced parentheses, as in CommonMark
9
+ * (titles accept curly quotes too, the Typography extension swaps them in while typing)
10
+ * the title delimiters must come in matching pairs
11
+ */
12
+ const MARKDOWN_LINK_INPUT_REGEX =
13
+ /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)$/
14
+
15
+ /**
16
+ * Same as the input regex but global, to find every Markdown link in pasted text.
17
+ */
18
+ const MARKDOWN_LINK_PASTE_REGEX =
19
+ /\[([^[\]]+)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+(?:(["'])(.*?)\3|“(.*?)”|‘(.*?)’))?\)/g
20
+
21
+ export interface MarkdownLinkRuleConfig {
22
+ type: MarkType
23
+
24
+ /**
25
+ * Return `false` to leave the Markdown syntax untouched.
26
+ */
27
+ isAllowedHref: (href: string) => boolean
28
+ }
29
+
30
+ export interface MarkdownLinkPasteRuleConfig extends MarkdownLinkRuleConfig {
31
+ /**
32
+ * Finds plain URLs to link in the same pass. Matches overlapping a
33
+ * converted Markdown link are dropped so its href is kept.
34
+ */
35
+ findPlainUrls?: (text: string) => PasteRuleMatch[]
36
+ }
37
+
38
+ function isEscaped(text: string, index: number): boolean {
39
+ let backslashes = 0
40
+
41
+ for (let position = index - 1; position >= 0 && text[position] === '\\'; position -= 1) {
42
+ backslashes += 1
43
+ }
44
+
45
+ return backslashes % 2 === 1
46
+ }
47
+
48
+ /**
49
+ * Pairs the backtick runs before the match by length, as CommonMark does.
50
+ * A run left open means the match sits in an unfinished code span.
51
+ */
52
+ function isInsideCodeSpan(text: string, matchIndex: number): boolean {
53
+ let openRunLength = 0
54
+ let index = 0
55
+
56
+ while (index < matchIndex) {
57
+ if (text[index] !== '`') {
58
+ index += 1
59
+ continue
60
+ }
61
+
62
+ // escapes only apply outside code spans
63
+ if (openRunLength === 0 && isEscaped(text, index)) {
64
+ index += 1
65
+ continue
66
+ }
67
+
68
+ let runLength = 0
69
+
70
+ while (index < matchIndex && text[index] === '`') {
71
+ runLength += 1
72
+ index += 1
73
+ }
74
+
75
+ if (openRunLength === 0) {
76
+ openRunLength = runLength
77
+ } else if (runLength === openRunLength) {
78
+ openRunLength = 0
79
+ }
80
+ }
81
+
82
+ return openRunLength > 0
83
+ }
84
+
85
+ function isConvertibleLink(
86
+ text: string,
87
+ match: RegExpMatchArray,
88
+ isAllowedHref: MarkdownLinkRuleConfig['isAllowedHref'],
89
+ ): boolean {
90
+ const [, linkText, href] = match
91
+ const characterBefore = match.index ? text[match.index - 1] : undefined
92
+
93
+ // `!` is the Markdown image syntax, `\` may escape the opening bracket
94
+ if (characterBefore === '!' || isEscaped(text, match.index ?? 0)) {
95
+ return false
96
+ }
97
+
98
+ if (isInsideCodeSpan(text, match.index ?? 0)) {
99
+ return false
100
+ }
101
+
102
+ return !!linkText.trim() && isAllowedHref(href)
103
+ }
104
+
105
+ function toRuleMatch(match: RegExpMatchArray): InputRuleMatch & PasteRuleMatch {
106
+ const [linkSyntax, linkText, href, , straightQuotedTitle, curlyDoubleTitle, curlySingleTitle] =
107
+ match
108
+ const title = straightQuotedTitle ?? curlyDoubleTitle ?? curlySingleTitle
109
+
110
+ return {
111
+ index: match.index ?? 0,
112
+ text: linkSyntax,
113
+ replaceWith: linkText,
114
+ data: {
115
+ href,
116
+ // an empty title ("") counts as no title, as in CommonMark
117
+ title: title || null,
118
+ markdown: true,
119
+ },
120
+ }
121
+ }
122
+
123
+ function matchesOverlap(a: PasteRuleMatch, b: PasteRuleMatch): boolean {
124
+ return a.index < b.index + b.text.length && b.index < a.index + a.text.length
125
+ }
126
+
127
+ function getMarkdownLinkAttributes(match: { data?: Record<string, any> }) {
128
+ return {
129
+ href: match.data?.href,
130
+ title: match.data?.title ?? null,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Turns typed Markdown link syntax into a link mark as soon as the closing `)` comes in.
136
+ * The transaction gets flagged so autolink doesn't touch the converted text again.
137
+ */
138
+ export function markdownLinkInputRule(config: MarkdownLinkRuleConfig): InputRule {
139
+ const rule = markInputRule({
140
+ find: text => {
141
+ const match = MARKDOWN_LINK_INPUT_REGEX.exec(text)
142
+
143
+ if (!match || !isConvertibleLink(text, match, config.isAllowedHref)) {
144
+ return null
145
+ }
146
+
147
+ return toRuleMatch(match)
148
+ },
149
+ type: config.type,
150
+ getAttributes: getMarkdownLinkAttributes,
151
+ })
152
+
153
+ return new InputRule({
154
+ find: rule.find,
155
+ handler: props => {
156
+ const result = rule.handler(props)
157
+
158
+ if (result !== null && props.state.tr.steps.length) {
159
+ props.state.tr.setMeta('preventAutolink', true)
160
+ }
161
+
162
+ return result
163
+ },
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Same for pasting, converts every Markdown link found in the pasted text
169
+ * and links the plain URLs from `findPlainUrls`.
170
+ */
171
+ export function markdownLinkPasteRule(config: MarkdownLinkPasteRuleConfig): PasteRule {
172
+ const rule = markPasteRule({
173
+ find: text => {
174
+ const markdownMatches: PasteRuleMatch[] = []
175
+
176
+ for (const match of text.matchAll(MARKDOWN_LINK_PASTE_REGEX)) {
177
+ if (isConvertibleLink(text, match, config.isAllowedHref)) {
178
+ markdownMatches.push(toRuleMatch(match))
179
+ }
180
+ }
181
+
182
+ const plainUrlMatches = (config.findPlainUrls?.(text) ?? []).filter(
183
+ urlMatch => !markdownMatches.some(markdownMatch => matchesOverlap(markdownMatch, urlMatch)),
184
+ )
185
+
186
+ return [...markdownMatches, ...plainUrlMatches]
187
+ },
188
+ type: config.type,
189
+ getAttributes: getMarkdownLinkAttributes,
190
+ })
191
+
192
+ return new PasteRule({
193
+ find: rule.find,
194
+ handler: props => {
195
+ const result = rule.handler(props)
196
+
197
+ // only Markdown conversions suppress autolink
198
+ if (result !== null && props.state.tr.steps.length && props.match.data?.markdown) {
199
+ props.state.tr.setMeta('preventAutolink', true)
200
+ }
201
+
202
+ return result
203
+ },
204
+ })
205
+ }
package/src/link.ts CHANGED
@@ -5,6 +5,7 @@ import { find, registerCustomProtocol, reset } from 'linkifyjs'
5
5
 
6
6
  import { autolink } from './helpers/autolink.js'
7
7
  import { clickHandler } from './helpers/clickHandler.js'
8
+ import { markdownLinkInputRule, markdownLinkPasteRule } from './helpers/markdownLink.js'
8
9
  import { pasteHandler } from './helpers/pasteHandler.js'
9
10
  import { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'
10
11
 
@@ -72,6 +73,14 @@ export interface LinkOptions {
72
73
  */
73
74
  linkOnPaste: boolean
74
75
 
76
+ /**
77
+ * If enabled, typing or pasting the Markdown link syntax, e.g. `[Tiptap](https://tiptap.dev)`
78
+ * or `[Tiptap](https://tiptap.dev "Rich text editor")`, converts it into a link.
79
+ * @default false
80
+ * @example true
81
+ */
82
+ markdownLinks: boolean
83
+
75
84
  /**
76
85
  * HTML attributes to add to the link element.
77
86
  * @default {}
@@ -247,6 +256,7 @@ export const Link = Mark.create<LinkOptions>({
247
256
  openOnClick: true,
248
257
  enableClickSelection: false,
249
258
  linkOnPaste: true,
259
+ markdownLinks: false, // TODO (major) - default to true on next major version
250
260
  autolink: true,
251
261
  protocols: [],
252
262
  defaultProtocol: 'http',
@@ -419,43 +429,76 @@ export const Link = Mark.create<LinkOptions>({
419
429
  }
420
430
  },
421
431
 
422
- addPasteRules() {
432
+ addInputRules() {
433
+ if (!this.options.markdownLinks) {
434
+ return []
435
+ }
436
+
423
437
  return [
424
- markPasteRule({
425
- find: text => {
426
- const foundLinks: PasteRuleMatch[] = []
427
-
428
- if (text) {
429
- const { protocols, defaultProtocol } = this.options
430
- const links = find(text).filter(
431
- item =>
432
- item.isLink &&
433
- this.options.isAllowedUri(item.value, {
434
- defaultValidate: href => !!isAllowedUri(href, protocols),
435
- protocols,
436
- defaultProtocol,
437
- }),
438
- )
439
-
440
- if (links.length) {
441
- links.forEach(link => {
442
- if (!this.options.shouldAutoLink(link.value)) {
443
- return
444
- }
445
-
446
- foundLinks.push({
447
- text: link.value,
448
- data: {
449
- href: link.href,
450
- },
451
- index: link.start,
452
- })
453
- })
454
- }
438
+ markdownLinkInputRule({
439
+ type: this.type,
440
+ isAllowedHref: href =>
441
+ this.options.isAllowedUri(href, {
442
+ defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
443
+ protocols: this.options.protocols,
444
+ defaultProtocol: this.options.defaultProtocol,
445
+ }),
446
+ }),
447
+ ]
448
+ },
449
+
450
+ addPasteRules() {
451
+ const findPlainUrls = (text: string): PasteRuleMatch[] => {
452
+ const foundLinks: PasteRuleMatch[] = []
453
+
454
+ if (text) {
455
+ const { protocols, defaultProtocol } = this.options
456
+ const links = find(text).filter(
457
+ item =>
458
+ item.isLink &&
459
+ this.options.isAllowedUri(item.value, {
460
+ defaultValidate: href => !!isAllowedUri(href, protocols),
461
+ protocols,
462
+ defaultProtocol,
463
+ }),
464
+ )
465
+
466
+ links.forEach(link => {
467
+ if (!this.options.shouldAutoLink(link.value)) {
468
+ return
455
469
  }
456
470
 
457
- return foundLinks
458
- },
471
+ foundLinks.push({
472
+ text: link.value,
473
+ data: {
474
+ href: link.href,
475
+ },
476
+ index: link.start,
477
+ })
478
+ })
479
+ }
480
+
481
+ return foundLinks
482
+ }
483
+
484
+ if (this.options.markdownLinks) {
485
+ return [
486
+ markdownLinkPasteRule({
487
+ type: this.type,
488
+ isAllowedHref: href =>
489
+ this.options.isAllowedUri(href, {
490
+ defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
491
+ protocols: this.options.protocols,
492
+ defaultProtocol: this.options.defaultProtocol,
493
+ }),
494
+ findPlainUrls,
495
+ }),
496
+ ]
497
+ }
498
+
499
+ return [
500
+ markPasteRule({
501
+ find: findPlainUrls,
459
502
  type: this.type,
460
503
  getAttributes: match => {
461
504
  return {