import { Node, nodePasteRule, type PasteRuleMatch } from '@tiptap/core' import type { Node as ProsemirrorNode } from '@tiptap/pm/model' import type { MarkdownSerializerState } from 'prosemirror-markdown' export const LINK_REGEX = /^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]*/gi export const createPasteRuleMatch = >( match: RegExpMatchArray, data: T, ): PasteRuleMatch => ({ index: match.index!, replaceWith: match[2], text: match[0], match, data }) export interface LinkAttributes { url: string } declare module '@tiptap/core' { interface Commands { link: { insertLink: (options: { url: string }) => ReturnType } } } export const LinkExtension = Node.create({ atom: true, name: 'link', group: 'inline', inline: true, selectable: true, draggable: true, priority: 1000, addAttributes() { return { url: { default: null }, } }, renderHTML(props) { return ['div', { 'data-url': props.node.attrs.url }] }, renderText(props) { return props.node.attrs.url }, addStorage() { return { markdown: { serialize(state: MarkdownSerializerState, node: ProsemirrorNode) { state.write(node.attrs.url) }, parse: {}, }, } }, addCommands() { return { insertLink: ({ url }) => ({ commands }) => { return commands.insertContent( { type: this.name, attrs: { url } }, { updateSelection: false, }, ) }, } }, addPasteRules() { return [ nodePasteRule({ type: this.type, getAttributes: (match) => match.data, find: (text) => { const matches = [] for (const match of text.matchAll(LINK_REGEX)) { try { matches.push(createPasteRuleMatch(match, { url: match[0] })) } catch (e) { continue } } return matches }, }), ] }, })