// Replace link-like texts with link nodes. // // Currently restricted by `md.validateLink()` to http/https/ftp // import { arrayReplaceAt } from '../common/utils.mjs' function isLinkOpen (str) { return /^\s]/i.test(str) } function isLinkClose (str) { return /^<\/a\s*>/i.test(str) } export default function linkify (state) { const blockTokens = state.tokens if (!state.md.options.linkify) { return } for (let j = 0, l = blockTokens.length; j < l; j++) { if (blockTokens[j].type !== 'inline' || !state.md.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' && state.md.linkify.test(currentToken.content)) { const text = currentToken.content let links = state.md.linkify.match(text) // Now split string to nodes const nodes = [] let level = currentToken.level let lastPos = 0 // forbid escape sequence at the start of the string, // this avoids http\://example.com/ from being linkified as // http://example.com/ if (links.length > 0 && links[0].index === 0 && i > 0 && tokens[i - 1].type === 'text_special') { links = links.slice(1) } for (let ln = 0; ln < links.length; ln++) { const url = links[ln].url 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) } const token_o = new state.Token('link_open', 'a', 1) token_o.attrs = [['href', fullUrl]] token_o.level = level++ token_o.markup = 'linkify' token_o.info = 'auto' nodes.push(token_o) const token_t = new state.Token('text', '', 0) token_t.content = urlText token_t.level = level nodes.push(token_t) const token_c = new state.Token('link_close', 'a', -1) token_c.level = --level token_c.markup = 'linkify' token_c.info = 'auto' nodes.push(token_c) 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) } } } }