summaryrefslogtreecommitdiff
path: root/packages/markdown-it-14.1.0/lib/rules_core/linkify.mjs
blob: a22528038b59d8780b05376a57a26b50011f434d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 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)
      }
    }
  }
}