/
index.js
175 lines (171 loc) · 7.26 KB
/
index.js
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/*!
* Keyword-linker v1.2.0
* http://wuliupo.github.io/keyword-linker
*
* Copyright 2018 Liupo Wu
* Released under the MIT license
*
* Date: 2018-12-18
*/
(function(global, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = global.document ? factory(global) : function(win) {
return factory(win);
};
} else {
factory(global);
}
}(typeof window !== 'undefined' ? window : this, function(win) {
if (!win || !win.document && !win.load) { // browser or cheerio
throw new Error('KeywordLinker requires window with document, or using cheerio');
}
var DEFAULT_CONFIG = {
max: 20, // global max replacement count
ignoreTags: ['a', 'img', 'textarea', 'select', 'input', 'button', 'script', 'style', 'link', 'pre', 'video', 'svg', 'canvas', 'audio', 'head', 'iframe', 'frame', 'area', 'meta', 'embed', 'object', 'applet', 'xml', 'xmp', 'plaintext']
};
var KeywordLinker = function(keywords, config) {
config = config || {};
var replacement = config.replacement;
if (!replacement || !keywords || !keywords.length) {
throw new Error('KeywordLinker requires parameters: keywords(array), replacement(string or function)');
}
if (typeof replacement === 'string') {
var placeholder = config.placeholder;
if (placeholder && (typeof placeholder === 'string')) {
placeholder = new RegExp(placeholder, 'g');
}
if (!placeholder || !(placeholder instanceof RegExp)) {
throw new Error('KeywordLinker string replacement parameter must have a peer placeholder(string or regexp)');
}
this.replacement = function(s0) {
return replacement.replace(placeholder, function() {
return s0;
});
}
} else if (typeof that.replacement === 'function') {
this.replacement = replacement;
} else {
throw new Error('KeywordLinker replacement parameter must be string or function');
}
this.config = Object.assign(DEFAULT_CONFIG, config);
this.keywords = keywords;
}
KeywordLinker.prototype = {
addLink2String: function(content) {
var dom, doc = win.document;
if (doc) {
dom = doc.createElement('div');
dom.innerHTML = content;
dom.style.display = 'none';
doc.documentElement.appendChild(dom);
this.addLink2Dom(dom);
content = dom.innerHTML;
doc.documentElement.removeChild(dom);
} else if (win.load) { // cheerio environment
dom = win('<div>' + content + '</div>', undefined, undefined, { decodeEntities: false });
this.addLink2Dom(dom[0]);
content = dom.html();
} else {
throw new Error('KeywordLinker.addLink2String need a DOM environment');
}
return content;
},
addLink2Dom: function(dom) {
var keywordArr = [];
var keywordMap = {};
for (var i = 0; i < this.keywords.length; i++) {
var item = Object.assign({}, this.keywords[i]);
keywordArr.push(item);
keywordMap[item.text] = item;
}
keywordArr.sort(function(a, b) {
return a.priority < b.priority;
});
this.keywordArr = keywordArr;
this.keywordMap = keywordMap;
this.max = this.config.max;
this.__initRegExp();
this.__addLink2Dom(dom);
},
__initRegExp: function() {
if (this.max <= 0 || !this.keywordArr) {
this.keywordRegExp = null;
return;
}
var rst = [];
for (var i = 0; i < this.keywordArr.length; i++) {
var item = this.keywordArr[i];
if (item.max > 0 && item.text && rst.indexOf(item.text) < 0) {
rst.push(item.text);
}
}
rst = rst.join('|');
this.keywordRegExp = rst && new RegExp(rst, 'g');
},
__addLink2Dom: function(dom) {
if (!dom || !dom.childNodes || !this.keywordRegExp) {
return dom;
}
for (var i = 0; i < dom.childNodes.length; i++) {
if (!this.keywordRegExp) {
return dom;
}
var node = dom.childNodes[i];
var nodeName = (node.nodeName || node.tagName || node.name || node.type || '').toLowerCase();
if (nodeName === '#text' || nodeName === 'text' || node.nodeType == 3) {
var text = node.nodeValue || '';
if (!text.trim()) {
continue;
}
var newText = this.__replaceText(text);
if (newText !== text) {
if (win.load) { // cheerio environment
win(node).replaceWith('<span class="hot-wrap">' + newText + '</span>');
} else if (win.document) {
// 文本节点,替换为带超链接的复杂dom
var span = win.document.createElement('span');
span.className = 'hot-wrap';
span.innerHTML = newText;
dom.replaceChild(span, node);
}
}
} else if (nodeName && this.config.ignoreTags.indexOf(nodeName) < 0 && nodeName.indexOf('#') !== 0) {
this.__addLink2Dom(node);
}
}
},
__replaceText: function(text) {
if (!this.keywordRegExp || !text || !text.trim()) {
return text;
}
var breaker = 0; // 如果一个关键词被匹配完了,后面的正则表达式要重新计算,不再进行后续替换
var that = this;
text = text.replace(this.keywordRegExp, function(s0) {
var item = that.keywordMap[s0]; // 替换过以后,数量减1
if (breaker) {
breaker++;
return s0;
}
if (--that.max < 1 || --item.max < 1) {
breaker++;
}
return that.replacement(s0);
});
if (breaker) { // 只要有关键词被用完,就得重新计算正则表达式
this.__initRegExp();
if (breaker > 1) { // 当前文本后面还有其他匹配的
var lastBracket = text.lastIndexOf('>') + 1;
if (lastBracket) {
if (this.keywordRegExp) {
// 中止的部分,前面的直接返回,后面的需要再次替换
text = text.substring(0, lastBracket) + this.__replaceText(text.substring(lastBracket));
}
}
}
}
return text;
}
};
win.KeywordLinker = KeywordLinker;
return KeywordLinker;
}));