UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

134 lines (125 loc) 4.94 kB
// File has been copied to packages/editor/editor-plugin-ai/src/provider/markdown-transformer/md/linkify-md-plugin.ts // If changes are made to this file, please make the same update in the linked file. // See https://product-fabric.atlassian.net/browse/ED-3097 for why this file exists. import LinkifyIt from 'linkify-it'; import { linkifyMatch } from '@atlaskit/adf-schema'; import { getBooleanFF } from '@atlaskit/platform-feature-flags'; import { findFilepaths, isLinkInMatches, shouldAutoLinkifyMatch } from '../../utils'; // modified version of the original markdown-it Linkify plugin // https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/linkify.js const arrayReplaceAt = (src, pos, newElements) => { return [].concat(src.slice(0, pos), newElements, src.slice(pos + 1)); }; const isLinkOpen = str => { return /^<a[>\s]/i.test(str); }; const isLinkClose = str => { return /^<\/a\s*>/i.test(str); }; const linkify = state => { const blockTokens = state.tokens; const linkify = new LinkifyIt(); for (let j = 0, l = blockTokens.length; j < l; j++) { if (blockTokens[j].type !== 'inline' || !linkify.pretest(blockTokens[j].content)) { continue; } let tokens = blockTokens[j].children; let htmlLinkLevel = 0; // We scan from the end, to keep position when new tags added. // Use reversed logic in links start/end match for (let i = tokens.length - 1; i >= 0; i--) { const currentToken = tokens[i]; // Skip content of markdown links if (currentToken.type === 'link_close') { i--; while (tokens[i].level !== currentToken.level && tokens[i].type !== 'link_open') { i--; } continue; } // Skip content of html tag links if (currentToken.type === 'html_inline') { if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) { htmlLinkLevel--; } if (isLinkClose(currentToken.content)) { htmlLinkLevel++; } } if (htmlLinkLevel > 0) { continue; } if (currentToken.type === 'text' && linkify.test(currentToken.content)) { const text = currentToken.content; let links = linkifyMatch(text); if (!links.length) { links = linkify.match(text) || []; } if (getBooleanFF('platform.linking-platform.prevent-suspicious-linkification')) { links = links.filter(link => shouldAutoLinkifyMatch(link)); } // Now split string to nodes const nodes = []; let level = currentToken.level; let lastPos = 0; const filepaths = findFilepaths(text); for (let ln = 0; ln < links.length; ln++) { if (isLinkInMatches(links[ln].index, filepaths)) { continue; } const { url } = links[ln]; const fullUrl = state.md.normalizeLink(url); if (!state.md.validateLink(fullUrl)) { continue; } let urlText = links[ln].text; // Linkifier might send raw hostnames like "example.com", where url // starts with domain name. So we prepend http:// in those cases, // and remove it afterwards. // if (!links[ln].schema) { urlText = state.md.normalizeLinkText('http://' + urlText).replace(/^http:\/\//, ''); } else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) { urlText = state.md.normalizeLinkText('mailto:' + urlText).replace(/^mailto:/, ''); } else { urlText = state.md.normalizeLinkText(urlText); } const pos = links[ln].index; if (pos > lastPos) { const token = new state.Token('text', '', 0); token.content = text.slice(lastPos, pos); token.level = level; nodes.push(token); } let token = new state.Token('link_open', 'a', 1); token.attrs = [['href', fullUrl]]; token.level = level++; token.markup = 'linkify'; token.info = 'auto'; nodes.push(token); token = new state.Token('text', '', 0); token.content = urlText; token.level = level; nodes.push(token); token = new state.Token('link_close', 'a', -1); token.level = --level; token.markup = 'linkify'; token.info = 'auto'; nodes.push(token); lastPos = links[ln].lastIndex; } if (lastPos < text.length) { const token = new state.Token('text', '', 0); token.content = text.slice(lastPos); token.level = level; nodes.push(token); } // replace current node blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes); } } } }; export default (md => md.core.ruler.push('custom-linkify', linkify));