import { Node, InputRule } from '@tiptap/core'; import { VueNodeViewRenderer } from '@tiptap/vue-2'; import ReferenceWrapper from '../components/wrappers/reference.vue'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; const getAnchor = (element) => { if (element.nodeName === 'A') return element; return element.querySelector('a'); }; const findReference = (editor, reference) => { let position; editor.view.state.doc.descendants((descendant, pos) => { if (descendant.isText && descendant.text.includes(reference)) { position = pos + descendant.text.indexOf(reference); return false; } return true; }); return position; }; export default Node.create({ name: 'reference', inline: true, group: 'inline', atom: true, addOptions() { return { assetResolver: null, }; }, addAttributes() { return { className: { default: null, parseHTML: (element) => getAnchor(element).className, }, referenceType: { default: null, parseHTML: (element) => getAnchor(element).dataset.referenceType, }, originalText: { default: null, parseHTML: (element) => getAnchor(element).dataset.original, }, href: { default: null, parseHTML: (element) => getAnchor(element).getAttribute('href'), }, text: { default: null, parseHTML: (element) => getAnchor(element).textContent, }, }; }, addCommands() { return { insertQuickAction: () => ({ commands }) => commands.insertContent('

/

'), }; }, addInputRules() { const { editor } = this; const { assetResolver } = this.options; const referenceInputRegex = /(?:^|\s)([\w/]*([!&#])\d+(\+?s?))(?:\s|\n)/m; const referenceTypes = { '#': 'issue', '!': 'merge_request', '&': 'epic', }; return [ new InputRule({ find: referenceInputRegex, handler: async ({ match }) => { const [, referenceId, referenceSymbol, expansionType] = match; const referenceType = referenceTypes[referenceSymbol]; const { href, text, expandedText, fullyExpandedText, } = await assetResolver.resolveReference(referenceId); if (!text) return; let referenceText = text; if (expansionType === '+') referenceText = expandedText; if (expansionType === '+s') referenceText = fullyExpandedText; const position = findReference(editor, referenceId); if (!position) return; editor.view.dispatch( editor.state.tr.replaceWith(position, position + referenceId.length, [ this.type.create({ referenceType, originalText: referenceId, href, text: referenceText, }), ]), ); }, }), ]; }, parseHTML() { return [ { tag: 'a.gfm:not([data-link=true])', priority: PARSE_HTML_PRIORITY_HIGHEST, }, ]; }, renderHTML({ node }) { return [ 'gl-reference', { 'data-reference-type': node.attrs.referenceType, 'data-original-text': node.attrs.originalText, href: node.attrs.href, text: node.attrs.text, }, node.attrs.text, ]; }, addNodeView() { return new VueNodeViewRenderer(ReferenceWrapper); }, });