PageSourceSearch

https://zoar-sulam-rus.org/wp-content/plugins/bluet-keywords-toolt…/library/findandreplacedomtext.js

js zoar-sulam-rus.org collected 2026-10-03 00:24:51 UTC 17,525 bytes, 636 lines download raw bytes

1/**
2 * findAndReplaceDOMText v 0.4.6
3 * @author James Padolsey http://james.padolsey.com
4 * @license http://unlicense.org/UNLICENSE
5 *
6 * Matches the text of a DOM node against a regular expression
7 * and replaces each match (or node-separated portions of the match)
8 * in the specified element.
9 */
10(function (root, factory) {
11	if (typeof module === 'object' && module.exports) {
12		// Node/CommonJS
13		module.exports = factory();
14	} else if (typeof define === 'function' && define.amd) {
15		// AMD. Register as an anonymous module.
16		define(factory);
17	} else {
18		// Browser globals
19		root.findAndReplaceDOMText = factory();
20	}
21}(this, function factory() {
22
23   var PORTION_MODE_RETAIN = 'retain';
24   var PORTION_MODE_FIRST = 'first';
25
26   var doc = document;
27   var hasOwn = {}.hasOwnProperty;
28
29   function escapeRegExp(s) {
30	   return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
31   }
32
33   function exposed() {
34	   // Try deprecated arg signature first:
35	   return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments);
36   }
37
38   function deprecated(regex, node, replacement, captureGroup, elFilter) {
39	   if ((node && !node.nodeType) && arguments.length <= 2) {
40		   return false;
41	   }
42	   var isReplacementFunction = typeof replacement == 'function';
43
44	   if (isReplacementFunction) {
45		   replacement = (function(original) {
46			   return function(portion, match) {
47				   return original(portion.text, match.startIndex);
48			   };
49		   }(replacement));
50	   }
51
52	   // Awkward support for deprecated argument signature (<0.4.0)
53	   var instance = findAndReplaceDOMText(node, {
54
55		   find: regex,
56
57		   wrap: isReplacementFunction ? null : replacement,
58		   replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'),
59
60		   prepMatch: function(m, mi) {
61
62			   // Support captureGroup (a deprecated feature)
63
64			   if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches';
65
66			   if (captureGroup > 0) {
67				   var cg = m[captureGroup];
68				   m.index += m[0].indexOf(cg);
69				   m[0] = cg;
70			   }
71
72			   m.endIndex = m.index + m[0].length;
73			   m.startIndex = m.index;
74			   m.index = mi;
75
76			   return m;
77		   },
78		   filterElements: elFilter
79	   });
80
81	   exposed.revert = function() {
82		   return instance.revert();
83	   };
84
85	   return true;
86   }
87
88   /**
89	* findAndReplaceDOMText
90	*
91	* Locates matches and replaces with replacementNode
92	*
93	* @param {Node} node Element or Text node to search within
94	* @param {RegExp} options.find The regular expression to match
95	* @param {String|Element} [options.wrap] A NodeName, or a Node to clone
96	* @param {String} [options.wrapClass] A classname to append to the wrapping element
97	* @param {String|Function} [options.replace='$&'] What to replace each match with
98	* @param {Function} [options.filterElements] A Function to be called to check whether to
99	*	process an element. (returning true = process element,
100	*	returning false = avoid element)
101	*/
102   function findAndReplaceDOMText(node, options) {
103	   return new Finder(node, options);
104   }
105
106   exposed.NON_PROSE_ELEMENTS = {
107	   br:1, hr:1,
108	   // Media / Source elements:
109	   script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
110	   // Input elements
111	   input:1, textarea:1, select:1, option:1, optgroup: 1, button:1
112   };
113
114   exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = {
115
116	   // Elements that will not contain prose or block elements where we don't
117	   // want prose to be matches across element borders:
118
119	   // Block Elements
120	   address:1, article:1, aside:1, blockquote:1, dd:1, div:1,
121	   dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1,
122	   h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1,
123	   output:1, p:1, pre:1, section:1, ul:1,
124	   // Other misc. elements that are not part of continuous inline prose:
125	   br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1,
126	   // Media / Source elements:
127	   script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
128	   // Input elements
129	   input:1, textarea:1, select:1, option:1, optgroup:1, button:1,
130	   // Table related elements:
131	   table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1
132
133   };
134
135   exposed.NON_INLINE_PROSE = function(el) {
136	   return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase());
137   };
138
139   // Presets accessed via `options.preset` when calling findAndReplaceDOMText():
140   exposed.PRESETS = {
141	   prose: {
142		   forceContext: exposed.NON_INLINE_PROSE,
143		   filterElements: function(el) {
144			   return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase());
145		   }
146	   }
147   };
148
149   exposed.Finder = Finder;
150
151   /**
152	* Finder -- encapsulates logic to find and replace.
153	*/
154   function Finder(node, options) {
155
156	   var preset = options.preset && exposed.PRESETS[options.preset];
157
158	   options.portionMode = options.portionMode || PORTION_MODE_RETAIN;
159
160	   if (preset) {
161		   for (var i in preset) {
162			   if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) {
163				   options[i] = preset[i];
164			   }
165		   }
166	   }
167
168	   this.node = node;
169	   this.options = options;
170
171	   // Enable match-preparation method to be passed as option:
172	   this.prepMatch = options.prepMatch || this.prepMatch;
173
174	   this.reverts = [];
175
176	   this.matches = this.search();
177
178	   if (this.matches.length) {
179		   this.processMatches();
180	   }
181
182   }
183
184   Finder.prototype = {
185
186	   /**
187		* Searches for all matches that comply with the instance's 'match' option
188		*/
189	   search: function() {
190
191		   var match;
192		   var matchIndex = 0;
193		   var offset = 0;
194		   var regex = this.options.find;
195		   var textAggregation = this.getAggregateText();
196		   var matches = [];
197		   var self = this;
198
199		   regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;
200
201		   matchAggregation(textAggregation);
202
203		   function matchAggregation(textAggregation) {
204			   for (var i = 0, l = textAggregation.length; i < l; ++i) {
205
206				   var text = textAggregation[i];
207
208				   if (typeof text !== 'string') {
209					   // Deal with nested contexts: (recursive)
210					   matchAggregation(text);
211					   continue;
212				   }
213
214				   if (regex.global) {
215					   while (match = regex.exec(text)) {
216						   matches.push(self.prepMatch(match, matchIndex++, offset));
217					   }
218				   } else {
219					   if (match = text.match(regex)) {
220						   matches.push(self.prepMatch(match, 0, offset));
221					   }
222				   }
223
224				   offset += text.length;
225			   }
226		   }
227
228		   return matches;
229
230	   },
231
232	   /**
233		* Prepares a single match with useful meta info:
234		*/
235	   prepMatch: function(match, matchIndex, characterOffset) {
236
237		   if (!match[0]) {
238			   throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
239		   }
240
241		   match.endIndex = characterOffset + match.index + match[0].length;
242		   match.startIndex = characterOffset + match.index;
243		   match.index = matchIndex;
244
245		   return match;
246	   },
247
248	   /**
249		* Gets aggregate text within subject node
250		*/
251	   getAggregateText: function() {
252
253		   var elementFilter = this.options.filterElements;
254		   var forceContext = this.options.forceContext;
255
256		   return getText(this.node);
257
258		   /**
259			* Gets aggregate text of a node without resorting
260			* to broken innerText/textContent
261			*/
262		   function getText(node) {
263
264			   if (node.nodeType === Node.TEXT_NODE) {
265				   return [node.data];
266			   }
267
268			   if (elementFilter && !elementFilter(node)) {
269				   return [];
270			   }
271
272			   var txt = [''];
273			   var i = 0;
274
275			   if (node = node.firstChild) do {
276
277				   if (node.nodeType === Node.TEXT_NODE) {
278					   txt[i] += node.data;
279					   continue;
280				   }
281
282				   var innerText = getText(node);
283
284				   if (
285					   forceContext &&
286					   node.nodeType === Node.ELEMENT_NODE &&
287					   (forceContext === true || forceContext(node))
288				   ) {
289					   txt[++i] = innerText;
290					   txt[++i] = '';
291				   } else {
292					   if (typeof innerText[0] === 'string') {
293						   // Bridge nested text-node data so that they're
294						   // not considered their own contexts:
295						   // I.e. ['some', ['thing']] -> ['something']
296						   txt[i] += innerText.shift();
297					   }
298					   if (innerText.length) {
299						   txt[++i] = innerText;
300						   txt[++i] = '';
301					   }
302				   }
303			   } while (node = node.nextSibling);
304
305			   return txt;
306
307		   }
308
309	   },
310
311	   /**
312		* Steps through the target node, looking for matches, and
313		* calling replaceFn when a match is found.
314		*/
315	   processMatches: function() {
316
317		   var matches = this.matches;
318		   var node = this.node;
319		   var elementFilter = this.options.filterElements;
320
321		   var startPortion,
322			   endPortion,
323			   innerPortions = [],
324			   curNode = node,
325			   match = matches.shift(),
326			   atIndex = 0, // i.e. nodeAtIndex
327			   matchIndex = 0,
328			   portionIndex = 0,
329			   doAvoidNode,
330			   nodeStack = [node];
331
332		   out: while (true) {
333
334			   if (curNode.nodeType === Node.TEXT_NODE) {
335
336				   if (!endPortion && curNode.length + atIndex >= match.endIndex) {
337					   // We've found the ending
338					   // (Note that, in the case of a single portion, it'll be an
339					   // endPortion, not a startPortion.)
340					   endPortion = {
341						   node: curNode,
342						   index: portionIndex++,
343						   text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
344
345						   // If it's the first match (atIndex==0) we should just return 0
346						   indexInMatch: atIndex === 0 ? 0 : atIndex - match.startIndex,
347
348						   indexInNode: match.startIndex - atIndex,
349						   endIndexInNode: match.endIndex - atIndex,
350						   isEnd: true
351					   };
352
353				   } else if (startPortion) {
354					   // Intersecting node
355					   innerPortions.push({
356						   node: curNode,
357						   index: portionIndex++,
358						   text: curNode.data,
359						   indexInMatch: atIndex - match.startIndex,
360						   indexInNode: 0 // always zero for inner-portions
361					   });
362				   }
363
364				   if (!startPortion && curNode.length + atIndex > match.startIndex) {
365					   // We've found the match start
366					   startPortion = {
367						   node: curNode,
368						   index: portionIndex++,
369						   indexInMatch: 0,
370						   indexInNode: match.startIndex - atIndex,
371						   endIndexInNode: match.endIndex - atIndex,
372						   text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
373					   };
374				   }
375
376				   atIndex += curNode.data.length;
377
378			   }
379
380			   doAvoidNode = curNode.nodeType === Node.ELEMENT_NODE && elementFilter && !elementFilter(curNode);
381
382			   if (startPortion && endPortion) {
383
384				   curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion);
385
386				   // processMatches has to return the node that replaced the endNode
387				   // and then we step back so we can continue from the end of the
388				   // match:
389
390				   atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode);
391
392				   startPortion = null;
393				   endPortion = null;
394				   innerPortions = [];
395				   match = matches.shift();
396				   portionIndex = 0;
397				   matchIndex++;
398
399				   if (!match) {
400					   break; // no more matches
401				   }
402
403			   } else if (
404				   !doAvoidNode &&
405				   (curNode.firstChild || curNode.nextSibling)
406			   ) {
407				   // Move down or forward:
408				   if (curNode.firstChild) {
409					   nodeStack.push(curNode);
410					   curNode = curNode.firstChild;
411				   } else {
412					   curNode = curNode.nextSibling;
413				   }
414				   continue;
415			   }
416
417			   // Move forward or up:
418			   while (true) {
419				   if (curNode.nextSibling) {
420					   curNode = curNode.nextSibling;
421					   break;
422				   }
423				   curNode = nodeStack.pop();
424				   if (curNode === node) {
425					   break out;
426				   }
427			   }
428
429		   }
430
431	   },
432
433	   /**
434		* Reverts ... TODO
435		*/
436	   revert: function() {
437		   // Reversion occurs backwards so as to avoid nodes subsequently
438		   // replaced during the matching phase (a forward process):
439		   for (var l = this.reverts.length; l--;) {
440			   this.reverts[l]();
441		   }
442		   this.reverts = [];
443	   },
444
445	   prepareReplacementString: function(string, portion, match) {
446		   var portionMode = this.options.portionMode;
447		   if (
448			   portionMode === PORTION_MODE_FIRST &&
449			   portion.indexInMatch > 0
450		   ) {
451			   return '';
452		   }
453		   string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
454			   var replacement;
455			   switch(t) {
456				   case '&':
457					   replacement = match[0];
458					   break;
459				   case '`':
460					   replacement = match.input.substring(0, match.startIndex);
461					   break;
462				   case '\'':
463					   replacement = match.input.substring(match.endIndex);
464					   break;
465				   default:
466					   replacement = match[+t] || '';
467			   }
468			   return replacement;
469		   });
470
471		   if (portionMode === PORTION_MODE_FIRST) {
472			   return string;
473		   }
474
475		   if (portion.isEnd) {
476			   return string.substring(portion.indexInMatch);
477		   }
478
479		   return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length);
480	   },
481
482	   getPortionReplacementNode: function(portion, match) {
483
484		   var replacement = this.options.replace || '$&';
485		   var wrapper = this.options.wrap;
486		   var wrapperClass = this.options.wrapClass;
487
488		   if (wrapper && wrapper.nodeType) {
489			   // Wrapper has been provided as a stencil-node for us to clone:
490			   var clone = doc.createElement('div');
491			   clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper);
492			   wrapper = clone.firstChild;
493		   }
494
495		   if (typeof replacement == 'function') {
496			   replacement = replacement(portion, match);
497			   if (replacement && replacement.nodeType) {
498				   return replacement;
499			   }
500			   return doc.createTextNode(String(replacement));
501		   }
502
503		   var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper;
504
505			if (el && wrapperClass) {
506			   el.className = wrapperClass;
507		   }
508
509		   replacement = doc.createTextNode(
510			   this.prepareReplacementString(
511				   replacement, portion, match
512			   )
513		   );
514
515		   if (!replacement.data) {
516			   return replacement;
517		   }
518
519		   if (!el) {
520			   return replacement;
521		   }
522
523		   el.appendChild(replacement);
524
525		   return el;
526	   },
527
528	   replaceMatch: function(match, startPortion, innerPortions, endPortion) {
529
530		   var matchStartNode = startPortion.node;
531		   var matchEndNode = endPortion.node;
532
533		   var precedingTextNode;
534		   var followingTextNode;
535
536		   if (matchStartNode === matchEndNode) {
537
538			   var node = matchStartNode;
539
540			   if (startPortion.indexInNode > 0) {
541				   // Add `before` text node (before the match)
542				   precedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode));
543				   node.parentNode.insertBefore(precedingTextNode, node);
544			   }
545
546			   // Create the replacement node:
547			   var newNode = this.getPortionReplacementNode(
548				   endPortion,
549				   match
550			   );
551
552			   node.parentNode.insertBefore(newNode, node);
553
554			   if (endPortion.endIndexInNode < node.length) { // ?????
555				   // Add `after` text node (after the match)
556				   followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode));
557				   node.parentNode.insertBefore(followingTextNode, node);
558			   }
559
560			   node.parentNode.removeChild(node);
561
562			   this.reverts.push(function() {
563				   if (precedingTextNode === newNode.previousSibling) {
564					   precedingTextNode.parentNode.removeChild(precedingTextNode);
565				   }
566				   if (followingTextNode === newNode.nextSibling) {
567					   followingTextNode.parentNode.removeChild(followingTextNode);
568				   }
569				   newNode.parentNode.replaceChild(node, newNode);
570			   });
571
572			   return newNode;
573
574		   } else {
575			   // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
576
577
578			   precedingTextNode = doc.createTextNode(
579				   matchStartNode.data.substring(0, startPortion.indexInNode)
580			   );
581
582			   followingTextNode = doc.createTextNode(
583				   matchEndNode.data.substring(endPortion.endIndexInNode)
584			   );
585
586			   var firstNode = this.getPortionReplacementNode(
587				   startPortion,
588				   match
589			   );
590
591			   var innerNodes = [];
592
593			   for (var i = 0, l = innerPortions.length; i < l; ++i) {
594				   var portion = innerPortions[i];
595				   var innerNode = this.getPortionReplacementNode(
596					   portion,
597					   match
598				   );
599				   portion.node.parentNode.replaceChild(innerNode, portion.node);
600				   this.reverts.push((function(portion, innerNode) {
601					   return function() {
602						   innerNode.parentNode.replaceChild(portion.node, innerNode);
603					   };
604				   }(portion, innerNode)));
605				   innerNodes.push(innerNode);
606			   }
607
608			   var lastNode = this.getPortionReplacementNode(
609				   endPortion,
610				   match
611			   );
612
613			   matchStartNode.parentNode.insertBefore(precedingTextNode, matchStartNode);
614			   matchStartNode.parentNode.insertBefore(firstNode, matchStartNode);
615			   matchStartNode.parentNode.removeChild(matchStartNode);
616
617			   matchEndNode.parentNode.insertBefore(lastNode, matchEndNode);
618			   matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode);
619			   matchEndNode.parentNode.removeChild(matchEndNode);
620
621			   this.reverts.push(function() {
622				   precedingTextNode.parentNode.removeChild(precedingTextNode);
623				   firstNode.parentNode.replaceChild(matchStartNode, firstNode);
624				   followingTextNode.parentNode.removeChild(followingTextNode);
625				   lastNode.parentNode.replaceChild(matchEndNode, lastNode);
626			   });
627
628			   return lastNode;
629		   }
630	   }
631
632   };
633
634   return exposed;
635
636}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.