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)
}
}
}
}
|