@ohos-ports/advanced-mark.js 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/jquery.mark.d.ts +235 -0
- package/dist/jquery.mark.es6.d.ts +235 -0
- package/dist/jquery.mark.es6.js +1281 -0
- package/dist/jquery.mark.es6.min.js +8 -0
- package/dist/jquery.mark.js +1681 -0
- package/dist/jquery.mark.min.js +8 -0
- package/dist/mark.d.ts +210 -0
- package/dist/mark.es6.d.ts +210 -0
- package/dist/mark.es6.js +1284 -0
- package/dist/mark.es6.min.js +8 -0
- package/dist/mark.js +1682 -0
- package/dist/mark.min.js +8 -0
- package/package.json +72 -0
- package/src/jquery.js +24 -0
- package/src/jquery_es6.js +23 -0
- package/src/lib/domiterator.js +419 -0
- package/src/lib/mark.js +1817 -0
- package/src/lib/regexpcreator.js +397 -0
- package/src/types/jquery.mark.d.ts +235 -0
- package/src/types/mark.d.ts +210 -0
- package/src/vanilla.js +25 -0
package/src/lib/mark.js
ADDED
|
@@ -0,0 +1,1817 @@
|
|
|
1
|
+
import DOMIterator from './domiterator';
|
|
2
|
+
import RegExpCreator from './regexpcreator';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marks search terms in DOM elements
|
|
6
|
+
* @example
|
|
7
|
+
* new Mark(document.querySelector('.context')).mark('lorem ipsum');
|
|
8
|
+
* @example
|
|
9
|
+
* new Mark(document.querySelector('.context')).markRegExp(/lorem/gmi);
|
|
10
|
+
* @example
|
|
11
|
+
* new Mark('.context').markRanges([{start:10,length:0}]);
|
|
12
|
+
*/
|
|
13
|
+
class Mark {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM
|
|
17
|
+
* element, an array of DOM elements, a NodeList or a selector
|
|
18
|
+
*/
|
|
19
|
+
constructor(ctx) {
|
|
20
|
+
/**
|
|
21
|
+
* The context of the instance. Either a DOM element, an array of DOM
|
|
22
|
+
* elements, a NodeList or a selector
|
|
23
|
+
* @type {HTMLElement|HTMLElement[]|NodeList|string}
|
|
24
|
+
* @access protected
|
|
25
|
+
*/
|
|
26
|
+
this.ctx = ctx;
|
|
27
|
+
/**
|
|
28
|
+
* The array of node names which must be excluded from search
|
|
29
|
+
* @type {array}
|
|
30
|
+
* @access protected
|
|
31
|
+
*/
|
|
32
|
+
this.nodeNames = ['script', 'style', 'title', 'head', 'html'];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef Mark~commonOptions
|
|
37
|
+
* @type {object.<string>}
|
|
38
|
+
* @property {object} [window] - A window object
|
|
39
|
+
* @property {Highlight} [highlight] - A Highlight object
|
|
40
|
+
* @property {string} [element="mark"] - HTML element tag name
|
|
41
|
+
* @property {string} [className] - An optional class name
|
|
42
|
+
* @property {string[]} [exclude] - An array with exclusion selectors.
|
|
43
|
+
* Elements matching those selectors will be ignored
|
|
44
|
+
* @property {boolean} [iframes=false] - Whether to search inside iframes
|
|
45
|
+
* @property {number} [iframesTimeout=5000] - Maximum ms to wait for a load
|
|
46
|
+
* event of an iframe
|
|
47
|
+
* @property {boolean} [acrossElements=false] - Whether to find matches across HTML elements.
|
|
48
|
+
* By default, only matches within single HTML elements will be found
|
|
49
|
+
* @property {Mark~markEachCallback} [each]
|
|
50
|
+
* @property {Mark~markNoMatchCallback} [noMatch]
|
|
51
|
+
* @property {Mark~commonDoneCallback} [done]
|
|
52
|
+
* @property {boolean} [debug=false] - Whether to log messages
|
|
53
|
+
* @property {object} [log=window.console] - Where to log messages (only if debug is true)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Options defined by the user. They will be initialized from one of the
|
|
58
|
+
* public methods. See {@link Mark#mark}, {@link Mark#markRegExp},
|
|
59
|
+
* {@link Mark#markRanges} and {@link Mark#unmark} for option properties.
|
|
60
|
+
* @type {object}
|
|
61
|
+
* @param {object} [val] - An object that will be merged with defaults
|
|
62
|
+
* @access protected
|
|
63
|
+
*/
|
|
64
|
+
set opt(val) {
|
|
65
|
+
if ( !(val && val.window && val.window.document) && typeof window === 'undefined') {
|
|
66
|
+
throw new Error('Mark.js: please provide a window object as an option.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const win = val && val.window || window,
|
|
70
|
+
// eslint-disable-next-line
|
|
71
|
+
highlight = val && val.highlight && val.highlight instanceof Highlight;
|
|
72
|
+
|
|
73
|
+
this._opt = Object.assign({}, {
|
|
74
|
+
'window': win,
|
|
75
|
+
'element': '',
|
|
76
|
+
'className': '',
|
|
77
|
+
'exclude': [],
|
|
78
|
+
'iframes': false,
|
|
79
|
+
'iframesTimeout': 5000,
|
|
80
|
+
'separateWordSearch': true,
|
|
81
|
+
'staticRanges': true,
|
|
82
|
+
'rangeAcrossElements': true,
|
|
83
|
+
'acrossElements': false,
|
|
84
|
+
'ignoreGroups': 0,
|
|
85
|
+
'each': () => {},
|
|
86
|
+
'noMatch': () => {},
|
|
87
|
+
'filter': () => true,
|
|
88
|
+
'done': () => {},
|
|
89
|
+
'debug': false,
|
|
90
|
+
'log': win.console
|
|
91
|
+
}, val);
|
|
92
|
+
|
|
93
|
+
if ( !this._opt.element) {
|
|
94
|
+
this._opt.element = 'mark';
|
|
95
|
+
}
|
|
96
|
+
// shortens a lengthy name
|
|
97
|
+
this.filter = win.NodeFilter;
|
|
98
|
+
// this empty text node used to simplify code
|
|
99
|
+
this.empty = win.document.createTextNode('');
|
|
100
|
+
|
|
101
|
+
if ( !this._opt.highlightName) {
|
|
102
|
+
this._opt.highlightName = 'advanced-markjs';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (highlight) {
|
|
106
|
+
this.rangeArray = [];
|
|
107
|
+
} else {
|
|
108
|
+
this._opt.highlight = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get opt() {
|
|
113
|
+
return this._opt;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* An instance of DOMIterator
|
|
118
|
+
* @type {DOMIterator}
|
|
119
|
+
* @access protected
|
|
120
|
+
*/
|
|
121
|
+
get iterator() {
|
|
122
|
+
// always return new instance in case there were option changes
|
|
123
|
+
return new DOMIterator(this.ctx, this.opt);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Logs a message if log is enabled
|
|
128
|
+
* @param {string} msg - The message to log
|
|
129
|
+
* @param {string} [level="debug"] - The log level, e.g. <code>warn</code>
|
|
130
|
+
* <code>error</code>, <code>debug</code>
|
|
131
|
+
* @access protected
|
|
132
|
+
*/
|
|
133
|
+
log(msg, level = 'debug') {
|
|
134
|
+
if (this.opt.debug) {
|
|
135
|
+
const log = this.opt.log;
|
|
136
|
+
if (typeof log === 'object' && typeof log[level] === 'function') {
|
|
137
|
+
log[level](`mark.js: ${msg}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef Mark~logObject
|
|
144
|
+
* @type {object}
|
|
145
|
+
* @property {string} message - The message
|
|
146
|
+
* @property {object} obj - The object
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Logs errors and info
|
|
151
|
+
* @param {array} array - The array of objects
|
|
152
|
+
*/
|
|
153
|
+
report(array) {
|
|
154
|
+
array.forEach(item => {
|
|
155
|
+
this.log(`${item.text} ${JSON.stringify(item.obj)}`, item.level || 'debug');
|
|
156
|
+
if ( !item.skip) {
|
|
157
|
+
this.opt.noMatch(item.obj);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Splits string into separate words if 'separateWordSearch' option has value 'true' but,
|
|
164
|
+
* if it has string value 'preserveTerms', prevents splitting terms surrounding by double quotes.
|
|
165
|
+
* Removes duplicate or empty entries and sort by the length in descending order.
|
|
166
|
+
* It also initializes termStats object.
|
|
167
|
+
* @param {string|string[]} sv - Search value, either a string or an array of strings
|
|
168
|
+
* @return {object}
|
|
169
|
+
* @access protected
|
|
170
|
+
*/
|
|
171
|
+
getSeachTerms(sv) {
|
|
172
|
+
const search = typeof sv === 'string' ? [sv] : sv,
|
|
173
|
+
separate = this.opt.separateWordSearch,
|
|
174
|
+
array = [],
|
|
175
|
+
termStats = {},
|
|
176
|
+
split = str => {
|
|
177
|
+
str.split(/ +/).forEach(word => add(word));
|
|
178
|
+
},
|
|
179
|
+
add = str => {
|
|
180
|
+
if (str.trim() && !array.includes(str)) {
|
|
181
|
+
array.push(str);
|
|
182
|
+
// initializes term property
|
|
183
|
+
termStats[str] = 0;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
search.forEach(str => {
|
|
188
|
+
if (separate) {
|
|
189
|
+
if (separate === 'preserveTerms') {
|
|
190
|
+
// allows highlight quoted terms no matter how many quotes it contains on each side,
|
|
191
|
+
// e.g. ' ""term"" ' or ' """"term" '
|
|
192
|
+
str.split(/"("*[^"]+"*)"/).forEach((term, i) => {
|
|
193
|
+
if (i % 2 > 0) add(term);
|
|
194
|
+
else split(term);
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
split(str);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
add(str);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
array.sort((a, b) => b.length - a.length);
|
|
204
|
+
return { terms: array, termStats };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if a value is a number
|
|
209
|
+
* @param {number|string} value - the value to check;
|
|
210
|
+
* numeric strings allowed
|
|
211
|
+
* @return {boolean}
|
|
212
|
+
* @access protected
|
|
213
|
+
*/
|
|
214
|
+
isNumeric(value) {
|
|
215
|
+
// http://stackoverflow.com/a/16655847/145346
|
|
216
|
+
// eslint-disable-next-line eqeqeq
|
|
217
|
+
return Number(parseFloat(value)) == value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Filters valid ranges, sorts and, if wrapAllRanges option is false, filters out nesting/overlapping ranges
|
|
222
|
+
* @param {Mark~setOfRanges} array - unprocessed raw array
|
|
223
|
+
* @param {Mark~logObject} logs - The array of logs objects
|
|
224
|
+
* @return {Mark~setOfRanges} - processed array with any invalid entries removed
|
|
225
|
+
* @access protected
|
|
226
|
+
*/
|
|
227
|
+
checkRanges(array, logs, min, max) {
|
|
228
|
+
// a range object must have the start and length properties with numeric values
|
|
229
|
+
// [{start: 0, length: 5}, ..]
|
|
230
|
+
const level = 'error';
|
|
231
|
+
|
|
232
|
+
// filters and sorts valid ranges
|
|
233
|
+
const ranges = array.filter(range => {
|
|
234
|
+
if (this.isNumeric(range.start) && this.isNumeric(range.length)) {
|
|
235
|
+
range.start = parseInt(range.start);
|
|
236
|
+
range.length = parseInt(range.length);
|
|
237
|
+
|
|
238
|
+
if (range.start >= min && range.start < max && range.length > 0) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
logs.push({ text: 'Invalid range: ', obj: range, level });
|
|
243
|
+
return false;
|
|
244
|
+
}).sort((a, b) => a.start - b.start);
|
|
245
|
+
|
|
246
|
+
if (this.opt.wrapAllRanges) {
|
|
247
|
+
return ranges;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let lastIndex = 0, index;
|
|
251
|
+
// filters out nesting/overlapping ranges
|
|
252
|
+
return ranges.filter(range => {
|
|
253
|
+
index = range.start + range.length;
|
|
254
|
+
|
|
255
|
+
if (range.start >= lastIndex) {
|
|
256
|
+
lastIndex = index;
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
logs.push({ text: (index < lastIndex ? 'Nest' : 'Overlapp') + 'ing range: ', obj: range, level });
|
|
260
|
+
return false;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @typedef Mark~blockElementsBoundaryObject
|
|
266
|
+
* @type {object}
|
|
267
|
+
* @property {array} [tagNames] - The array of custom tag names
|
|
268
|
+
* @property {boolean} [extend] - Whether to extend the default boundary elements with custom elements
|
|
269
|
+
* or set only custom elements to boundary type
|
|
270
|
+
* @property {string} [char] - The custom separating char
|
|
271
|
+
*/
|
|
272
|
+
/**
|
|
273
|
+
* Sets type: 1 - separate by space, 2 - separate by boundary char with space(s)
|
|
274
|
+
* @param {object} tags - The object containing HTML element tag names
|
|
275
|
+
*/
|
|
276
|
+
setType(tags, boundary) {
|
|
277
|
+
const custom = Array.isArray(boundary.tagNames) && boundary.tagNames.length;
|
|
278
|
+
|
|
279
|
+
if (custom) {
|
|
280
|
+
// normalizes custom elements names and adds to the tags object with boundary type value
|
|
281
|
+
boundary.tagNames.forEach(name => tags[name.toLowerCase()] = 2);
|
|
282
|
+
}
|
|
283
|
+
// if not extend, the only custom tag names are set to a boundary type
|
|
284
|
+
if ( !custom || boundary.extend) {
|
|
285
|
+
// sets all tags value to the boundary
|
|
286
|
+
for (const key in tags) {
|
|
287
|
+
tags[key] = 2;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
tags['br'] = 3;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @typedef Mark~nodeInfoAcross
|
|
295
|
+
* @property {Text} node - The DOM text node
|
|
296
|
+
* @property {number} start - The start index within the composite string
|
|
297
|
+
* @property {number} end - The end index within the composite string
|
|
298
|
+
* @property {number} offset - The offset is used to correct position if a space or string
|
|
299
|
+
* was added to end of composite string after this node textContent
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @typedef Mark~getTextNodesAcrossDict
|
|
304
|
+
* @type {object.<string>}
|
|
305
|
+
* @property {string} text - The composite string of all text nodes
|
|
306
|
+
* @property {Mark~nodeInfoAcross[]} nodes - An array of node info objects
|
|
307
|
+
* @property {number} lastIndex - The property used to store the nodes last index
|
|
308
|
+
*/
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Callback
|
|
312
|
+
* @callback Mark~getTextNodesAcrossCallback
|
|
313
|
+
* @param {Mark~getTextNodesAcrossDict}
|
|
314
|
+
*/
|
|
315
|
+
/**
|
|
316
|
+
* Calls the callback with an object containing all text nodes (including iframe text nodes)
|
|
317
|
+
* with start and end positions and the composite value of them (string)
|
|
318
|
+
* @param {Mark~getTextNodesAcrossCallback} cb - Callback
|
|
319
|
+
* @access protected
|
|
320
|
+
*/
|
|
321
|
+
getTextNodesAcross(cb) {
|
|
322
|
+
// a space or string can be safely added to the end of a text node when two text nodes
|
|
323
|
+
// are 'separated' by element with one of these names
|
|
324
|
+
const tags = { div: 1, p: 1, li: 1, td: 1, tr: 1, th: 1, ul: 1,
|
|
325
|
+
ol: 1, dd: 1, dl: 1, dt: 1, h1: 1, h2: 1, h3: 1, h4: 1,
|
|
326
|
+
h5: 1, h6: 1, hr: 1, blockquote: 1, figcaption: 1, figure: 1,
|
|
327
|
+
pre: 1, table: 1, thead: 1, tbody: 1, tfoot: 1, input: 1,
|
|
328
|
+
img: 1, nav: 1, details: 1, label: 1, form: 1, select: 1, menu: 1,
|
|
329
|
+
br: 3, menuitem: 1,
|
|
330
|
+
main: 1, section: 1, article: 1, aside: 1, picture: 1, output: 1,
|
|
331
|
+
button: 1, header: 1, footer: 1, address: 1, area: 1, canvas: 1,
|
|
332
|
+
map: 1, fieldset: 1, textarea: 1, track: 1, video: 1, audio: 1,
|
|
333
|
+
body: 1, iframe: 1, meter: 1, object: 1, svg: 1 };
|
|
334
|
+
|
|
335
|
+
const nodes = [],
|
|
336
|
+
boundary = this.opt.blockElementsBoundary,
|
|
337
|
+
priorityType = boundary ? 2 : 1;
|
|
338
|
+
|
|
339
|
+
let ch = '\x01', tempType, type, prevNode;
|
|
340
|
+
|
|
341
|
+
if (boundary) {
|
|
342
|
+
this.setType(tags, boundary);
|
|
343
|
+
|
|
344
|
+
if (boundary.char) {
|
|
345
|
+
ch = boundary.char.charAt(0);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const obj = {
|
|
350
|
+
text: '', regex: /\s/, tags: tags,
|
|
351
|
+
boundary: boundary, str: '', ch: ch
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
this.iterator.forEachNode(this.filter.SHOW_ELEMENT | this.filter.SHOW_TEXT, node => { // each
|
|
355
|
+
if (prevNode) {
|
|
356
|
+
nodes.push(this.getNodeInfo(prevNode, node, type, obj));
|
|
357
|
+
}
|
|
358
|
+
type = null;
|
|
359
|
+
prevNode = node;
|
|
360
|
+
|
|
361
|
+
}, node => { // filter
|
|
362
|
+
if (node.nodeType === 1) { // element
|
|
363
|
+
tempType = tags[node.nodeName.toLowerCase()];
|
|
364
|
+
|
|
365
|
+
if (tempType === 3) { // br element
|
|
366
|
+
obj.str += '\n';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if ( !type || tempType === priorityType) {
|
|
370
|
+
type = tempType;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
return !this.excluded(node.parentNode);
|
|
375
|
+
|
|
376
|
+
}, () => { // done
|
|
377
|
+
// processes the last node
|
|
378
|
+
if (prevNode) {
|
|
379
|
+
nodes.push(this.getNodeInfo(prevNode, null, type, obj));
|
|
380
|
+
}
|
|
381
|
+
cb({
|
|
382
|
+
text: obj.text,
|
|
383
|
+
nodes: nodes,
|
|
384
|
+
lastIndex: 0
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Creates object
|
|
391
|
+
* @param {Text} prevNode - The previous DOM text node
|
|
392
|
+
* @param {Text} node - The current DOM text node
|
|
393
|
+
* @param {number|null} type - define how to separate the previous and current text nodes textContent;
|
|
394
|
+
* type is null when nodes doesn't separated by block elements
|
|
395
|
+
* @param {object} obj - The auxiliary object to pass multiple parameters to the method
|
|
396
|
+
*/
|
|
397
|
+
getNodeInfo(prevNode, node, type, obj) {
|
|
398
|
+
const start = obj.text.length,
|
|
399
|
+
ch = obj.ch;
|
|
400
|
+
let offset = 0,
|
|
401
|
+
str = obj.str,
|
|
402
|
+
text = prevNode.textContent;
|
|
403
|
+
|
|
404
|
+
if (node) {
|
|
405
|
+
const startBySpace = obj.regex.test(node.textContent[0]),
|
|
406
|
+
both = startBySpace && obj.regex.test(text[text.length - 1]);
|
|
407
|
+
|
|
408
|
+
if (obj.boundary || !both) {
|
|
409
|
+
let separate = type;
|
|
410
|
+
// searches for the first parent of the previous text node that met condition
|
|
411
|
+
// and checks does they have the same parent or the parent contains the current text node
|
|
412
|
+
if (!type) {
|
|
413
|
+
let parent = prevNode.parentNode;
|
|
414
|
+
while (parent) {
|
|
415
|
+
type = obj.tags[parent.nodeName.toLowerCase()];
|
|
416
|
+
if (type) {
|
|
417
|
+
separate = !(parent === node.parentNode || parent.contains(node));
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
parent = parent.parentNode;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (separate) {
|
|
425
|
+
if ( !both) {
|
|
426
|
+
str += type === 1 ? ' ' : type === 2 ? ' ' + ch + ' ' : '';
|
|
427
|
+
|
|
428
|
+
} else if (type === 2) {
|
|
429
|
+
str += both ? ch : startBySpace ? ' ' + ch : ch + ' ';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (str) {
|
|
436
|
+
text += str;
|
|
437
|
+
offset = str.length;
|
|
438
|
+
obj.str = '';
|
|
439
|
+
}
|
|
440
|
+
obj.text += text;
|
|
441
|
+
|
|
442
|
+
return this.createInfo(prevNode, start, obj.text.length - offset, offset);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @typedef Mark~nodeInfo
|
|
447
|
+
* @property {Text} node - The DOM text node
|
|
448
|
+
* @property {number} start - The start index within the composite string
|
|
449
|
+
* @property {number} end - The end index within the composite string
|
|
450
|
+
* @property {number} offset - This property is required for compatibility with [Mark~nodeInfoAcross]
|
|
451
|
+
* for {@link Mark#markRanges}
|
|
452
|
+
*/
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @typedef Mark~getTextNodesDict
|
|
456
|
+
* @type {object.<string>}
|
|
457
|
+
* @property {string} text - The composite value of all text nodes
|
|
458
|
+
* @property {Mark~nodeInfo[]} nodes - The array of objects
|
|
459
|
+
* @property {number} lastIndex - The property used to store the nodes last index
|
|
460
|
+
*/
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Callback
|
|
464
|
+
* @callback Mark~getTextNodesCallback
|
|
465
|
+
* @param {Mark~getTextNodesDict}
|
|
466
|
+
*/
|
|
467
|
+
/**
|
|
468
|
+
* Calls the callback with an object containing all text nodes (including iframe text nodes)
|
|
469
|
+
* with start and end positions and the composite value of them (string)
|
|
470
|
+
* @param {Mark~getTextNodesCallback} cb - Callback
|
|
471
|
+
* @access protected
|
|
472
|
+
*/
|
|
473
|
+
getRangesTextNodes(cb, lines) {
|
|
474
|
+
const nodes = [],
|
|
475
|
+
regex = /\n/g,
|
|
476
|
+
newLines = [0],
|
|
477
|
+
show = this.filter.SHOW_TEXT | (lines ? this.filter.SHOW_ELEMENT : 0);
|
|
478
|
+
let text = '',
|
|
479
|
+
len = 0,
|
|
480
|
+
rm;
|
|
481
|
+
|
|
482
|
+
this.iterator.forEachNode(show, node => { // each
|
|
483
|
+
if (lines) {
|
|
484
|
+
while ((rm = regex.exec(node.textContent)) !== null) {
|
|
485
|
+
newLines.push(len + rm.index);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
text += node.textContent;
|
|
489
|
+
|
|
490
|
+
nodes.push({
|
|
491
|
+
start: len,
|
|
492
|
+
end: (len = text.length),
|
|
493
|
+
offset: 0,
|
|
494
|
+
node: node
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
}, node => { // filter
|
|
498
|
+
if (lines && node.nodeType === 1) {
|
|
499
|
+
if (node.tagName.toLowerCase() === 'br') {
|
|
500
|
+
newLines.push(len);
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return !this.excluded(node.parentNode);
|
|
505
|
+
|
|
506
|
+
}, () => { // done
|
|
507
|
+
const dict = { text, nodes, lastIndex: 0 };
|
|
508
|
+
|
|
509
|
+
if (lines) {
|
|
510
|
+
newLines.push(len);
|
|
511
|
+
dict.newLines = newLines;
|
|
512
|
+
}
|
|
513
|
+
cb(dict);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* @typedef Mark~nodeInfo
|
|
519
|
+
* @property {Text} node - The DOM text node
|
|
520
|
+
* @property {number} start - The start index within the composite string
|
|
521
|
+
*/
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* @typedef Mark~getTextNodesDict
|
|
525
|
+
* @type {object.<string>}
|
|
526
|
+
* @property {Mark~nodeInfo[]} nodes - The array of objects
|
|
527
|
+
* @property {number} lastIndex - The property used to store the nodes last index
|
|
528
|
+
*/
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Callback
|
|
532
|
+
* @callback Mark~getTextNodesCallback
|
|
533
|
+
* @param {Mark~getTextNodesDict}
|
|
534
|
+
*/
|
|
535
|
+
/**
|
|
536
|
+
* Calls the callback with an object containing all text nodes (including iframe text nodes)
|
|
537
|
+
* @param {Mark~getTextNodesCallback} cb - Callback
|
|
538
|
+
* @access protected
|
|
539
|
+
*/
|
|
540
|
+
getTextNodes(cb) {
|
|
541
|
+
const nodes = [];
|
|
542
|
+
let start = 0;
|
|
543
|
+
|
|
544
|
+
this.iterator.forEachNode(this.filter.SHOW_TEXT, node => { // each
|
|
545
|
+
nodes.push({
|
|
546
|
+
node,
|
|
547
|
+
start
|
|
548
|
+
});
|
|
549
|
+
start += node.textContent.length;
|
|
550
|
+
|
|
551
|
+
}, node => { // filter
|
|
552
|
+
return !this.excluded(node.parentNode);
|
|
553
|
+
|
|
554
|
+
}, () => { // done
|
|
555
|
+
cb({ nodes, lastIndex: 0 });
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Checks if an element matches any of the specified exclude selectors.
|
|
561
|
+
* @param {HTMLElement} elem - The element to check
|
|
562
|
+
* @return {boolean}
|
|
563
|
+
* @access protected
|
|
564
|
+
*/
|
|
565
|
+
excluded(elem) {
|
|
566
|
+
// it's faster to check if an array contains the node name than a selector in 'DOMIterator.matches()'
|
|
567
|
+
// also it allows using a string of selectors instead of an array with the 'exclude' option
|
|
568
|
+
return this.nodeNames.includes(elem.nodeName.toLowerCase()) || DOMIterator.matches(elem, this.opt.exclude);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Splits the text node into two or three nodes and wraps the necessary node or wraps the input node
|
|
573
|
+
* Creates info object(s) related to the newly created node(s) and inserts into dict.nodes or replace an existing one
|
|
574
|
+
* It doesn't create empty sibling text nodes when `Text.splitText()` method splits a text node at the start/end
|
|
575
|
+
* @param {Mark~wrapRangeInsertDict} dict - The dictionary
|
|
576
|
+
* @param {object} n - The currently processed info object
|
|
577
|
+
* @param {number} s - The position where to start wrapping
|
|
578
|
+
* @param {number} e - The position where to end wrapping
|
|
579
|
+
* @param {number} start - The start position of the match
|
|
580
|
+
* @param {number} index - The current index of the processed object
|
|
581
|
+
* @return {object} Returns object containing the mark element, the splitted text node
|
|
582
|
+
* that will appear after the wrapped text node, and increment number
|
|
583
|
+
*/
|
|
584
|
+
wrapRangeInsert(dict, n, s, e, start, index) {
|
|
585
|
+
const ended = e === n.node.textContent.length,
|
|
586
|
+
end = n.end;
|
|
587
|
+
// type: 0 - whole text node, 1 - from the start, 2 - to the end, 3 - between
|
|
588
|
+
let type = 1,
|
|
589
|
+
splitIndex = e,
|
|
590
|
+
node = n.node;
|
|
591
|
+
// prevents creating empty sibling text nodes at the start/end of a text node
|
|
592
|
+
if (s !== 0) {
|
|
593
|
+
node = node.splitText(s);
|
|
594
|
+
splitIndex = e - s;
|
|
595
|
+
type = ended ? 2 : 3;
|
|
596
|
+
|
|
597
|
+
} else if (ended) { // whole
|
|
598
|
+
type = 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const retNode = ended ? this.empty : node.splitText(splitIndex),
|
|
602
|
+
mark = this.createElement(node),
|
|
603
|
+
markChild = mark.childNodes[0],
|
|
604
|
+
nodeInfo = this.createInfo(retNode, type === 0 || type === 2 ? end : n.start + e, end, n.offset);
|
|
605
|
+
|
|
606
|
+
if (type === 0) {
|
|
607
|
+
n.node = markChild;
|
|
608
|
+
return { mark, nodeInfo, increment: 0 };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const info = this.createInfo(markChild, type === 1 ? n.start : start, n.start + e, 0);
|
|
612
|
+
// inserts new node(s) info in dict.nodes depending where a range is located in a text node
|
|
613
|
+
if (type === 1) {
|
|
614
|
+
dict.nodes.splice(index, 1, info, nodeInfo);
|
|
615
|
+
} else {
|
|
616
|
+
if (type === 2) {
|
|
617
|
+
dict.nodes.splice(index + 1, 0, info);
|
|
618
|
+
} else {
|
|
619
|
+
dict.nodes.splice(index + 1, 0, info, nodeInfo);
|
|
620
|
+
}
|
|
621
|
+
n.end = start;
|
|
622
|
+
n.offset = 0;
|
|
623
|
+
}
|
|
624
|
+
return { mark, nodeInfo, increment: type < 3 ? 1 : 2 };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Creates object
|
|
629
|
+
* @param {Text} node - The DOM text node
|
|
630
|
+
* @param {number} start - The position where to start wrapping
|
|
631
|
+
* @param {number} end - The position where to end wrapping
|
|
632
|
+
* @param {number} offset - The length of space/string that is added to end of composite string
|
|
633
|
+
* after this node textContent
|
|
634
|
+
*/
|
|
635
|
+
createInfo(node, start, end, offset) {
|
|
636
|
+
return { node, start, end, offset };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Each callback
|
|
641
|
+
* @callback Mark~wrapRangeEachCallback
|
|
642
|
+
* @param {HTMLElement|StaticRange|Range} node - The wrapped DOM element or range (Highlight API)
|
|
643
|
+
*/
|
|
644
|
+
/**
|
|
645
|
+
* Splits the text node into two or three nodes and wraps the necessary node or wraps the input node
|
|
646
|
+
* It doesn't create empty sibling text nodes when `Text.splitText()` method splits a text node at the start/end
|
|
647
|
+
* @param {Text} node - The DOM text node
|
|
648
|
+
* @param {number} start - The position where to start wrapping
|
|
649
|
+
* @param {number} end - The position where to end wrapping
|
|
650
|
+
* @param {Mark~wrapRangeEachCallback} eachCb - Each callback
|
|
651
|
+
* @return {Text}
|
|
652
|
+
* @access protected
|
|
653
|
+
*/
|
|
654
|
+
wrapRange(n, start, end, eachCb) {
|
|
655
|
+
let node = n.node,
|
|
656
|
+
retNode;
|
|
657
|
+
|
|
658
|
+
if (this.rangeArray) {
|
|
659
|
+
this.createRange(node, start, node, end, n.start + start, eachCb);
|
|
660
|
+
retNode = node;
|
|
661
|
+
|
|
662
|
+
} else {
|
|
663
|
+
let ended = end === node.textContent.length,
|
|
664
|
+
index = end;
|
|
665
|
+
|
|
666
|
+
if (start !== 0) {
|
|
667
|
+
node = node.splitText(start);
|
|
668
|
+
index = end - start;
|
|
669
|
+
}
|
|
670
|
+
retNode = ended ? this.empty : node.splitText(index);
|
|
671
|
+
eachCb(this.createElement(node));
|
|
672
|
+
}
|
|
673
|
+
return retNode;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Each callback
|
|
678
|
+
* @callback Mark~createRangeEachCallback
|
|
679
|
+
* @param {StaticRange|Range} range - The created range
|
|
680
|
+
* @param {boolean} true - Required only for across elements code with the rangeAcrossElements option
|
|
681
|
+
*/
|
|
682
|
+
/**
|
|
683
|
+
* Creates the new StaticRange/Range object with the specified parameters
|
|
684
|
+
* @param {Text} startNode - The text node where a match is started
|
|
685
|
+
* @param {number} startOffset - The start index of the match in startNode
|
|
686
|
+
* @param {number} endNode - The text node where a match is ended
|
|
687
|
+
* @param {number} endOffset - The end index of the match in endNode
|
|
688
|
+
* @param {number} absoluteOffset - The absolute start index from the beginning of first context.
|
|
689
|
+
* Uses to sort ranges by ascending order.
|
|
690
|
+
* @param {Mark~createRangeEachCallback} eachCb - Each callback
|
|
691
|
+
*/
|
|
692
|
+
createRange(startNode, startOffset, endNode, endOffset, absoluteOffset, eachCb) {
|
|
693
|
+
let range;
|
|
694
|
+
|
|
695
|
+
if (this.opt.staticRanges) {
|
|
696
|
+
range = new StaticRange({ startContainer: startNode, startOffset, endContainer: endNode, endOffset });
|
|
697
|
+
|
|
698
|
+
} else {
|
|
699
|
+
range = new Range();
|
|
700
|
+
range.setStart(startNode, startOffset);
|
|
701
|
+
range.setEnd(endNode, endOffset);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
range.absoluteOffset = absoluteOffset;
|
|
705
|
+
|
|
706
|
+
eachCb(range, true);
|
|
707
|
+
// a range can be destroyed on the 'each' callback
|
|
708
|
+
if (range) this.rangeArray.push(range);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Wraps the new element with the necessary attributes around text node
|
|
713
|
+
* @param {Text} node - The DOM text node
|
|
714
|
+
* @return {HTMLElement} Returns the created DOM node
|
|
715
|
+
*/
|
|
716
|
+
createElement(node) {
|
|
717
|
+
let markNode = this.opt.window.document.createElement(this.opt.element);
|
|
718
|
+
markNode.setAttribute('data-markjs', 'true');
|
|
719
|
+
|
|
720
|
+
if (this.opt.className) {
|
|
721
|
+
markNode.setAttribute('class', this.opt.className);
|
|
722
|
+
}
|
|
723
|
+
markNode.textContent = node.textContent;
|
|
724
|
+
node.parentNode.replaceChild(markNode, node);
|
|
725
|
+
|
|
726
|
+
return markNode;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* @typedef Mark~wrapRangeAcrossDict
|
|
731
|
+
* @type {object.<string>}
|
|
732
|
+
* @property {string} text - The composite string of all text nodes
|
|
733
|
+
* @property {Mark~nodeInfoAcross[]} nodes - An array of node info objects
|
|
734
|
+
* @property {number} lastIndex - The property used to store the nodes last index
|
|
735
|
+
*/
|
|
736
|
+
/**
|
|
737
|
+
* Each callback
|
|
738
|
+
* @callback Mark~wrapRangeAcrossEachCallback
|
|
739
|
+
* @param {HTMLElement|StaticRange|Range} node - The wrapped DOM element or range (Highlight API)
|
|
740
|
+
* @param {boolean} rangeStart - Indicate the start of the current range or always true (Highlight API)
|
|
741
|
+
*/
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Filter callback
|
|
745
|
+
* @callback Mark~wrapRangeAcrossFilterCallback
|
|
746
|
+
* @param {Text|Text[]} node - The current text node or an array of text nodes when using the Highlight API
|
|
747
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
748
|
+
*/
|
|
749
|
+
/**
|
|
750
|
+
* Determines matches by start and end positions using the text node dictionary
|
|
751
|
+
* and calls {@link Mark#wrapRange} or {@link Mark#wrapRangeInsert} to wrap them
|
|
752
|
+
* @param {Mark~wrapRangeAcrossDict} dict - The dictionary
|
|
753
|
+
* @param {number} start - The start index of the match
|
|
754
|
+
* @param {number} end - The end index of the match
|
|
755
|
+
* @param {Mark~wrapRangeAcrossFilterCallback} filterCb - Filter callback
|
|
756
|
+
* @param {Mark~wrapRangeAcrossEachCallback} eachCb - Each callback
|
|
757
|
+
* @access protected
|
|
758
|
+
*/
|
|
759
|
+
wrapRangeAcross(dict, start, end, filterCb, eachCb) {
|
|
760
|
+
// dict.lastIndex stores the last node index to avoid iteration from the beginning
|
|
761
|
+
let i = dict.lastIndex,
|
|
762
|
+
rangeStart = true,
|
|
763
|
+
startInfo,
|
|
764
|
+
filterNodes = [],
|
|
765
|
+
e;
|
|
766
|
+
const wrapAllRanges = this.opt.wrapAllRanges,
|
|
767
|
+
highlightAPI = !!this.opt.highlight, // when using the Highlight API, no text nodes are split
|
|
768
|
+
singleRange = highlightAPI && this.opt.rangeAcrossElements;
|
|
769
|
+
|
|
770
|
+
if (wrapAllRanges) {
|
|
771
|
+
// finds the start index in case of nesting/overlapping
|
|
772
|
+
while (i > 0 && dict.nodes[i].start > start) {
|
|
773
|
+
i--;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
for (i; i < dict.nodes.length; i++) {
|
|
778
|
+
if (i + 1 === dict.nodes.length || dict.nodes[i+1].start > start) {
|
|
779
|
+
let n = dict.nodes[i];
|
|
780
|
+
|
|
781
|
+
if (singleRange) {
|
|
782
|
+
filterNodes.push(n.node);
|
|
783
|
+
|
|
784
|
+
} else if ( !filterCb(n.node)) {
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
// map range from dict.text to text node
|
|
788
|
+
const s = start - n.start;
|
|
789
|
+
e = (end > n.end ? n.end : end) - n.start;
|
|
790
|
+
|
|
791
|
+
// prevents creating an empty mark node, prevents exception if something went wrong, useful for debug
|
|
792
|
+
if (s >= 0 && e > s) {
|
|
793
|
+
if (singleRange) {
|
|
794
|
+
if (rangeStart) {
|
|
795
|
+
startInfo = [n.node, s, n.start + s];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
} else if ( !highlightAPI && wrapAllRanges) {
|
|
799
|
+
const obj = this.wrapRangeInsert(dict, n, s, e, start, i);
|
|
800
|
+
n = obj.nodeInfo;
|
|
801
|
+
eachCb(obj.mark, rangeStart);
|
|
802
|
+
|
|
803
|
+
} else {
|
|
804
|
+
// when using the Highlight API and the 'rangeAcrossElements: false',
|
|
805
|
+
// it creates multiple ranges for matches that are located across elements
|
|
806
|
+
n.node = this.wrapRange(n, s, e, elemOrRange => {
|
|
807
|
+
eachCb(elemOrRange, rangeStart);
|
|
808
|
+
});
|
|
809
|
+
// sets the new text node start index in the case of subsequent matches in the same text node
|
|
810
|
+
if ( !highlightAPI) n.start += e;
|
|
811
|
+
}
|
|
812
|
+
rangeStart = false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (end > n.end) {
|
|
816
|
+
// the range extends to the next text node
|
|
817
|
+
start = n.end + n.offset;
|
|
818
|
+
|
|
819
|
+
} else {
|
|
820
|
+
// creates a single StaticRange/Range for matches that are located across elements
|
|
821
|
+
if (startInfo && filterCb(filterNodes)) {
|
|
822
|
+
this.createRange(startInfo[0], startInfo[1], n.node, e, startInfo[2], eachCb);
|
|
823
|
+
}
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// sets the last index
|
|
829
|
+
dict.lastIndex = i;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Filter callback before each wrapping
|
|
834
|
+
* @callback Mark~wrapGroupsFilterCallback
|
|
835
|
+
* @param {Text} node - The text node where the match occurs
|
|
836
|
+
* @param {string} group - The current group matching string
|
|
837
|
+
* @param {number} i - The current group index
|
|
838
|
+
*/
|
|
839
|
+
/**
|
|
840
|
+
* Callback for each wrapped element
|
|
841
|
+
* @callback Mark~wrapGroupsEachCallback
|
|
842
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
843
|
+
* @param {number} i - The current group index
|
|
844
|
+
*/
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Wraps match groups with RegExp.hasIndices
|
|
848
|
+
* @param {Text} node - The text node where the match occurs
|
|
849
|
+
* @param {array} match - The result of RegExp exec() method
|
|
850
|
+
* @param {RegExp} regex - The regular expression
|
|
851
|
+
* @param {Mark~wrapGroupsFilterCallback} filterCb - Filter callback
|
|
852
|
+
* @param {Mark~wrapGroupsEachCallback} eachCb - Each callback
|
|
853
|
+
*/
|
|
854
|
+
wrapGroups(n, match, regex, filterCb, eachCb) {
|
|
855
|
+
let lastIndex = 0,
|
|
856
|
+
offset = 0,
|
|
857
|
+
i = 0,
|
|
858
|
+
highlightAPI = this.opt.highlight,
|
|
859
|
+
isWrapped = false,
|
|
860
|
+
group, start, end = 0;
|
|
861
|
+
|
|
862
|
+
while (++i < match.length) {
|
|
863
|
+
group = match[i];
|
|
864
|
+
|
|
865
|
+
if (group) {
|
|
866
|
+
start = match.indices[i][0];
|
|
867
|
+
//it prevents marking nested group - parent group is already marked
|
|
868
|
+
if (start >= lastIndex) {
|
|
869
|
+
end = match.indices[i][1];
|
|
870
|
+
|
|
871
|
+
if (filterCb(n.node, group, i)) {
|
|
872
|
+
// when a group is wrapping, a text node is split at the end index,
|
|
873
|
+
// so to correct the start & end indexes of a new text node, subtract
|
|
874
|
+
// the end index of the last wrapped group (offset)
|
|
875
|
+
n.node = this.wrapRange(n, start - offset, end - offset, elemOrRange => { // each
|
|
876
|
+
eachCb(elemOrRange);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
if (end > lastIndex) {
|
|
880
|
+
lastIndex = end;
|
|
881
|
+
}
|
|
882
|
+
// when using the Highlight API, no text nodes are split
|
|
883
|
+
if ( !highlightAPI) offset = end;
|
|
884
|
+
isWrapped = true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// resets the lastIndex when at least one group is wrapped (prevents infinite loop)
|
|
890
|
+
if (isWrapped) {
|
|
891
|
+
if ( !highlightAPI) regex.lastIndex = 0;
|
|
892
|
+
|
|
893
|
+
} else if (match[0].length === 0) {
|
|
894
|
+
this.setLastIndex(regex, end);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Filter callback before each wrapping
|
|
900
|
+
* @callback Mark~wrapGroupsAcrossFilterCallback
|
|
901
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
902
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
903
|
+
* @param {string} group - The current group matching string
|
|
904
|
+
* @param {number} i - The current group index
|
|
905
|
+
*/
|
|
906
|
+
/**
|
|
907
|
+
* Callback for each wrapped element
|
|
908
|
+
* @callback Mark~wrapGroupsAcrossEachCallback
|
|
909
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
910
|
+
* @param {number} i - The current group index
|
|
911
|
+
* @param {boolean} groupStart - Indicate the start of a group
|
|
912
|
+
*/
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Wraps match groups with RegExp.hasIndices across elements
|
|
916
|
+
* @param {Mark~wrapGroupsAcrossDict} dict - The dictionary
|
|
917
|
+
* @param {array} match - The result of RegExp exec() method
|
|
918
|
+
* @param {RegExp} regex - The regular expression
|
|
919
|
+
* @param {Mark~wrapGroupsAcrossFilterCallback} filterCb - Filter callback
|
|
920
|
+
* @param {Mark~wrapGroupsAcrossEachCallback} eachCb - Each callback
|
|
921
|
+
*/
|
|
922
|
+
wrapGroupsAcross(dict, match, regex, filterCb, eachCb) {
|
|
923
|
+
let lastIndex = 0,
|
|
924
|
+
i = 0,
|
|
925
|
+
end = 0,
|
|
926
|
+
start,
|
|
927
|
+
group,
|
|
928
|
+
isWrapped;
|
|
929
|
+
|
|
930
|
+
while (++i < match.length) {
|
|
931
|
+
group = match[i];
|
|
932
|
+
|
|
933
|
+
if (group) {
|
|
934
|
+
start = match.indices[i][0];
|
|
935
|
+
// the wrapAllRanges option allows wrapping nested group(s),
|
|
936
|
+
// the 'start >= lastIndex' prevents wrapping nested group(s) - the parent group is already wrapped
|
|
937
|
+
if (this.opt.wrapAllRanges || start >= lastIndex) {
|
|
938
|
+
end = match.indices[i][1];
|
|
939
|
+
isWrapped = false;
|
|
940
|
+
|
|
941
|
+
this.wrapRangeAcross(dict, start, end, nodeOrArray => { // filter
|
|
942
|
+
return filterCb(nodeOrArray, group, i);
|
|
943
|
+
|
|
944
|
+
}, (elemOrRange, groupStart) => { // each
|
|
945
|
+
isWrapped = true;
|
|
946
|
+
eachCb(elemOrRange, groupStart);
|
|
947
|
+
});
|
|
948
|
+
// group may be filtered out
|
|
949
|
+
if (isWrapped && end > lastIndex) {
|
|
950
|
+
lastIndex = end;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (match[0].length === 0) {
|
|
957
|
+
this.setLastIndex(regex, end);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* When the length of a match is zero, there is a need to set the RegExp lastIndex depending on conditions.
|
|
963
|
+
* It's necessary to avoid infinite loop and set position from which to start the next match
|
|
964
|
+
* @param {RegExp} regex - The regular expression
|
|
965
|
+
* @param {number} end - The end index of the last processed group
|
|
966
|
+
*/
|
|
967
|
+
setLastIndex(regex, end) {
|
|
968
|
+
const index = regex.lastIndex;
|
|
969
|
+
// end > index - case when a capturing group is inside positive lookahead assertion
|
|
970
|
+
// end > 0 - case when a match is filtered out or a capturing group is inside positive lookbehind assertion
|
|
971
|
+
regex.lastIndex = end > index ? end : end > 0 ? index + 1 : Infinity;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* @typedef Mark~filterInfoObject
|
|
976
|
+
* @type {object}
|
|
977
|
+
* @property {array} match - The result of RegExp exec() method
|
|
978
|
+
* @property {boolean} matchStart - Indicate the start of match. It's only available
|
|
979
|
+
* with the 'acrossElements' option
|
|
980
|
+
* @property {number} groupIndex - The group index. It's only available
|
|
981
|
+
* with 'separateGroups' option
|
|
982
|
+
* @property {object} execution - The helper object for early abort. Contains
|
|
983
|
+
* boolean 'abort' property.
|
|
984
|
+
*/
|
|
985
|
+
/**
|
|
986
|
+
* @typedef Mark~eachInfoObject
|
|
987
|
+
* @type {object}
|
|
988
|
+
* @property {array} match - The result of RegExp exec() method
|
|
989
|
+
* @property {boolean} matchStart - Indicate the start of match. It's only available
|
|
990
|
+
* with the 'acrossElements' option
|
|
991
|
+
* @property {number} count - The current number of matches
|
|
992
|
+
* @property {number} groupIndex - The index of current match group. It's only
|
|
993
|
+
* available with 'separateGroups' option
|
|
994
|
+
* @property {boolean} groupStart - Indicate the start of group. It's only
|
|
995
|
+
* available with both 'acrossElements' and 'separateGroups' options
|
|
996
|
+
*/
|
|
997
|
+
/**
|
|
998
|
+
* @typedef Mark~infoObject
|
|
999
|
+
* @type {object}
|
|
1000
|
+
* @property {number} count - The number of matches so far
|
|
1001
|
+
* @property {object} execution - The helper object for early abort. Contains boolean 'abort' property.
|
|
1002
|
+
*/
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Group filter callback before each wrapping
|
|
1006
|
+
* @callback Mark~processGroupsFilterCallback
|
|
1007
|
+
* @param {Text} node - The text node where the match occurs
|
|
1008
|
+
* @param {string} group - The matching string of the current group
|
|
1009
|
+
* @param {Mark~filterInfoObject} info - The object containing the match information
|
|
1010
|
+
*/
|
|
1011
|
+
/**
|
|
1012
|
+
* Callback for each wrapped element
|
|
1013
|
+
* @callback Mark~processGroupsEachCallback
|
|
1014
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1015
|
+
* @param {Mark~eachInfoObject} - The object containing the match information
|
|
1016
|
+
*/
|
|
1017
|
+
/**
|
|
1018
|
+
* Callback on end
|
|
1019
|
+
* @callback Mark~processGroupsEndCallback
|
|
1020
|
+
* @param {number} count - The number of matches
|
|
1021
|
+
*/
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Wraps match capturing groups
|
|
1025
|
+
* @param {RegExp} regex - The regular expression to be searched for
|
|
1026
|
+
* @param {number} unused
|
|
1027
|
+
* @param {Mark~infoObject} info - The object used on filter and each callbacks
|
|
1028
|
+
* @param {Mark~processGroupsFilterCallback} filterCb - Filter callback
|
|
1029
|
+
* @param {Mark~processGroupsEachCallback} eachCb - Each callback
|
|
1030
|
+
* @param {Mark~processGroupsEndCallback} endCb
|
|
1031
|
+
* @access protected
|
|
1032
|
+
*/
|
|
1033
|
+
processGroups(regex, unused, info, filterCb, eachCb, endCb) {
|
|
1034
|
+
let count = info.count, match, filterStart, eachStart;
|
|
1035
|
+
|
|
1036
|
+
this.getTextNodes(dict => {
|
|
1037
|
+
dict.nodes.every(n => {
|
|
1038
|
+
while ((match = regex.exec(n.node.textContent)) !== null) {
|
|
1039
|
+
info.match = match;
|
|
1040
|
+
filterStart = eachStart = true;
|
|
1041
|
+
|
|
1042
|
+
this.wrapGroups(n, match, regex, (node, group, grIndex) => { // filter
|
|
1043
|
+
info.matchStart = filterStart;
|
|
1044
|
+
info.groupIndex = grIndex;
|
|
1045
|
+
filterStart = false;
|
|
1046
|
+
return filterCb(node, group, info);
|
|
1047
|
+
|
|
1048
|
+
}, (elemOrRange) => { // each
|
|
1049
|
+
if (eachStart) count++;
|
|
1050
|
+
|
|
1051
|
+
info.count = count;
|
|
1052
|
+
info.matchStart = eachStart;
|
|
1053
|
+
eachStart = false;
|
|
1054
|
+
|
|
1055
|
+
eachCb(elemOrRange, info);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
if (info.abort) break;
|
|
1059
|
+
}
|
|
1060
|
+
// breaks loop on custom abort
|
|
1061
|
+
return !info.abort;
|
|
1062
|
+
});
|
|
1063
|
+
endCb(count);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Filter callback before each wrapping
|
|
1069
|
+
* @callback Mark~processGroupsAcrossFilterCallback
|
|
1070
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
1071
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
1072
|
+
* @param {string} group - The matching string of the current group
|
|
1073
|
+
* @param {Mark~filterInfoObject} info - The object containing the match information
|
|
1074
|
+
*/
|
|
1075
|
+
/**
|
|
1076
|
+
* Callback for each wrapped element
|
|
1077
|
+
* @callback Mark~processGroupsAcrossEachCallback
|
|
1078
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1079
|
+
* @param {Mark~eachInfoObject} - The object containing the match information
|
|
1080
|
+
*/
|
|
1081
|
+
/**
|
|
1082
|
+
* Callback on end
|
|
1083
|
+
* @callback Mark~processGroupsAcrossEndCallback
|
|
1084
|
+
* @param {number} count - The number of all matches
|
|
1085
|
+
*/
|
|
1086
|
+
/**
|
|
1087
|
+
* Wraps match capturing groups across elements
|
|
1088
|
+
* @param {RegExp} regex - The regular expression to be searched for
|
|
1089
|
+
* @param {number} unused
|
|
1090
|
+
* @param {Mark~infoObject} info - The object used on filter and each callbacks
|
|
1091
|
+
* @param {Mark~processGroupsAcrossFilterCallback} filterCb - Filter callback
|
|
1092
|
+
* @param {Mark~processGroupsAcrossEachCallback} eachCb - Each callback
|
|
1093
|
+
* @param {Mark~processGroupsAcrossEndCallback} endCb
|
|
1094
|
+
* @access protected
|
|
1095
|
+
*/
|
|
1096
|
+
processGroupsAcross(regex, unused, info, filterCb, eachCb, endCb) {
|
|
1097
|
+
let count = info.count, match, filterStart, eachStart;
|
|
1098
|
+
|
|
1099
|
+
this.getTextNodesAcross(dict => {
|
|
1100
|
+
while ((match = regex.exec(dict.text)) !== null) {
|
|
1101
|
+
info.match = match;
|
|
1102
|
+
filterStart = eachStart = true;
|
|
1103
|
+
|
|
1104
|
+
this.wrapGroupsAcross(dict, match, regex, (nodeOrArray, group, grIndex) => { // filter
|
|
1105
|
+
// eslint-disable-next-line
|
|
1106
|
+
info.groupStart = undefined;
|
|
1107
|
+
info.matchStart = filterStart;
|
|
1108
|
+
info.groupIndex = grIndex;
|
|
1109
|
+
filterStart = false;
|
|
1110
|
+
return filterCb(nodeOrArray, group, info);
|
|
1111
|
+
|
|
1112
|
+
}, (elemOrRange, groupStart) => { // each
|
|
1113
|
+
if (eachStart) count++;
|
|
1114
|
+
|
|
1115
|
+
info.count = count;
|
|
1116
|
+
info.matchStart = eachStart;
|
|
1117
|
+
info.groupStart = groupStart;
|
|
1118
|
+
eachCb(elemOrRange, info);
|
|
1119
|
+
eachStart = false;
|
|
1120
|
+
});
|
|
1121
|
+
// breaks loop on custom abort
|
|
1122
|
+
if (info.abort) break;
|
|
1123
|
+
}
|
|
1124
|
+
endCb(count);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Filter callback before each wrapping
|
|
1130
|
+
* @callback Mark~processMatchesFilterCallback
|
|
1131
|
+
* @param {Text} node - The text node where the match occurs
|
|
1132
|
+
* @param {string} str - The matching string
|
|
1133
|
+
* @param {Mark~filterInfoObject} filterInfo - The object containing the match information
|
|
1134
|
+
*/
|
|
1135
|
+
/**
|
|
1136
|
+
* Callback for each wrapped element
|
|
1137
|
+
* @callback Mark~processMatchesEachCallback
|
|
1138
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1139
|
+
* @param {Mark~eachInfoObject} eachInfo - The object containing the match information
|
|
1140
|
+
*/
|
|
1141
|
+
/**
|
|
1142
|
+
* Callback on end
|
|
1143
|
+
* @callback Mark~processMatchesEndCallback
|
|
1144
|
+
* @param {number} count - The number of all matches
|
|
1145
|
+
*/
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Wraps the instance element and class around matches within single HTML elements in all contexts
|
|
1149
|
+
* @param {RegExp} regex - The regular expression to be searched for
|
|
1150
|
+
* @param {number} ignoreGroups - A number of RegExp capturing groups to ignore from the beginning of a match
|
|
1151
|
+
* @param {Mark~infoObject} info - The object used on filter and each callbacks
|
|
1152
|
+
* @param {Mark~processMatchesFilterCallback} filterCb - Filter callback
|
|
1153
|
+
* @param {Mark~processMatchesEachCallback} eachCb - Each callback
|
|
1154
|
+
* @param {Mark~processMatchesEndCallback} endCb
|
|
1155
|
+
* @access protected
|
|
1156
|
+
*/
|
|
1157
|
+
processMatches(regex, ignoreGroups, info, filterCb, eachCb, endCb) {
|
|
1158
|
+
const index = ignoreGroups === 0 ? 0 : ignoreGroups + 1;
|
|
1159
|
+
let count = info.count, match, str;
|
|
1160
|
+
|
|
1161
|
+
this.getTextNodes(dict => {
|
|
1162
|
+
dict.nodes.every(n => {
|
|
1163
|
+
while ((match = regex.exec(n.node.textContent)) !== null) {
|
|
1164
|
+
// prevents an infinite loop
|
|
1165
|
+
if ((str = match[index]) === '') {
|
|
1166
|
+
regex.lastIndex++;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
info.match = match;
|
|
1170
|
+
|
|
1171
|
+
if ( !filterCb(n.node, str, info)) {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
// calculates the start index inside node.textContent
|
|
1175
|
+
let i = 0, start = match.index;
|
|
1176
|
+
while (++i < index) {
|
|
1177
|
+
if (match[i]) { // allows any ignore group to be undefined
|
|
1178
|
+
start += match[i].length;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
n.node = this.wrapRange(n, start, start + str.length, elemOrRange => {
|
|
1183
|
+
info.count = ++count;
|
|
1184
|
+
eachCb(elemOrRange, info);
|
|
1185
|
+
});
|
|
1186
|
+
// when using the Highlight API, no text nodes are split
|
|
1187
|
+
if ( !this.opt.highlight) regex.lastIndex = 0;
|
|
1188
|
+
|
|
1189
|
+
if (info.abort) break;
|
|
1190
|
+
}
|
|
1191
|
+
// breaks loop on custom abort
|
|
1192
|
+
return !info.abort;
|
|
1193
|
+
});
|
|
1194
|
+
endCb(count);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Filter callback before each wrapping
|
|
1200
|
+
* @callback Mark~processMatchesAcrossFilterCallback
|
|
1201
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
1202
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
1203
|
+
* @param {string} str - The matching string
|
|
1204
|
+
* @param {Mark~filterInfoObject} filterInfo - The object containing the match information
|
|
1205
|
+
*/
|
|
1206
|
+
/**
|
|
1207
|
+
* Callback for each wrapped element
|
|
1208
|
+
* @callback Mark~processMatchesAcrossEachCallback
|
|
1209
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1210
|
+
* @param {Mark~eachInfoObject} - The object containing the match information
|
|
1211
|
+
*/
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Callback on end
|
|
1215
|
+
* @callback Mark~processMatchesAcrossEndCallback
|
|
1216
|
+
* @param {number} count - The number of all matches
|
|
1217
|
+
*/
|
|
1218
|
+
/**
|
|
1219
|
+
* Wraps the instance element and class around matches across all HTML elements in all contexts
|
|
1220
|
+
* @param {RegExp} regex - The regular expression to be searched for
|
|
1221
|
+
* @param {number} ignoreGroups - A number of RegExp capturing groups to ignore from the beginning of a match
|
|
1222
|
+
* @param {Mark~infoObject} info - The object used on filter and each callbacks
|
|
1223
|
+
* @param {Mark~processMatchesAcrossFilterCallback} filterCb - Filter callback
|
|
1224
|
+
* @param {Mark~processMatchesAcrossEachCallback} eachCb - Each callback
|
|
1225
|
+
* @param {Mark~processMatchesAcrossEndCallback} endCb
|
|
1226
|
+
* @access protected
|
|
1227
|
+
*/
|
|
1228
|
+
processMatchesAcross(regex, ignoreGroups, info, filterCb, eachCb, endCb) {
|
|
1229
|
+
const index = ignoreGroups === 0 ? 0 : ignoreGroups + 1;
|
|
1230
|
+
let count = info.count, match, str, matchStart;
|
|
1231
|
+
|
|
1232
|
+
this.getTextNodesAcross(dict => {
|
|
1233
|
+
while ((match = regex.exec(dict.text)) !== null) {
|
|
1234
|
+
// prevents an infinite loop
|
|
1235
|
+
if ((str = match[index]) === '') {
|
|
1236
|
+
regex.lastIndex++;
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
info.match = match;
|
|
1240
|
+
matchStart = true;
|
|
1241
|
+
|
|
1242
|
+
// calculates the start index inside dict.text
|
|
1243
|
+
let i = 0, start = match.index;
|
|
1244
|
+
while (++i < index) {
|
|
1245
|
+
if (match[i]) { // allows any ignore group to be undefined
|
|
1246
|
+
start += match[i].length;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
this.wrapRangeAcross(dict, start, start + str.length, nodeOrArray => { // filter
|
|
1251
|
+
info.matchStart = matchStart;
|
|
1252
|
+
matchStart = false;
|
|
1253
|
+
return filterCb(nodeOrArray, str, info);
|
|
1254
|
+
|
|
1255
|
+
}, (elemOrRange, mStart) => { // each
|
|
1256
|
+
if (mStart) count++;
|
|
1257
|
+
|
|
1258
|
+
info.count = count;
|
|
1259
|
+
info.matchStart = mStart;
|
|
1260
|
+
eachCb(elemOrRange, info);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
if (info.abort) break;
|
|
1264
|
+
}
|
|
1265
|
+
endCb(count);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Callback for each wrapped element
|
|
1271
|
+
* @callback Mark~wrapRangesEachCallback
|
|
1272
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1273
|
+
* @param {Mark~rangeObject} range - the current range object; the start and length values can be
|
|
1274
|
+
* modified if they are not numeric integers
|
|
1275
|
+
* @param {Mark~rangeInfoObject} rangeInfo - The object containing the range information
|
|
1276
|
+
*/
|
|
1277
|
+
/**
|
|
1278
|
+
* Filter callback before each wrapping
|
|
1279
|
+
* @callback Mark~wrapRangesFilterCallback
|
|
1280
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
1281
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
1282
|
+
* @param {Mark~rangeObject} range - the current range object
|
|
1283
|
+
* @param {string} substr - string extracted from the matching range
|
|
1284
|
+
* @param {number} index - The current range index ???
|
|
1285
|
+
*/
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Callback on end
|
|
1289
|
+
* @callback Mark~wrapRangesEndCallback
|
|
1290
|
+
* @param {number} count - The number of wrapped ranges
|
|
1291
|
+
* @param {Mark~logObject[]} logs - The array of objects
|
|
1292
|
+
*/
|
|
1293
|
+
/**
|
|
1294
|
+
* Wraps the indicated ranges across all HTML elements in all contexts
|
|
1295
|
+
* @param {Mark~setOfRanges} ranges
|
|
1296
|
+
* @param {Mark~wrapRangesFilterCallback} filterCb
|
|
1297
|
+
* @param {Mark~wrapRangesEachCallback} eachCb
|
|
1298
|
+
* @param {Mark~wrapRangesEndCallback} endCb
|
|
1299
|
+
* @access protected
|
|
1300
|
+
*/
|
|
1301
|
+
processRanges(ranges, filterCb, eachCb, endCb) {
|
|
1302
|
+
const lines = this.opt.markLines,
|
|
1303
|
+
logs = [],
|
|
1304
|
+
skipped = [],
|
|
1305
|
+
level = 'warn';
|
|
1306
|
+
let count = 0;
|
|
1307
|
+
|
|
1308
|
+
this.getRangesTextNodes(dict => {
|
|
1309
|
+
const max = lines ? dict.newLines.length : dict.text.length,
|
|
1310
|
+
array = this.checkRanges(ranges, logs, lines ? 1 : 0, max);
|
|
1311
|
+
|
|
1312
|
+
array.forEach((range, index) => {
|
|
1313
|
+
let start = range.start,
|
|
1314
|
+
end = start + range.length;
|
|
1315
|
+
|
|
1316
|
+
if (end > max) {
|
|
1317
|
+
// with wrapAllRanges option, there can be several report of limited ranges
|
|
1318
|
+
logs.push({ text: `Range was limited to: ${max}`, obj: range, skip: true, level });
|
|
1319
|
+
end = max;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (lines) {
|
|
1323
|
+
start = dict.newLines[start-1];
|
|
1324
|
+
if (dict.text[start] === '\n') {
|
|
1325
|
+
start++;
|
|
1326
|
+
}
|
|
1327
|
+
end = dict.newLines[end-1];
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const substr = dict.text.slice(start, end);
|
|
1331
|
+
|
|
1332
|
+
if (substr.trim()) {
|
|
1333
|
+
this.wrapRangeAcross(dict, start, end, nodeOrArray => { // filter
|
|
1334
|
+
return filterCb(nodeOrArray, range, substr, index);
|
|
1335
|
+
|
|
1336
|
+
}, (elemOrRange, rangeStart) => { // each
|
|
1337
|
+
if (rangeStart) {
|
|
1338
|
+
count++;
|
|
1339
|
+
}
|
|
1340
|
+
eachCb(elemOrRange, range, {
|
|
1341
|
+
matchStart: rangeStart,
|
|
1342
|
+
count: count
|
|
1343
|
+
});
|
|
1344
|
+
});
|
|
1345
|
+
} else {
|
|
1346
|
+
// whitespace only; even if wrapped it is not visible
|
|
1347
|
+
logs.push({ text: 'Skipping whitespace only range: ', obj: range, level });
|
|
1348
|
+
skipped.push(range);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
this.log(`Valid ranges: ${JSON.stringify(array.filter(range => !skipped.includes(range)))}`);
|
|
1353
|
+
endCb(count, logs);
|
|
1354
|
+
}, lines);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Unwraps the specified DOM node with its content (text nodes or HTML)
|
|
1359
|
+
* without destroying possibly present events (using innerHTML) and normalizes text nodes
|
|
1360
|
+
* @param {HTMLElement} node - The DOM node to unwrap
|
|
1361
|
+
* @access protected
|
|
1362
|
+
*/
|
|
1363
|
+
unwrapMatches(node) {
|
|
1364
|
+
const parent = node.parentNode,
|
|
1365
|
+
first = node.firstChild;
|
|
1366
|
+
|
|
1367
|
+
if (node.childNodes.length === 1) {
|
|
1368
|
+
// unwraps and normalizes text nodes
|
|
1369
|
+
if (first.nodeType === 3) {
|
|
1370
|
+
// the most common case - mark element with child text node
|
|
1371
|
+
const previous = node.previousSibling,
|
|
1372
|
+
next = node.nextSibling;
|
|
1373
|
+
|
|
1374
|
+
if (previous && previous.nodeType === 3) {
|
|
1375
|
+
if (next && next.nodeType === 3) {
|
|
1376
|
+
previous.nodeValue += first.nodeValue + next.nodeValue;
|
|
1377
|
+
parent.removeChild(next);
|
|
1378
|
+
|
|
1379
|
+
} else {
|
|
1380
|
+
previous.nodeValue += first.nodeValue;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
} else if (next && next.nodeType === 3) {
|
|
1384
|
+
next.nodeValue = first.nodeValue + next.nodeValue;
|
|
1385
|
+
|
|
1386
|
+
} else {
|
|
1387
|
+
parent.replaceChild(node.firstChild, node);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
parent.removeChild(node);
|
|
1391
|
+
|
|
1392
|
+
} else {
|
|
1393
|
+
// most likely is a nested mark element or modified by user element
|
|
1394
|
+
parent.replaceChild(node.firstChild, node);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
} else {
|
|
1398
|
+
if ( !first) {
|
|
1399
|
+
// an empty mark element
|
|
1400
|
+
parent.removeChild(node);
|
|
1401
|
+
|
|
1402
|
+
} else {
|
|
1403
|
+
// most likely is a nested mark element(s) with sibling text node(s) or modified by user element(s)
|
|
1404
|
+
let docFrag = this.opt.window.document.createDocumentFragment();
|
|
1405
|
+
while (node.firstChild) {
|
|
1406
|
+
docFrag.appendChild(node.removeChild(node.firstChild));
|
|
1407
|
+
}
|
|
1408
|
+
parent.replaceChild(docFrag, node);
|
|
1409
|
+
}
|
|
1410
|
+
parent.normalize();
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Callback to filter matches
|
|
1416
|
+
* @callback Mark~markRegExpFilterCallback
|
|
1417
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
1418
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
1419
|
+
* @param {string} match - The matching string:
|
|
1420
|
+
* 1) without 'ignoreGroups' and 'separateGroups' options - the whole match.
|
|
1421
|
+
* 2) with 'ignoreGroups' option - the match[ignoreGroups+1] group matching string.
|
|
1422
|
+
* 3) with 'separateGroups' option - the current group matching string
|
|
1423
|
+
* @param {number} matchesSoFar - The number of wrapped matches so far
|
|
1424
|
+
* @param {Mark~filterInfoObject} filterInfo - The object containing the match information.
|
|
1425
|
+
*/
|
|
1426
|
+
/**
|
|
1427
|
+
* Callback for each marked element
|
|
1428
|
+
* @callback Mark~markRegExpEachCallback
|
|
1429
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1430
|
+
* @param {Mark~eachInfoObject} eachInfo - The object containing the match information.
|
|
1431
|
+
*/
|
|
1432
|
+
/**
|
|
1433
|
+
* Callback if there were no matches
|
|
1434
|
+
* @callback Mark~markRegExpNoMatchCallback
|
|
1435
|
+
* @param {RegExp} regexp - The regular expression
|
|
1436
|
+
*/
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* These options also include the common options from {@link Mark~commonOptions}
|
|
1440
|
+
* @typedef Mark~markRegExpOptions
|
|
1441
|
+
* @type {object.<string>}
|
|
1442
|
+
* @property {number} [ignoreGroups=0] - A number of RegExp capturing groups to ignore from the beginning of a match
|
|
1443
|
+
* @property {boolean} [separateGroups] - Whether to mark RegExp capturing groups instead of whole match
|
|
1444
|
+
* @property {Mark~markRegExpNoMatchCallback} [noMatch]
|
|
1445
|
+
* @property {Mark~markRegExpFilterCallback} [filter]
|
|
1446
|
+
* @property {Mark~markRegExpEachCallback} [each]
|
|
1447
|
+
*/
|
|
1448
|
+
/**
|
|
1449
|
+
* Marks a custom regular expression
|
|
1450
|
+
* @param {RegExp} regexp - The regular expression
|
|
1451
|
+
* @param {Mark~markRegExpOptions} [opt] - Optional options object
|
|
1452
|
+
* @access public
|
|
1453
|
+
*/
|
|
1454
|
+
markRegExp(regexp, opt) {
|
|
1455
|
+
this.opt = opt;
|
|
1456
|
+
|
|
1457
|
+
let totalMarks = 0,
|
|
1458
|
+
matchesSoFar = 0,
|
|
1459
|
+
across = this.opt.acrossElements,
|
|
1460
|
+
fn = 'processMatches';
|
|
1461
|
+
|
|
1462
|
+
if (this.opt.separateGroups) {
|
|
1463
|
+
if ( !regexp.hasIndices) {
|
|
1464
|
+
throw new Error('Mark.js: RegExp must have a `d` flag');
|
|
1465
|
+
}
|
|
1466
|
+
fn = across ? 'processGroupsAcross' : 'processGroups';
|
|
1467
|
+
|
|
1468
|
+
} else if (across) {
|
|
1469
|
+
fn = 'processMatchesAcross';
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const info = { count: 0, abort: false };
|
|
1473
|
+
|
|
1474
|
+
// solves backward-compatibility
|
|
1475
|
+
if ( !regexp.global && !regexp.sticky) {
|
|
1476
|
+
let splits = regexp.toString().split('/');
|
|
1477
|
+
regexp = new RegExp(regexp.source, 'g' + splits[splits.length-1]);
|
|
1478
|
+
this.log('RegExp is recompiled - it must have a `g` flag', 'warn');
|
|
1479
|
+
}
|
|
1480
|
+
this.log(`RegExp "${regexp}"`);
|
|
1481
|
+
|
|
1482
|
+
this[fn](regexp, this.opt.ignoreGroups, info, (nodeOrArray, match, filterInfo) => { // filter
|
|
1483
|
+
return this.opt.filter(nodeOrArray, match, matchesSoFar, filterInfo);
|
|
1484
|
+
|
|
1485
|
+
}, (elemOrRange, eachInfo) => { // each
|
|
1486
|
+
matchesSoFar = eachInfo.count;
|
|
1487
|
+
totalMarks++;
|
|
1488
|
+
this.opt.each(elemOrRange, eachInfo);
|
|
1489
|
+
|
|
1490
|
+
}, (totalMatches) => { // done
|
|
1491
|
+
if (totalMatches === 0) {
|
|
1492
|
+
this.opt.noMatch(regexp);
|
|
1493
|
+
}
|
|
1494
|
+
this.registerHighlight();
|
|
1495
|
+
this.opt.done(totalMarks, totalMatches);
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Callback to filter matches
|
|
1501
|
+
* @callback Mark~markFilterCallback
|
|
1502
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
1503
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
1504
|
+
* @param {string} term - The current term
|
|
1505
|
+
* @param {number} matches - The number of all wrapped matches so far
|
|
1506
|
+
* @param {number} termMatches - The number of wrapped matches for the current term so far
|
|
1507
|
+
* @param {Mark~filterInfoObject} filterInfo - The object containing the match information.
|
|
1508
|
+
*/
|
|
1509
|
+
/**
|
|
1510
|
+
* Callback for each marked element
|
|
1511
|
+
* @callback Mark~markEachCallback
|
|
1512
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1513
|
+
* @param {Mark~eachInfoObject} eachInfo - The object containing the match information.
|
|
1514
|
+
*/
|
|
1515
|
+
/**
|
|
1516
|
+
* Callback if there were no matches
|
|
1517
|
+
* @callback Mark~markNoMatchCallback
|
|
1518
|
+
* @param {string[]} array - Not found search terms
|
|
1519
|
+
*/
|
|
1520
|
+
/**
|
|
1521
|
+
* Callback when finished
|
|
1522
|
+
* @callback Mark~commonDoneCallback
|
|
1523
|
+
* @param {number} totalMatches - The total number of matches
|
|
1524
|
+
* @param {object} termStats - The object containing an individual term's matches counts for {@link Mark#mark} method
|
|
1525
|
+
*/
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* These options also include the common options from {@link Mark~commonOptions}
|
|
1529
|
+
* @typedef Mark~markOptions
|
|
1530
|
+
* @type {object.<string>}
|
|
1531
|
+
* @property {boolean} [separateWordSearch=true] - Whether to break term into words
|
|
1532
|
+
* and search for individual word instead of the complete term
|
|
1533
|
+
* @property {Mark~markFilterCallback} [filter]
|
|
1534
|
+
*/
|
|
1535
|
+
/**
|
|
1536
|
+
* Marks the specified search terms
|
|
1537
|
+
* @param {string|string[]} [sv] - A search string or an array of search strings
|
|
1538
|
+
* @param {Mark~markOptions} [opt] - Optional options object
|
|
1539
|
+
* @access public
|
|
1540
|
+
*/
|
|
1541
|
+
mark(sv, opt) {
|
|
1542
|
+
this.opt = opt;
|
|
1543
|
+
const { terms, termStats } = this.getSeachTerms(sv);
|
|
1544
|
+
|
|
1545
|
+
if ( !terms.length) {
|
|
1546
|
+
this.opt.done(0, 0, termStats);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
let index = 0,
|
|
1551
|
+
totalMarks = 0,
|
|
1552
|
+
matchesSoFar = 0,
|
|
1553
|
+
term;
|
|
1554
|
+
|
|
1555
|
+
const across = this.opt.acrossElements,
|
|
1556
|
+
fn = across ? 'processMatchesAcross' : 'processMatches',
|
|
1557
|
+
array = this.getRegExps(terms),
|
|
1558
|
+
info = { count: 0, abort: false };
|
|
1559
|
+
|
|
1560
|
+
const loop = ({ regex, regTerms }) => {
|
|
1561
|
+
this.log(`RegExp ${regex}`);
|
|
1562
|
+
|
|
1563
|
+
this[fn](regex, 1, info, (nodeOrArray, _, filterInfo) => { // filter
|
|
1564
|
+
if ( !across || filterInfo.matchStart) {
|
|
1565
|
+
term = this.getCurrentTerm(filterInfo.match, regTerms);
|
|
1566
|
+
}
|
|
1567
|
+
// termStats[term] is the number of wrapped matches so far for the current term
|
|
1568
|
+
return this.opt.filter(nodeOrArray, term, matchesSoFar, termStats[term], filterInfo);
|
|
1569
|
+
|
|
1570
|
+
}, (elemOrRange, eachInfo) => { // each
|
|
1571
|
+
totalMarks++;
|
|
1572
|
+
matchesSoFar = eachInfo.count;
|
|
1573
|
+
|
|
1574
|
+
if ( !across || eachInfo.matchStart) {
|
|
1575
|
+
termStats[term] += 1;
|
|
1576
|
+
}
|
|
1577
|
+
this.opt.each(elemOrRange, eachInfo);
|
|
1578
|
+
|
|
1579
|
+
}, (totalMatches) => { // end
|
|
1580
|
+
const noMatches = regTerms.filter(term => termStats[term] === 0);
|
|
1581
|
+
if (noMatches.length) {
|
|
1582
|
+
this.opt.noMatch(noMatches);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if ( !info.abort && ++index < array.length) {
|
|
1586
|
+
loop(array[index]);
|
|
1587
|
+
} else {
|
|
1588
|
+
this.registerHighlight();
|
|
1589
|
+
this.opt.done(totalMarks, totalMatches, termStats);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
loop(array[0]);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* @param {array} match - The result of RegExp exec() method
|
|
1599
|
+
* @param {array} terms - The array of strings
|
|
1600
|
+
* @return {string} - The matched term
|
|
1601
|
+
*/
|
|
1602
|
+
getCurrentTerm(match, terms) {
|
|
1603
|
+
// it's better to search from the end of array because the terms are sorted by
|
|
1604
|
+
// length in descending order - shorter term appears more frequently
|
|
1605
|
+
let i = match.length;
|
|
1606
|
+
while (--i > 2) {
|
|
1607
|
+
// the current term index is the first non-undefined capturing group index minus 3
|
|
1608
|
+
if (match[i]) {
|
|
1609
|
+
// the first 3 groups are: match[0], lookbehind, and main group
|
|
1610
|
+
return terms[i-3];
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return ' ';
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Splits an array of strings into chunks by the specified number and creates RegExp from each chunk
|
|
1618
|
+
* @param {array} terms - The array of strings
|
|
1619
|
+
* @return {array} - The array of arrays with RegExp and its term chunks
|
|
1620
|
+
*/
|
|
1621
|
+
getRegExps(terms) {
|
|
1622
|
+
const creator = new RegExpCreator(this.opt),
|
|
1623
|
+
option = this.opt.combineBy || this.opt.combinePatterns,
|
|
1624
|
+
length = terms.length,
|
|
1625
|
+
array = [];
|
|
1626
|
+
let num = 100,
|
|
1627
|
+
value;
|
|
1628
|
+
|
|
1629
|
+
if (option === Infinity) {
|
|
1630
|
+
num = length;
|
|
1631
|
+
} else if ( !isNaN(+option) && (value = parseInt(option)) > 0) {
|
|
1632
|
+
num = value;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
for (let i = 0; i < length; i += num) {
|
|
1636
|
+
// get a chunk of terms to create combine pattern
|
|
1637
|
+
const chunk = terms.slice(i, Math.min(i + num, length));
|
|
1638
|
+
array.push({ regex: creator.create(chunk), regTerms: chunk });
|
|
1639
|
+
}
|
|
1640
|
+
return array;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* @typedef Mark~rangeObject
|
|
1645
|
+
* @type {object}
|
|
1646
|
+
* @property {number} start - The start index within the composite string
|
|
1647
|
+
* @property {number} length - The length of the string to mark within the composite string.
|
|
1648
|
+
*/
|
|
1649
|
+
/**
|
|
1650
|
+
* @typedef Mark~setOfRanges
|
|
1651
|
+
* @type {object[]}
|
|
1652
|
+
* @property {Mark~rangeObject}
|
|
1653
|
+
*/
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* @typedef Mark~rangeInfoObject
|
|
1657
|
+
* @type {object}
|
|
1658
|
+
* @property {boolean} matchStart - Indicate the start of range
|
|
1659
|
+
* @property {number} count - The current number of wrapped ranges
|
|
1660
|
+
*/
|
|
1661
|
+
/**
|
|
1662
|
+
* These options also include the common options from {@link Mark~commonOptions}
|
|
1663
|
+
* @typedef Mark~markRangesOptions
|
|
1664
|
+
* @type {object.<string>}
|
|
1665
|
+
* @property {Mark~markRangesEachCallback} [each]
|
|
1666
|
+
* @property {Mark~markRangesNoMatchCallback} [noMatch]
|
|
1667
|
+
* @property {Mark~markRangesFilterCallback} [filter]
|
|
1668
|
+
*/
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* Callback to filter matches
|
|
1672
|
+
* @callback Mark~markRangesFilterCallback
|
|
1673
|
+
* @param {Text|Text[]} nodeOrArray - The current text node or an array of text nodes when using the Highlight API
|
|
1674
|
+
* with the options 'acrossElements: true' and 'rangeAcrossElements: true'
|
|
1675
|
+
* @param {Mark~rangeObject} range - The range object
|
|
1676
|
+
* @param {string} match - The current range matching string
|
|
1677
|
+
* @param {number} index - The current range index ???
|
|
1678
|
+
*/
|
|
1679
|
+
/**
|
|
1680
|
+
* Callback for each marked element
|
|
1681
|
+
* @callback Mark~markRangesEachCallback
|
|
1682
|
+
* @param {HTMLElement|StaticRange|Range} elemOrRange - The marked DOM element or range (Highlight API)
|
|
1683
|
+
* @param {Mark~rangeObject} range - The range object
|
|
1684
|
+
* @param {Mark~rangeInfoObject} - The object containing the range information
|
|
1685
|
+
*/
|
|
1686
|
+
/**
|
|
1687
|
+
* Callback if a processed range is invalid, out-of-bounds, overlaps another
|
|
1688
|
+
* range, or only matches whitespace
|
|
1689
|
+
* @callback Mark~markRangesNoMatchCallback
|
|
1690
|
+
* @param {Mark~rangeObject} range - The range object
|
|
1691
|
+
*/
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Marks an array of objects containing start and length properties
|
|
1695
|
+
* @param {Mark~setOfRanges} ranges - The original array of objects
|
|
1696
|
+
* @param {Mark~markRangesOptions} [opt] - Optional options object
|
|
1697
|
+
* @access public
|
|
1698
|
+
*/
|
|
1699
|
+
markRanges(ranges, opt) {
|
|
1700
|
+
this.opt = opt;
|
|
1701
|
+
|
|
1702
|
+
if (Array.isArray(ranges)) {
|
|
1703
|
+
let totalMarks = 0;
|
|
1704
|
+
|
|
1705
|
+
this.processRanges(ranges, (nodeOrArray, range, match, index) => { // filter
|
|
1706
|
+
return this.opt.filter(nodeOrArray, range, match, index);
|
|
1707
|
+
|
|
1708
|
+
}, (elemOrRange, range, rangeInfo) => { // each
|
|
1709
|
+
totalMarks++;
|
|
1710
|
+
this.opt.each(elemOrRange, range, rangeInfo);
|
|
1711
|
+
|
|
1712
|
+
}, (totalRanges, logs) => { // end
|
|
1713
|
+
this.report(logs);
|
|
1714
|
+
this.registerHighlight();
|
|
1715
|
+
this.opt.done(totalMarks, totalRanges);
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
} else {
|
|
1719
|
+
this.report([{ text: 'markRanges() accept an array of objects: ', obj: ranges, level: 'error' }]);
|
|
1720
|
+
this.opt.done(0, 0);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* Iterates over specified names or the default name, if the HighlightRegistry contains name,
|
|
1726
|
+
* it deletes all (if the 'exclude' option is specified according this option) ranges from the Highlight object;
|
|
1727
|
+
* next, if allowed, unwraps all marked elements inside the context and normalizes text nodes
|
|
1728
|
+
* @param {Mark~commonOptions} [opt] - Optional options object without each,
|
|
1729
|
+
* noMatch and acrossElements properties
|
|
1730
|
+
* @access public
|
|
1731
|
+
*/
|
|
1732
|
+
unmark(opt) {
|
|
1733
|
+
this.opt = opt;
|
|
1734
|
+
const registry = CSS.highlights,
|
|
1735
|
+
exclude = this.opt.exclude && this.opt.exclude.length;
|
|
1736
|
+
// if the browser supports the Highlight API
|
|
1737
|
+
if (registry) {
|
|
1738
|
+
let names = this.opt.highlightName,
|
|
1739
|
+
highlight;
|
|
1740
|
+
if (typeof names === 'string') names = [names];
|
|
1741
|
+
|
|
1742
|
+
names.forEach((name) => {
|
|
1743
|
+
if ((highlight = registry.get(name)) && highlight.size) {
|
|
1744
|
+
// unregister the Highlight object before deleting ranges
|
|
1745
|
+
registry.delete(name);
|
|
1746
|
+
// iterates over highlight when 'exclude' option is specified
|
|
1747
|
+
if (exclude) {
|
|
1748
|
+
highlight.forEach((range) => {
|
|
1749
|
+
let node = range.startContainer;
|
|
1750
|
+
|
|
1751
|
+
if (node.nodeType === 3) node = node.parentNode;
|
|
1752
|
+
|
|
1753
|
+
if ( !this.excluded(node)) highlight.delete(range);
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
} else {
|
|
1757
|
+
// much faster way to remove highlights
|
|
1758
|
+
highlight.clear();
|
|
1759
|
+
}
|
|
1760
|
+
// register the Highlight object with excluded ranges
|
|
1761
|
+
if (highlight.size) registry.set(name, highlight);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
// removes only StaticRange/Range objects
|
|
1766
|
+
if (this.opt.highlight) {
|
|
1767
|
+
this.opt.done();
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
let selector = this.opt.element + '[data-markjs]';
|
|
1772
|
+
|
|
1773
|
+
if (this.opt.className) {
|
|
1774
|
+
selector += `.${this.opt.className}`;
|
|
1775
|
+
}
|
|
1776
|
+
this.log(`Removal selector "${selector}"`);
|
|
1777
|
+
|
|
1778
|
+
this.iterator.forEachNode(this.filter.SHOW_ELEMENT, node => { // each
|
|
1779
|
+
this.unwrapMatches(node);
|
|
1780
|
+
}, node => { // filter
|
|
1781
|
+
return DOMIterator.matches(node, selector) && !(exclude && this.excluded(node));
|
|
1782
|
+
}, this.opt.done);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Registers a Highlight object using the HighlightRegistry
|
|
1787
|
+
*/
|
|
1788
|
+
registerHighlight() {
|
|
1789
|
+
const highlight = this.opt.highlight;
|
|
1790
|
+
|
|
1791
|
+
if (highlight) {
|
|
1792
|
+
const name = this.opt.highlightName,
|
|
1793
|
+
// eslint-disable-next-line
|
|
1794
|
+
registry = CSS.highlights;
|
|
1795
|
+
|
|
1796
|
+
if (this.rangeArray.length) {
|
|
1797
|
+
registry.delete(name);
|
|
1798
|
+
|
|
1799
|
+
if (highlight.size) {
|
|
1800
|
+
highlight.forEach(range => {
|
|
1801
|
+
this.rangeArray.push(range);
|
|
1802
|
+
});
|
|
1803
|
+
highlight.clear();
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
this.rangeArray.sort((a, b) => a.absoluteOffset - b.absoluteOffset);
|
|
1807
|
+
this.rangeArray.forEach(range => {
|
|
1808
|
+
highlight.add(range);
|
|
1809
|
+
});
|
|
1810
|
+
this.rangeArray = [];
|
|
1811
|
+
}
|
|
1812
|
+
if (highlight.size) registry.set(name, highlight);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
export default Mark;
|