@tiptap/extension-link 3.0.0-next.1 → 3.0.0-next.2
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 +77 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +77 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/helpers/autolink.ts +3 -0
- package/src/helpers/clickHandler.ts +1 -1
- package/src/link.ts +164 -33
package/dist/index.cjs
CHANGED
|
@@ -107,7 +107,7 @@ function autolink(options) {
|
|
|
107
107
|
link.to,
|
|
108
108
|
newState.schema.marks.code
|
|
109
109
|
);
|
|
110
|
-
}).filter((link) => options.validate(link.value)).forEach((link) => {
|
|
110
|
+
}).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => {
|
|
111
111
|
if ((0, import_core.getMarksBetween)(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
@@ -201,9 +201,34 @@ function pasteHandler(options) {
|
|
|
201
201
|
// src/link.ts
|
|
202
202
|
var pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
|
|
203
203
|
var ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
function isAllowedUri(uri, protocols) {
|
|
205
|
+
const allowedProtocols = [
|
|
206
|
+
"http",
|
|
207
|
+
"https",
|
|
208
|
+
"ftp",
|
|
209
|
+
"ftps",
|
|
210
|
+
"mailto",
|
|
211
|
+
"tel",
|
|
212
|
+
"callto",
|
|
213
|
+
"sms",
|
|
214
|
+
"cid",
|
|
215
|
+
"xmpp"
|
|
216
|
+
];
|
|
217
|
+
if (protocols) {
|
|
218
|
+
protocols.forEach((protocol) => {
|
|
219
|
+
const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
|
|
220
|
+
if (nextProtocol) {
|
|
221
|
+
allowedProtocols.push(nextProtocol);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return !uri || uri.replace(ATTR_WHITESPACE, "").match(
|
|
226
|
+
new RegExp(
|
|
227
|
+
// eslint-disable-next-line no-useless-escape
|
|
228
|
+
`^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))`,
|
|
229
|
+
"i"
|
|
230
|
+
)
|
|
231
|
+
);
|
|
207
232
|
}
|
|
208
233
|
var Link = import_core3.Mark.create({
|
|
209
234
|
name: "link",
|
|
@@ -211,6 +236,12 @@ var Link = import_core3.Mark.create({
|
|
|
211
236
|
keepOnSplit: false,
|
|
212
237
|
exitable: true,
|
|
213
238
|
onCreate() {
|
|
239
|
+
if (this.options.validate && !this.options.shouldAutoLink) {
|
|
240
|
+
this.options.shouldAutoLink = this.options.validate;
|
|
241
|
+
console.warn(
|
|
242
|
+
"The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead."
|
|
243
|
+
);
|
|
244
|
+
}
|
|
214
245
|
this.options.protocols.forEach((protocol) => {
|
|
215
246
|
if (typeof protocol === "string") {
|
|
216
247
|
(0, import_linkifyjs3.registerCustomProtocol)(protocol);
|
|
@@ -237,7 +268,9 @@ var Link = import_core3.Mark.create({
|
|
|
237
268
|
rel: "noopener noreferrer nofollow",
|
|
238
269
|
class: null
|
|
239
270
|
},
|
|
240
|
-
|
|
271
|
+
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
|
272
|
+
validate: (url) => !!url,
|
|
273
|
+
shouldAutoLink: (url) => !!url
|
|
241
274
|
};
|
|
242
275
|
},
|
|
243
276
|
addAttributes() {
|
|
@@ -260,20 +293,34 @@ var Link = import_core3.Mark.create({
|
|
|
260
293
|
};
|
|
261
294
|
},
|
|
262
295
|
parseHTML() {
|
|
263
|
-
return [
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
296
|
+
return [
|
|
297
|
+
{
|
|
298
|
+
tag: "a[href]",
|
|
299
|
+
getAttrs: (dom) => {
|
|
300
|
+
const href = dom.getAttribute("href");
|
|
301
|
+
if (!href || !this.options.isAllowedUri(href, {
|
|
302
|
+
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
|
303
|
+
protocols: this.options.protocols,
|
|
304
|
+
defaultProtocol: this.options.defaultProtocol
|
|
305
|
+
})) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
269
309
|
}
|
|
270
|
-
return null;
|
|
271
310
|
}
|
|
272
|
-
|
|
311
|
+
];
|
|
273
312
|
},
|
|
274
313
|
renderHTML({ HTMLAttributes }) {
|
|
275
|
-
if (!isAllowedUri(HTMLAttributes.href
|
|
276
|
-
|
|
314
|
+
if (!this.options.isAllowedUri(HTMLAttributes.href, {
|
|
315
|
+
defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols),
|
|
316
|
+
protocols: this.options.protocols,
|
|
317
|
+
defaultProtocol: this.options.defaultProtocol
|
|
318
|
+
})) {
|
|
319
|
+
return [
|
|
320
|
+
"a",
|
|
321
|
+
(0, import_core3.mergeAttributes)(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }),
|
|
322
|
+
0
|
|
323
|
+
];
|
|
277
324
|
}
|
|
278
325
|
return ["a", (0, import_core3.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes), 0];
|
|
279
326
|
},
|
|
@@ -296,8 +343,14 @@ var Link = import_core3.Mark.create({
|
|
|
296
343
|
find: (text) => {
|
|
297
344
|
const foundLinks = [];
|
|
298
345
|
if (text) {
|
|
299
|
-
const {
|
|
300
|
-
const links = (0, import_linkifyjs3.find)(text
|
|
346
|
+
const { protocols, defaultProtocol } = this.options;
|
|
347
|
+
const links = (0, import_linkifyjs3.find)(text.replaceAll("\uFFFC", " ")).filter(
|
|
348
|
+
(item) => item.isLink && this.options.isAllowedUri(item.value, {
|
|
349
|
+
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
|
350
|
+
protocols,
|
|
351
|
+
defaultProtocol
|
|
352
|
+
})
|
|
353
|
+
);
|
|
301
354
|
if (links.length) {
|
|
302
355
|
links.forEach((link) => foundLinks.push({
|
|
303
356
|
text: link.value,
|
|
@@ -322,12 +375,18 @@ var Link = import_core3.Mark.create({
|
|
|
322
375
|
},
|
|
323
376
|
addProseMirrorPlugins() {
|
|
324
377
|
const plugins = [];
|
|
378
|
+
const { protocols, defaultProtocol } = this.options;
|
|
325
379
|
if (this.options.autolink) {
|
|
326
380
|
plugins.push(
|
|
327
381
|
autolink({
|
|
328
382
|
type: this.type,
|
|
329
383
|
defaultProtocol: this.options.defaultProtocol,
|
|
330
|
-
validate: this.options.
|
|
384
|
+
validate: (url) => this.options.isAllowedUri(url, {
|
|
385
|
+
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
|
386
|
+
protocols,
|
|
387
|
+
defaultProtocol
|
|
388
|
+
}),
|
|
389
|
+
shouldAutoLink: this.options.shouldAutoLink
|
|
331
390
|
})
|
|
332
391
|
);
|
|
333
392
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/link.ts","../src/helpers/autolink.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 {\n Mark, markPasteRule, mergeAttributes, PasteRuleMatch,\n} from '@tiptap/core'\nimport { 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'\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 = /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 * 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 * 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\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: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => 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: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => ReturnType\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType\n }\n }\n}\n\n// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js\nconst ATTR_WHITESPACE = /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\nconst IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n\nfunction isAllowedUri(uri: string | undefined) {\n return !uri || uri.replace(ATTR_WHITESPACE, '').match(IS_ALLOWED_URI)\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 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 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 validate: url => !!url,\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 default: this.options.HTMLAttributes.target,\n },\n rel: {\n default: this.options.HTMLAttributes.rel,\n },\n class: {\n default: this.options.HTMLAttributes.class,\n },\n }\n },\n\n parseHTML() {\n return [{\n tag: 'a[href]',\n getAttrs: dom => {\n const href = (dom as HTMLElement).getAttribute('href')\n\n // prevent XSS attacks\n if (!href || !isAllowedUri(href)) {\n return false\n }\n return null\n },\n }]\n },\n\n renderHTML({ HTMLAttributes }) {\n // prevent XSS attacks\n if (!isAllowedUri(HTMLAttributes.href)) {\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 addCommands() {\n return {\n setLink:\n attributes => ({ chain }) => {\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes => ({ chain }) => {\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\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 { validate } = this.options\n const links = find(text).filter(item => item.isLink && validate(item.value))\n\n if (links.length) {\n links.forEach(link => (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 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\n if (this.options.autolink) {\n plugins.push(\n autolink({\n type: this.type,\n defaultProtocol: this.options.defaultProtocol,\n validate: this.options.validate,\n }),\n )\n }\n\n if (this.options.openOnClick === true) {\n plugins.push(\n clickHandler({\n type: this.type,\n }),\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 }),\n )\n }\n\n return plugins\n },\n})\n","import {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n NodeWithPos,\n} from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { MultiToken, tokenize } from 'linkifyjs'\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}\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 = 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 => transaction.getMeta('preventAutolink'))\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 (\n nodesInChangedRanges.length\n // We want to make sure to include the block seperator argument to treat hard breaks like spaces.\n && newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ').endsWith(' ')\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.split(' ').filter(s => s !== '')\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))\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(\n link.from,\n link.to,\n newState.schema.marks.code,\n )\n })\n // validate link\n .filter(link => options.validate(link.value))\n // Add link mark.\n .forEach(link => {\n if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {\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","import { getAttributes } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType;\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 a = event.target as HTMLElement\n const els = []\n\n while (a.nodeName !== 'DIV') {\n els.push(a)\n a = a.parentNode as HTMLElement\n }\n\n if (!els.find(value => value.nodeName === 'A')) {\n return false\n }\n\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLLinkElement)\n\n const href = link?.href ?? attrs.href\n const target = link?.target ?? attrs.target\n\n if (link && href) {\n window.open(href, target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\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 { 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(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\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/clickHandler.ts","../src/helpers/pasteHandler.ts"],"sourcesContent":["import { Link } from './link.js'\n\nexport * from './link.js'\n\nexport default Link\n","import {\n Mark, markPasteRule, mergeAttributes, PasteRuleMatch,\n} from '@tiptap/core'\nimport { 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'\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 = /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 * 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 }) => 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 }) => ReturnType;\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType;\n };\n }\n}\n\n// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js\n// eslint-disable-next-line no-control-regex\nconst ATTR_WHITESPACE = /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g\n\nfunction 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(ATTR_WHITESPACE, '')\n .match(\n new RegExp(\n // eslint-disable-next-line no-useless-escape\n `^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\\-]+(?:[^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 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 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 => !!url,\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 default: this.options.HTMLAttributes.target,\n },\n rel: {\n default: this.options.HTMLAttributes.rel,\n },\n class: {\n default: this.options.HTMLAttributes.class,\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 [\n 'a',\n mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }),\n 0,\n ]\n }\n\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n addCommands() {\n return {\n setLink:\n attributes => ({ chain }) => {\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes => ({ chain }) => {\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\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 // Prosemirror replaces zero-width non-joiner characters\n // with Object Replacement Character from unicode.\n // Therefore, linkifyjs does not recognize the\n // link properly. We replace these characters\n // with regular spaces to fix this issue.\n const links = find(text.replaceAll('\\uFFFC', ' ')).filter(\n item => 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 => 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 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 => 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 if (this.options.openOnClick === true) {\n plugins.push(\n clickHandler({\n type: this.type,\n }),\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 }),\n )\n }\n\n return plugins\n },\n})\n","import {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n NodeWithPos,\n} from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { MultiToken, tokenize } from 'linkifyjs'\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 = 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 => transaction.getMeta('preventAutolink'))\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 (\n nodesInChangedRanges.length\n // We want to make sure to include the block seperator argument to treat hard breaks like spaces.\n && newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ').endsWith(' ')\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.split(' ').filter(s => s !== '')\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))\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(\n link.from,\n link.to,\n newState.schema.marks.code,\n )\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 (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {\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","import { getAttributes } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType;\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 a = event.target as HTMLElement\n const els = []\n\n while (a.nodeName !== 'DIV') {\n els.push(a)\n a = a.parentNode as HTMLElement\n }\n\n if (!els.find(value => value.nodeName === 'A')) {\n return false\n }\n\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLAnchorElement)\n\n const href = link?.href ?? attrs.href\n const target = link?.target ?? attrs.target\n\n if (link && href) {\n window.open(href, target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\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 { 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(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\n },\n },\n })\n}\n"],"mappings":"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","names":["import_core","import_linkifyjs","import_core","import_state","import_state","import_linkifyjs"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -57,11 +57,48 @@ interface LinkOptions {
|
|
|
57
57
|
*/
|
|
58
58
|
HTMLAttributes: Record<string, any>;
|
|
59
59
|
/**
|
|
60
|
+
* @deprecated Use the `shouldAutoLink` option instead.
|
|
60
61
|
* A validation function that modifies link verification for the auto linker.
|
|
61
62
|
* @param url - The url to be validated.
|
|
62
63
|
* @returns - True if the url is valid, false otherwise.
|
|
63
64
|
*/
|
|
64
65
|
validate: (url: string) => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
|
68
|
+
* Only modify this if you know what you're doing.
|
|
69
|
+
*
|
|
70
|
+
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
|
74
|
+
* return url.startsWith('./') || defaultValidate(url)
|
|
75
|
+
* }
|
|
76
|
+
*/
|
|
77
|
+
isAllowedUri: (
|
|
78
|
+
/**
|
|
79
|
+
* The URL to be validated.
|
|
80
|
+
*/
|
|
81
|
+
url: string, ctx: {
|
|
82
|
+
/**
|
|
83
|
+
* The default validation function.
|
|
84
|
+
*/
|
|
85
|
+
defaultValidate: (url: string) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
|
88
|
+
*/
|
|
89
|
+
protocols: Array<LinkProtocolOptions | string>;
|
|
90
|
+
/**
|
|
91
|
+
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
|
92
|
+
*/
|
|
93
|
+
defaultProtocol: string;
|
|
94
|
+
}) => boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Determines whether a valid link should be automatically linked in the content.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} url - The URL that has already been validated.
|
|
99
|
+
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
|
100
|
+
*/
|
|
101
|
+
shouldAutoLink: (url: string) => boolean;
|
|
65
102
|
}
|
|
66
103
|
declare module '@tiptap/core' {
|
|
67
104
|
interface Commands<ReturnType> {
|
package/dist/index.d.ts
CHANGED
|
@@ -57,11 +57,48 @@ interface LinkOptions {
|
|
|
57
57
|
*/
|
|
58
58
|
HTMLAttributes: Record<string, any>;
|
|
59
59
|
/**
|
|
60
|
+
* @deprecated Use the `shouldAutoLink` option instead.
|
|
60
61
|
* A validation function that modifies link verification for the auto linker.
|
|
61
62
|
* @param url - The url to be validated.
|
|
62
63
|
* @returns - True if the url is valid, false otherwise.
|
|
63
64
|
*/
|
|
64
65
|
validate: (url: string) => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
|
68
|
+
* Only modify this if you know what you're doing.
|
|
69
|
+
*
|
|
70
|
+
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
|
74
|
+
* return url.startsWith('./') || defaultValidate(url)
|
|
75
|
+
* }
|
|
76
|
+
*/
|
|
77
|
+
isAllowedUri: (
|
|
78
|
+
/**
|
|
79
|
+
* The URL to be validated.
|
|
80
|
+
*/
|
|
81
|
+
url: string, ctx: {
|
|
82
|
+
/**
|
|
83
|
+
* The default validation function.
|
|
84
|
+
*/
|
|
85
|
+
defaultValidate: (url: string) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
|
88
|
+
*/
|
|
89
|
+
protocols: Array<LinkProtocolOptions | string>;
|
|
90
|
+
/**
|
|
91
|
+
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
|
92
|
+
*/
|
|
93
|
+
defaultProtocol: string;
|
|
94
|
+
}) => boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Determines whether a valid link should be automatically linked in the content.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} url - The URL that has already been validated.
|
|
99
|
+
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
|
100
|
+
*/
|
|
101
|
+
shouldAutoLink: (url: string) => boolean;
|
|
65
102
|
}
|
|
66
103
|
declare module '@tiptap/core' {
|
|
67
104
|
interface Commands<ReturnType> {
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ function autolink(options) {
|
|
|
88
88
|
link.to,
|
|
89
89
|
newState.schema.marks.code
|
|
90
90
|
);
|
|
91
|
-
}).filter((link) => options.validate(link.value)).forEach((link) => {
|
|
91
|
+
}).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => {
|
|
92
92
|
if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
@@ -182,9 +182,34 @@ function pasteHandler(options) {
|
|
|
182
182
|
// src/link.ts
|
|
183
183
|
var pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi;
|
|
184
184
|
var ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
function isAllowedUri(uri, protocols) {
|
|
186
|
+
const allowedProtocols = [
|
|
187
|
+
"http",
|
|
188
|
+
"https",
|
|
189
|
+
"ftp",
|
|
190
|
+
"ftps",
|
|
191
|
+
"mailto",
|
|
192
|
+
"tel",
|
|
193
|
+
"callto",
|
|
194
|
+
"sms",
|
|
195
|
+
"cid",
|
|
196
|
+
"xmpp"
|
|
197
|
+
];
|
|
198
|
+
if (protocols) {
|
|
199
|
+
protocols.forEach((protocol) => {
|
|
200
|
+
const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme;
|
|
201
|
+
if (nextProtocol) {
|
|
202
|
+
allowedProtocols.push(nextProtocol);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return !uri || uri.replace(ATTR_WHITESPACE, "").match(
|
|
207
|
+
new RegExp(
|
|
208
|
+
// eslint-disable-next-line no-useless-escape
|
|
209
|
+
`^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))`,
|
|
210
|
+
"i"
|
|
211
|
+
)
|
|
212
|
+
);
|
|
188
213
|
}
|
|
189
214
|
var Link = Mark.create({
|
|
190
215
|
name: "link",
|
|
@@ -192,6 +217,12 @@ var Link = Mark.create({
|
|
|
192
217
|
keepOnSplit: false,
|
|
193
218
|
exitable: true,
|
|
194
219
|
onCreate() {
|
|
220
|
+
if (this.options.validate && !this.options.shouldAutoLink) {
|
|
221
|
+
this.options.shouldAutoLink = this.options.validate;
|
|
222
|
+
console.warn(
|
|
223
|
+
"The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
195
226
|
this.options.protocols.forEach((protocol) => {
|
|
196
227
|
if (typeof protocol === "string") {
|
|
197
228
|
registerCustomProtocol(protocol);
|
|
@@ -218,7 +249,9 @@ var Link = Mark.create({
|
|
|
218
249
|
rel: "noopener noreferrer nofollow",
|
|
219
250
|
class: null
|
|
220
251
|
},
|
|
221
|
-
|
|
252
|
+
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
|
253
|
+
validate: (url) => !!url,
|
|
254
|
+
shouldAutoLink: (url) => !!url
|
|
222
255
|
};
|
|
223
256
|
},
|
|
224
257
|
addAttributes() {
|
|
@@ -241,20 +274,34 @@ var Link = Mark.create({
|
|
|
241
274
|
};
|
|
242
275
|
},
|
|
243
276
|
parseHTML() {
|
|
244
|
-
return [
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
277
|
+
return [
|
|
278
|
+
{
|
|
279
|
+
tag: "a[href]",
|
|
280
|
+
getAttrs: (dom) => {
|
|
281
|
+
const href = dom.getAttribute("href");
|
|
282
|
+
if (!href || !this.options.isAllowedUri(href, {
|
|
283
|
+
defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols),
|
|
284
|
+
protocols: this.options.protocols,
|
|
285
|
+
defaultProtocol: this.options.defaultProtocol
|
|
286
|
+
})) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
250
290
|
}
|
|
251
|
-
return null;
|
|
252
291
|
}
|
|
253
|
-
|
|
292
|
+
];
|
|
254
293
|
},
|
|
255
294
|
renderHTML({ HTMLAttributes }) {
|
|
256
|
-
if (!isAllowedUri(HTMLAttributes.href
|
|
257
|
-
|
|
295
|
+
if (!this.options.isAllowedUri(HTMLAttributes.href, {
|
|
296
|
+
defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols),
|
|
297
|
+
protocols: this.options.protocols,
|
|
298
|
+
defaultProtocol: this.options.defaultProtocol
|
|
299
|
+
})) {
|
|
300
|
+
return [
|
|
301
|
+
"a",
|
|
302
|
+
mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }),
|
|
303
|
+
0
|
|
304
|
+
];
|
|
258
305
|
}
|
|
259
306
|
return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
|
260
307
|
},
|
|
@@ -277,8 +324,14 @@ var Link = Mark.create({
|
|
|
277
324
|
find: (text) => {
|
|
278
325
|
const foundLinks = [];
|
|
279
326
|
if (text) {
|
|
280
|
-
const {
|
|
281
|
-
const links = find2(text
|
|
327
|
+
const { protocols, defaultProtocol } = this.options;
|
|
328
|
+
const links = find2(text.replaceAll("\uFFFC", " ")).filter(
|
|
329
|
+
(item) => item.isLink && this.options.isAllowedUri(item.value, {
|
|
330
|
+
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
|
331
|
+
protocols,
|
|
332
|
+
defaultProtocol
|
|
333
|
+
})
|
|
334
|
+
);
|
|
282
335
|
if (links.length) {
|
|
283
336
|
links.forEach((link) => foundLinks.push({
|
|
284
337
|
text: link.value,
|
|
@@ -303,12 +356,18 @@ var Link = Mark.create({
|
|
|
303
356
|
},
|
|
304
357
|
addProseMirrorPlugins() {
|
|
305
358
|
const plugins = [];
|
|
359
|
+
const { protocols, defaultProtocol } = this.options;
|
|
306
360
|
if (this.options.autolink) {
|
|
307
361
|
plugins.push(
|
|
308
362
|
autolink({
|
|
309
363
|
type: this.type,
|
|
310
364
|
defaultProtocol: this.options.defaultProtocol,
|
|
311
|
-
validate: this.options.
|
|
365
|
+
validate: (url) => this.options.isAllowedUri(url, {
|
|
366
|
+
defaultValidate: (href) => !!isAllowedUri(href, protocols),
|
|
367
|
+
protocols,
|
|
368
|
+
defaultProtocol
|
|
369
|
+
}),
|
|
370
|
+
shouldAutoLink: this.options.shouldAutoLink
|
|
312
371
|
})
|
|
313
372
|
);
|
|
314
373
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/link.ts","../src/helpers/autolink.ts","../src/helpers/clickHandler.ts","../src/helpers/pasteHandler.ts","../src/index.ts"],"sourcesContent":["import {\n Mark, markPasteRule, mergeAttributes, PasteRuleMatch,\n} from '@tiptap/core'\nimport { 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'\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 = /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 * 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 * 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\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: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => 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: { href: string; target?: string | null; rel?: string | null; class?: string | null }) => ReturnType\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType\n }\n }\n}\n\n// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js\nconst ATTR_WHITESPACE = /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\nconst IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n\nfunction isAllowedUri(uri: string | undefined) {\n return !uri || uri.replace(ATTR_WHITESPACE, '').match(IS_ALLOWED_URI)\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 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 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 validate: url => !!url,\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 default: this.options.HTMLAttributes.target,\n },\n rel: {\n default: this.options.HTMLAttributes.rel,\n },\n class: {\n default: this.options.HTMLAttributes.class,\n },\n }\n },\n\n parseHTML() {\n return [{\n tag: 'a[href]',\n getAttrs: dom => {\n const href = (dom as HTMLElement).getAttribute('href')\n\n // prevent XSS attacks\n if (!href || !isAllowedUri(href)) {\n return false\n }\n return null\n },\n }]\n },\n\n renderHTML({ HTMLAttributes }) {\n // prevent XSS attacks\n if (!isAllowedUri(HTMLAttributes.href)) {\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 addCommands() {\n return {\n setLink:\n attributes => ({ chain }) => {\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes => ({ chain }) => {\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\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 { validate } = this.options\n const links = find(text).filter(item => item.isLink && validate(item.value))\n\n if (links.length) {\n links.forEach(link => (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 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\n if (this.options.autolink) {\n plugins.push(\n autolink({\n type: this.type,\n defaultProtocol: this.options.defaultProtocol,\n validate: this.options.validate,\n }),\n )\n }\n\n if (this.options.openOnClick === true) {\n plugins.push(\n clickHandler({\n type: this.type,\n }),\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 }),\n )\n }\n\n return plugins\n },\n})\n","import {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n NodeWithPos,\n} from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { MultiToken, tokenize } from 'linkifyjs'\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}\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 = 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 => transaction.getMeta('preventAutolink'))\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 (\n nodesInChangedRanges.length\n // We want to make sure to include the block seperator argument to treat hard breaks like spaces.\n && newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ').endsWith(' ')\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.split(' ').filter(s => s !== '')\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))\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(\n link.from,\n link.to,\n newState.schema.marks.code,\n )\n })\n // validate link\n .filter(link => options.validate(link.value))\n // Add link mark.\n .forEach(link => {\n if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {\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","import { getAttributes } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType;\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 a = event.target as HTMLElement\n const els = []\n\n while (a.nodeName !== 'DIV') {\n els.push(a)\n a = a.parentNode as HTMLElement\n }\n\n if (!els.find(value => value.nodeName === 'A')) {\n return false\n }\n\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLLinkElement)\n\n const href = link?.href ?? attrs.href\n const target = link?.target ?? attrs.target\n\n if (link && href) {\n window.open(href, target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\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 { 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(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\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/clickHandler.ts","../src/helpers/pasteHandler.ts","../src/index.ts"],"sourcesContent":["import {\n Mark, markPasteRule, mergeAttributes, PasteRuleMatch,\n} from '@tiptap/core'\nimport { 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'\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 = /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 * 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 }) => 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 }) => ReturnType;\n /**\n * Unset a link mark\n * @example editor.commands.unsetLink()\n */\n unsetLink: () => ReturnType;\n };\n }\n}\n\n// From DOMPurify\n// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js\n// eslint-disable-next-line no-control-regex\nconst ATTR_WHITESPACE = /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g\n\nfunction 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(ATTR_WHITESPACE, '')\n .match(\n new RegExp(\n // eslint-disable-next-line no-useless-escape\n `^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\\-]+(?:[^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 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 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 => !!url,\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 default: this.options.HTMLAttributes.target,\n },\n rel: {\n default: this.options.HTMLAttributes.rel,\n },\n class: {\n default: this.options.HTMLAttributes.class,\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 [\n 'a',\n mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }),\n 0,\n ]\n }\n\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n addCommands() {\n return {\n setLink:\n attributes => ({ chain }) => {\n return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()\n },\n\n toggleLink:\n attributes => ({ chain }) => {\n return chain()\n .toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n .setMeta('preventAutolink', true)\n .run()\n },\n\n unsetLink:\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 // Prosemirror replaces zero-width non-joiner characters\n // with Object Replacement Character from unicode.\n // Therefore, linkifyjs does not recognize the\n // link properly. We replace these characters\n // with regular spaces to fix this issue.\n const links = find(text.replaceAll('\\uFFFC', ' ')).filter(\n item => 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 => 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 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 => 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 if (this.options.openOnClick === true) {\n plugins.push(\n clickHandler({\n type: this.type,\n }),\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 }),\n )\n }\n\n return plugins\n },\n})\n","import {\n combineTransactionSteps,\n findChildrenInRange,\n getChangedRanges,\n getMarksBetween,\n NodeWithPos,\n} from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { MultiToken, tokenize } from 'linkifyjs'\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 = 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 => transaction.getMeta('preventAutolink'))\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 (\n nodesInChangedRanges.length\n // We want to make sure to include the block seperator argument to treat hard breaks like spaces.\n && newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ').endsWith(' ')\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.split(' ').filter(s => s !== '')\n\n if (wordsBeforeWhitespace.length <= 0) {\n return false\n }\n\n const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]\n const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)\n\n if (!lastWordBeforeSpace) {\n return false\n }\n\n const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))\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(\n link.from,\n link.to,\n newState.schema.marks.code,\n )\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 (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {\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","import { getAttributes } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\ntype ClickHandlerOptions = {\n type: MarkType;\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 a = event.target as HTMLElement\n const els = []\n\n while (a.nodeName !== 'DIV') {\n els.push(a)\n a = a.parentNode as HTMLElement\n }\n\n if (!els.find(value => value.nodeName === 'A')) {\n return false\n }\n\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLAnchorElement)\n\n const href = link?.href ?? attrs.href\n const target = link?.target ?? attrs.target\n\n if (link && href) {\n window.open(href, target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { MarkType } from '@tiptap/pm/model'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor\n defaultProtocol: string\n type: MarkType\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 { 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(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\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"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/extension-link",
|
|
3
3
|
"description": "link extension for tiptap",
|
|
4
|
-
"version": "3.0.0-next.
|
|
4
|
+
"version": "3.0.0-next.2",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"linkifyjs": "^4.1.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@tiptap/core": "^3.0.0-next.
|
|
35
|
-
"@tiptap/pm": "^3.0.0-next.
|
|
34
|
+
"@tiptap/core": "^3.0.0-next.2",
|
|
35
|
+
"@tiptap/pm": "^3.0.0-next.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@tiptap/core": "^3.0.0-next.1",
|
package/src/helpers/autolink.ts
CHANGED
|
@@ -35,6 +35,7 @@ type AutolinkOptions = {
|
|
|
35
35
|
type: MarkType
|
|
36
36
|
defaultProtocol: string
|
|
37
37
|
validate: (url: string) => boolean
|
|
38
|
+
shouldAutoLink: (url: string) => boolean
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -144,6 +145,8 @@ export function autolink(options: AutolinkOptions): Plugin {
|
|
|
144
145
|
})
|
|
145
146
|
// validate link
|
|
146
147
|
.filter(link => options.validate(link.value))
|
|
148
|
+
// check whether should autolink
|
|
149
|
+
.filter(link => options.shouldAutoLink(link.value))
|
|
147
150
|
// Add link mark.
|
|
148
151
|
.forEach(link => {
|
|
149
152
|
if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {
|
|
@@ -32,7 +32,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const attrs = getAttributes(view.state, options.type.name)
|
|
35
|
-
const link = (event.target as
|
|
35
|
+
const link = (event.target as HTMLAnchorElement)
|
|
36
36
|
|
|
37
37
|
const href = link?.href ?? attrs.href
|
|
38
38
|
const target = link?.target ?? attrs.target
|
package/src/link.ts
CHANGED
|
@@ -38,46 +38,87 @@ export interface LinkOptions {
|
|
|
38
38
|
* @default true
|
|
39
39
|
* @example false
|
|
40
40
|
*/
|
|
41
|
-
autolink: boolean
|
|
41
|
+
autolink: boolean;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* An array of custom protocols to be registered with linkifyjs.
|
|
45
45
|
* @default []
|
|
46
46
|
* @example ['ftp', 'git']
|
|
47
47
|
*/
|
|
48
|
-
protocols: Array<LinkProtocolOptions | string
|
|
48
|
+
protocols: Array<LinkProtocolOptions | string>;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Default protocol to use when no protocol is specified.
|
|
52
52
|
* @default 'http'
|
|
53
53
|
*/
|
|
54
|
-
defaultProtocol: string
|
|
54
|
+
defaultProtocol: string;
|
|
55
55
|
/**
|
|
56
56
|
* If enabled, links will be opened on click.
|
|
57
57
|
* @default true
|
|
58
58
|
* @example false
|
|
59
59
|
*/
|
|
60
|
-
openOnClick: boolean | DeprecatedOpenWhenNotEditable
|
|
60
|
+
openOnClick: boolean | DeprecatedOpenWhenNotEditable;
|
|
61
61
|
/**
|
|
62
62
|
* Adds a link to the current selection if the pasted content only contains an url.
|
|
63
63
|
* @default true
|
|
64
64
|
* @example false
|
|
65
65
|
*/
|
|
66
|
-
linkOnPaste: boolean
|
|
66
|
+
linkOnPaste: boolean;
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* HTML attributes to add to the link element.
|
|
70
70
|
* @default {}
|
|
71
71
|
* @example { class: 'foo' }
|
|
72
72
|
*/
|
|
73
|
-
HTMLAttributes: Record<string, any
|
|
73
|
+
HTMLAttributes: Record<string, any>;
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
+
* @deprecated Use the `shouldAutoLink` option instead.
|
|
76
77
|
* A validation function that modifies link verification for the auto linker.
|
|
77
78
|
* @param url - The url to be validated.
|
|
78
79
|
* @returns - True if the url is valid, false otherwise.
|
|
79
80
|
*/
|
|
80
|
-
validate: (url: string) => boolean
|
|
81
|
+
validate: (url: string) => boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
|
85
|
+
* Only modify this if you know what you're doing.
|
|
86
|
+
*
|
|
87
|
+
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
|
91
|
+
* return url.startsWith('./') || defaultValidate(url)
|
|
92
|
+
* }
|
|
93
|
+
*/
|
|
94
|
+
isAllowedUri: (
|
|
95
|
+
/**
|
|
96
|
+
* The URL to be validated.
|
|
97
|
+
*/
|
|
98
|
+
url: string,
|
|
99
|
+
ctx: {
|
|
100
|
+
/**
|
|
101
|
+
* The default validation function.
|
|
102
|
+
*/
|
|
103
|
+
defaultValidate: (url: string) => boolean;
|
|
104
|
+
/**
|
|
105
|
+
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
|
106
|
+
*/
|
|
107
|
+
protocols: Array<LinkProtocolOptions | string>;
|
|
108
|
+
/**
|
|
109
|
+
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
|
110
|
+
*/
|
|
111
|
+
defaultProtocol: string;
|
|
112
|
+
}
|
|
113
|
+
) => boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Determines whether a valid link should be automatically linked in the content.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} url - The URL that has already been validated.
|
|
119
|
+
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
|
120
|
+
*/
|
|
121
|
+
shouldAutoLink: (url: string) => boolean;
|
|
81
122
|
}
|
|
82
123
|
|
|
83
124
|
declare module '@tiptap/core' {
|
|
@@ -88,29 +129,73 @@ declare module '@tiptap/core' {
|
|
|
88
129
|
* @param attributes The link attributes
|
|
89
130
|
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
|
90
131
|
*/
|
|
91
|
-
setLink: (attributes: {
|
|
132
|
+
setLink: (attributes: {
|
|
133
|
+
href: string;
|
|
134
|
+
target?: string | null;
|
|
135
|
+
rel?: string | null;
|
|
136
|
+
class?: string | null;
|
|
137
|
+
}) => ReturnType;
|
|
92
138
|
/**
|
|
93
139
|
* Toggle a link mark
|
|
94
140
|
* @param attributes The link attributes
|
|
95
141
|
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
|
96
142
|
*/
|
|
97
|
-
toggleLink: (attributes: {
|
|
143
|
+
toggleLink: (attributes: {
|
|
144
|
+
href: string;
|
|
145
|
+
target?: string | null;
|
|
146
|
+
rel?: string | null;
|
|
147
|
+
class?: string | null;
|
|
148
|
+
}) => ReturnType;
|
|
98
149
|
/**
|
|
99
150
|
* Unset a link mark
|
|
100
151
|
* @example editor.commands.unsetLink()
|
|
101
152
|
*/
|
|
102
|
-
unsetLink: () => ReturnType
|
|
103
|
-
}
|
|
153
|
+
unsetLink: () => ReturnType;
|
|
154
|
+
};
|
|
104
155
|
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
158
|
// From DOMPurify
|
|
108
159
|
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js
|
|
109
|
-
|
|
110
|
-
const
|
|
160
|
+
// eslint-disable-next-line no-control-regex
|
|
161
|
+
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
|
|
162
|
+
|
|
163
|
+
function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
|
|
164
|
+
const allowedProtocols: string[] = [
|
|
165
|
+
'http',
|
|
166
|
+
'https',
|
|
167
|
+
'ftp',
|
|
168
|
+
'ftps',
|
|
169
|
+
'mailto',
|
|
170
|
+
'tel',
|
|
171
|
+
'callto',
|
|
172
|
+
'sms',
|
|
173
|
+
'cid',
|
|
174
|
+
'xmpp',
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
if (protocols) {
|
|
178
|
+
protocols.forEach(protocol => {
|
|
179
|
+
const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
|
|
180
|
+
|
|
181
|
+
if (nextProtocol) {
|
|
182
|
+
allowedProtocols.push(nextProtocol)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
111
186
|
|
|
112
|
-
|
|
113
|
-
|
|
187
|
+
return (
|
|
188
|
+
!uri
|
|
189
|
+
|| uri
|
|
190
|
+
.replace(ATTR_WHITESPACE, '')
|
|
191
|
+
.match(
|
|
192
|
+
new RegExp(
|
|
193
|
+
// eslint-disable-next-line no-useless-escape
|
|
194
|
+
`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`,
|
|
195
|
+
'i',
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
114
199
|
}
|
|
115
200
|
|
|
116
201
|
/**
|
|
@@ -127,6 +212,13 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
127
212
|
exitable: true,
|
|
128
213
|
|
|
129
214
|
onCreate() {
|
|
215
|
+
if (this.options.validate && !this.options.shouldAutoLink) {
|
|
216
|
+
// Copy the validate function to the shouldAutoLink option
|
|
217
|
+
this.options.shouldAutoLink = this.options.validate
|
|
218
|
+
console.warn(
|
|
219
|
+
'The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.',
|
|
220
|
+
)
|
|
221
|
+
}
|
|
130
222
|
this.options.protocols.forEach(protocol => {
|
|
131
223
|
if (typeof protocol === 'string') {
|
|
132
224
|
registerCustomProtocol(protocol)
|
|
@@ -156,7 +248,9 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
156
248
|
rel: 'noopener noreferrer nofollow',
|
|
157
249
|
class: null,
|
|
158
250
|
},
|
|
251
|
+
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
|
159
252
|
validate: url => !!url,
|
|
253
|
+
shouldAutoLink: url => !!url,
|
|
160
254
|
}
|
|
161
255
|
},
|
|
162
256
|
|
|
@@ -181,25 +275,44 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
181
275
|
},
|
|
182
276
|
|
|
183
277
|
parseHTML() {
|
|
184
|
-
return [
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
tag: 'a[href]',
|
|
281
|
+
getAttrs: dom => {
|
|
282
|
+
const href = (dom as HTMLElement).getAttribute('href')
|
|
283
|
+
|
|
284
|
+
// prevent XSS attacks
|
|
285
|
+
if (
|
|
286
|
+
!href
|
|
287
|
+
|| !this.options.isAllowedUri(href, {
|
|
288
|
+
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
|
289
|
+
protocols: this.options.protocols,
|
|
290
|
+
defaultProtocol: this.options.defaultProtocol,
|
|
291
|
+
})
|
|
292
|
+
) {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
return null
|
|
296
|
+
},
|
|
194
297
|
},
|
|
195
|
-
|
|
298
|
+
]
|
|
196
299
|
},
|
|
197
300
|
|
|
198
301
|
renderHTML({ HTMLAttributes }) {
|
|
199
302
|
// prevent XSS attacks
|
|
200
|
-
if (
|
|
303
|
+
if (
|
|
304
|
+
!this.options.isAllowedUri(HTMLAttributes.href, {
|
|
305
|
+
defaultValidate: href => !!isAllowedUri(href, this.options.protocols),
|
|
306
|
+
protocols: this.options.protocols,
|
|
307
|
+
defaultProtocol: this.options.defaultProtocol,
|
|
308
|
+
})
|
|
309
|
+
) {
|
|
201
310
|
// strip out the href
|
|
202
|
-
return [
|
|
311
|
+
return [
|
|
312
|
+
'a',
|
|
313
|
+
mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }),
|
|
314
|
+
0,
|
|
315
|
+
]
|
|
203
316
|
}
|
|
204
317
|
|
|
205
318
|
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
|
@@ -237,17 +350,29 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
237
350
|
const foundLinks: PasteRuleMatch[] = []
|
|
238
351
|
|
|
239
352
|
if (text) {
|
|
240
|
-
const {
|
|
241
|
-
|
|
353
|
+
const { protocols, defaultProtocol } = this.options
|
|
354
|
+
// Prosemirror replaces zero-width non-joiner characters
|
|
355
|
+
// with Object Replacement Character from unicode.
|
|
356
|
+
// Therefore, linkifyjs does not recognize the
|
|
357
|
+
// link properly. We replace these characters
|
|
358
|
+
// with regular spaces to fix this issue.
|
|
359
|
+
const links = find(text.replaceAll('\uFFFC', ' ')).filter(
|
|
360
|
+
item => item.isLink
|
|
361
|
+
&& this.options.isAllowedUri(item.value, {
|
|
362
|
+
defaultValidate: href => !!isAllowedUri(href, protocols),
|
|
363
|
+
protocols,
|
|
364
|
+
defaultProtocol,
|
|
365
|
+
}),
|
|
366
|
+
)
|
|
242
367
|
|
|
243
368
|
if (links.length) {
|
|
244
|
-
links.forEach(link =>
|
|
369
|
+
links.forEach(link => foundLinks.push({
|
|
245
370
|
text: link.value,
|
|
246
371
|
data: {
|
|
247
372
|
href: link.href,
|
|
248
373
|
},
|
|
249
374
|
index: link.start,
|
|
250
|
-
}))
|
|
375
|
+
}))
|
|
251
376
|
}
|
|
252
377
|
}
|
|
253
378
|
|
|
@@ -265,13 +390,19 @@ export const Link = Mark.create<LinkOptions>({
|
|
|
265
390
|
|
|
266
391
|
addProseMirrorPlugins() {
|
|
267
392
|
const plugins: Plugin[] = []
|
|
393
|
+
const { protocols, defaultProtocol } = this.options
|
|
268
394
|
|
|
269
395
|
if (this.options.autolink) {
|
|
270
396
|
plugins.push(
|
|
271
397
|
autolink({
|
|
272
398
|
type: this.type,
|
|
273
399
|
defaultProtocol: this.options.defaultProtocol,
|
|
274
|
-
validate: this.options.
|
|
400
|
+
validate: url => this.options.isAllowedUri(url, {
|
|
401
|
+
defaultValidate: href => !!isAllowedUri(href, protocols),
|
|
402
|
+
protocols,
|
|
403
|
+
defaultProtocol,
|
|
404
|
+
}),
|
|
405
|
+
shouldAutoLink: this.options.shouldAutoLink,
|
|
275
406
|
}),
|
|
276
407
|
)
|
|
277
408
|
}
|