123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- // 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 /^<a[>\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:<a href="//example.com/">//example.com/</a>
- 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)
- }
- }
- }
- }
|