https://zoar-sulam-rus.org/wp-content/plugins/bluet-keywords-toolt…/library/findandreplacedomtext.js
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.