// src/link.ts import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core"; import { find as find2, registerCustomProtocol, reset } from "linkifyjs"; // src/helpers/autolink.ts import { combineTransactionSteps, findChildrenInRange, getChangedRanges, getMarksBetween } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { tokenize } from "linkifyjs"; // src/helpers/whitespace.ts var UNICODE_WHITESPACE_PATTERN = "[\0- \xA0\u1680\u180E\u2000-\u2029\u205F\u3000]"; var UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN); var UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`); var UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, "g"); // src/helpers/autolink.ts function isValidLinkStructure(tokens) { if (tokens.length === 1) { return tokens[0].isLink; } if (tokens.length === 3 && tokens[1].isLink) { return ["()", "[]"].includes(tokens[0].value + tokens[2].value); } return false; } function autolink(options) { return new Plugin({ key: new PluginKey("autolink"), appendTransaction: (transactions, oldState, newState) => { const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc); const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink")); if (!docChanges || preventAutolink) { return; } const { tr } = newState; const transform = combineTransactionSteps(oldState.doc, [...transactions]); const changes = getChangedRanges(transform); changes.forEach(({ newRange }) => { const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, (node) => node.isTextblock); let textBlock; let textBeforeWhitespace; if (nodesInChangedRanges.length > 1) { textBlock = nodesInChangedRanges[0]; textBeforeWhitespace = newState.doc.textBetween( textBlock.pos, textBlock.pos + textBlock.node.nodeSize, void 0, " " ); } else if (nodesInChangedRanges.length) { const endText = newState.doc.textBetween(newRange.from, newRange.to, " ", " "); if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) { return; } textBlock = nodesInChangedRanges[0]; textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, void 0, " "); } if (textBlock && textBeforeWhitespace) { const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean); if (wordsBeforeWhitespace.length <= 0) { return false; } const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]; const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace); if (!lastWordBeforeSpace) { return false; } const linksBeforeSpace = tokenize(lastWordBeforeSpace).map((t) => t.toObject(options.defaultProtocol)); if (!isValidLinkStructure(linksBeforeSpace)) { return false; } linksBeforeSpace.filter((link) => link.isLink).map((link) => ({ ...link, from: lastWordAndBlockOffset + link.start + 1, to: lastWordAndBlockOffset + link.end + 1 })).filter((link) => { if (!newState.schema.marks.code) { return true; } return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code); }).filter((link) => options.validate(link.value)).filter((link) => options.shouldAutoLink(link.value)).forEach((link) => { if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) { return; } tr.addMark( link.from, link.to, options.type.create({ href: link.href }) ); }); } }); if (!tr.steps.length) { return; } return tr; } }); } // src/helpers/clickHandler.ts import { getAttributes } from "@tiptap/core"; import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state"; function clickHandler(options) { return new Plugin2({ key: new PluginKey2("handleClickLink"), props: { handleClick: (view, pos, event) => { var _a, _b; if (event.button !== 0) { return false; } if (!view.editable) { return false; } let link = null; if (event.target instanceof HTMLAnchorElement) { link = event.target; } else { const target = event.target; if (!target) { return false; } const root = options.editor.view.dom; link = target.closest("a"); if (link && !root.contains(link)) { link = null; } } if (!link) { return false; } let handled = false; if (options.enableClickSelection) { const commandResult = options.editor.commands.extendMarkRange(options.type.name); handled = commandResult; } if (options.openOnClick) { const attrs = getAttributes(view.state, options.type.name); const href = (_a = link.href) != null ? _a : attrs.href; const target = (_b = link.target) != null ? _b : attrs.target; if (href) { window.open(href, target); handled = true; } } return handled; } } }); } // src/helpers/pasteHandler.ts import { Plugin as Plugin3, PluginKey as PluginKey3 } from "@tiptap/pm/state"; import { find } from "linkifyjs"; function pasteHandler(options) { return new Plugin3({ key: new PluginKey3("handlePasteLink"), props: { handlePaste: (view, _event, slice) => { const { shouldAutoLink } = options; const { state } = view; const { selection } = state; const { empty } = selection; if (empty) { return false; } let textContent = ""; slice.content.forEach((node) => { textContent += node.textContent; }); const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find( (item) => item.isLink && item.value === textContent ); if (!textContent || !link || shouldAutoLink !== void 0 && !shouldAutoLink(link.value)) { return false; } return options.editor.commands.setMark(options.type, { href: link.href }); } } }); } // src/link.ts var pasteRegex = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi; function isAllowedUri(uri, protocols) { const allowedProtocols = ["http", "https", "ftp", "ftps", "mailto", "tel", "callto", "sms", "cid", "xmpp"]; if (protocols) { protocols.forEach((protocol) => { const nextProtocol = typeof protocol === "string" ? protocol : protocol.scheme; if (nextProtocol) { allowedProtocols.push(nextProtocol); } }); } return !uri || uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, "").match( new RegExp( // eslint-disable-next-line no-useless-escape `^(?:(?:${allowedProtocols.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`, "i" ) ); } var Link = Mark.create({ name: "link", priority: 1e3, keepOnSplit: false, exitable: true, onCreate() { if (this.options.validate && !this.options.shouldAutoLink) { this.options.shouldAutoLink = this.options.validate; console.warn("The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead."); } this.options.protocols.forEach((protocol) => { if (typeof protocol === "string") { registerCustomProtocol(protocol); return; } registerCustomProtocol(protocol.scheme, protocol.optionalSlashes); }); }, onDestroy() { reset(); }, inclusive() { return this.options.autolink; }, addOptions() { return { openOnClick: true, enableClickSelection: false, linkOnPaste: true, autolink: true, protocols: [], defaultProtocol: "http", HTMLAttributes: { target: "_blank", rel: "noopener noreferrer nofollow", class: null }, isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols), validate: (url) => !!url, shouldAutoLink: (url) => { const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url); const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url); if (hasProtocol || hasMaybeProtocol && !url.includes("@")) { return true; } const urlWithoutUserinfo = url.includes("@") ? url.split("@").pop() : url; const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]; if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) { return false; } if (!/\./.test(hostname)) { return false; } return true; } }; }, addAttributes() { return { href: { default: null, parseHTML(element) { return element.getAttribute("href"); } }, target: { default: this.options.HTMLAttributes.target }, rel: { default: this.options.HTMLAttributes.rel }, class: { default: this.options.HTMLAttributes.class }, title: { default: null } }; }, parseHTML() { return [ { tag: "a[href]", getAttrs: (dom) => { const href = dom.getAttribute("href"); if (!href || !this.options.isAllowedUri(href, { defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) { return false; } return null; } } ]; }, renderHTML({ HTMLAttributes }) { if (!this.options.isAllowedUri(HTMLAttributes.href, { defaultValidate: (href) => !!isAllowedUri(href, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) { return ["a", mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0]; } return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; }, markdownTokenName: "link", parseMarkdown: (token, helpers) => { return helpers.applyMark("link", helpers.parseInline(token.tokens || []), { href: token.href, title: token.title || null }); }, renderMarkdown: (node, h) => { var _a, _b, _c, _d; const href = (_b = (_a = node.attrs) == null ? void 0 : _a.href) != null ? _b : ""; const title = (_d = (_c = node.attrs) == null ? void 0 : _c.title) != null ? _d : ""; const text = h.renderChildren(node); return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`; }, addCommands() { return { setLink: (attributes) => ({ chain }) => { const { href } = attributes; if (!this.options.isAllowedUri(href, { defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) { return false; } return chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run(); }, toggleLink: (attributes) => ({ chain }) => { const { href } = attributes || {}; if (href && !this.options.isAllowedUri(href, { defaultValidate: (url) => !!isAllowedUri(url, this.options.protocols), protocols: this.options.protocols, defaultProtocol: this.options.defaultProtocol })) { return false; } return chain().toggleMark(this.name, attributes, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run(); }, unsetLink: () => ({ chain }) => { return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run(); } }; }, addPasteRules() { return [ markPasteRule({ find: (text) => { const foundLinks = []; if (text) { const { protocols, defaultProtocol } = this.options; const links = find2(text).filter( (item) => item.isLink && this.options.isAllowedUri(item.value, { defaultValidate: (href) => !!isAllowedUri(href, protocols), protocols, defaultProtocol }) ); if (links.length) { links.forEach((link) => { if (!this.options.shouldAutoLink(link.value)) { return; } foundLinks.push({ text: link.value, data: { href: link.href }, index: link.start }); }); } } return foundLinks; }, type: this.type, getAttributes: (match) => { var _a; return { href: (_a = match.data) == null ? void 0 : _a.href }; } }) ]; }, addProseMirrorPlugins() { const plugins = []; const { protocols, defaultProtocol } = this.options; if (this.options.autolink) { plugins.push( autolink({ type: this.type, defaultProtocol: this.options.defaultProtocol, validate: (url) => this.options.isAllowedUri(url, { defaultValidate: (href) => !!isAllowedUri(href, protocols), protocols, defaultProtocol }), shouldAutoLink: this.options.shouldAutoLink }) ); } plugins.push( clickHandler({ type: this.type, editor: this.editor, openOnClick: this.options.openOnClick === "whenNotEditable" ? true : this.options.openOnClick, enableClickSelection: this.options.enableClickSelection }) ); if (this.options.linkOnPaste) { plugins.push( pasteHandler({ editor: this.editor, defaultProtocol: this.options.defaultProtocol, type: this.type, shouldAutoLink: this.options.shouldAutoLink }) ); } return plugins; } }); // src/index.ts var index_default = Link; export { Link, index_default as default, isAllowedUri, pasteRegex }; //# sourceMappingURL=index.js.map