1/** 2 * A NodeIterator with iframes support and a method to check if an element is 3 * matching a specified selector 4 * @example 5 * const iterator = new DOMIterator( 6 * document.querySelector("#context"), true 7 * ); 8 * iterator.forEachNode(NodeFilter.SHOW_TEXT, node => { 9 * console.log(node); 10 * }, node => { 11 * if(DOMIterator.matches(node.parentNode, ".ignore")){ 12 * return NodeFilter.FILTER_REJECT; 13 * } else { 14 * return NodeFilter.FILTER_ACCEPT; 15 * } 16 * }, () => { 17 * console.log("DONE"); 18 * }); 19 * @todo Outsource into separate repository 20 */ 21class DOMIterator { 22 /** 23 * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM 24 * element, an array of DOM elements, a NodeList or a selector 25 * @param {boolean} [iframes=true] - A boolean indicating if iframes should 26 * be handled 27 * @param {string[]} [exclude=[]] - An array containing exclusion selectors 28 * for iframes 29 * @param {number} [iframesTimeout=5000] - A number indicating the ms to 30 * wait before an iframe should be skipped, in case the load event isn't 31 * fired. This also applies if the user is offline and the resource of the 32 * iframe is online (either by the browsers "offline" mode or because 33 * there's no internet connection) 34 */ 35 constructor(ctx, iframes = true, exclude = [], iframesTimeout = 5000) { 36 /** 37 * The context of the instance. Either a DOM element, an array of DOM 38 * elements, a NodeList or a selector 39 * @type {HTMLElement|HTMLElement[]|NodeList|string} 40 * @access protected 41 */ 42 this.ctx = ctx; 43 /** 44 * Boolean indicating if iframe support is enabled 45 * @type {boolean} 46 * @access protected 47 */ 48 49 this.iframes = iframes; 50 /** 51 * An array containing exclusion selectors for iframes 52 * @type {string[]} 53 */ 54 55 this.exclude = exclude; 56 /** 57 * The maximum ms to wait for a load event before skipping an iframe 58 * @type {number} 59 */ 60 61 this.iframesTimeout = iframesTimeout; 62 } 63 /** 64 * Checks if the specified DOM element matches the selector 65 * @param {HTMLElement} element - The DOM element 66 * @param {string|string[]} selector - The selector or an array with 67 * selectors 68 * @return {boolean} 69 * @access public 70 */ 71 72 static matches(element, selector) { 73 const selectors = typeof selector === "string" ? [selector] : selector, 74 fn = 75 element.matches || 76 element.matchesSelector || 77 element.msMatchesSelector || 78 element.mozMatchesSelector || 79 element.oMatchesSelector || 80 element.webkitMatchesSelector; 81 82 if (fn) { 83 let match = false; 84 selectors.every((sel) => { 85 if (fn.call(element, sel)) { 86 match = true; 87 return false; 88 } 89 90 return true; 91 }); 92 return match; 93 } else { 94 // may be false e.g. when el is a textNode 95 return false; 96 } 97 } 98 /** 99 * Returns all contexts filtered by duplicates (even nested) 100 * @return {HTMLElement[]} - An array containing DOM contexts 101 * @access protected 102 */ 103 104 getContexts() {
vendor: 2,237 bytes, lines 105-182
105 let ctx, 106 filteredCtx = []; 107 108 if (typeof this.ctx === "undefined" || !this.ctx) { 109 // e.g. null 110 ctx = []; 111 } else if (NodeList.prototype.isPrototypeOf(this.ctx)) { 112 ctx = Array.prototype.slice.call(this.ctx); 113 } else if (Array.isArray(this.ctx)) { 114 ctx = this.ctx; 115 } else if (typeof this.ctx === "string") { 116 ctx = Array.prototype.slice.call(document.querySelectorAll(this.ctx)); 117 } else { 118 // e.g. HTMLElement or element inside iframe 119 ctx = [this.ctx]; 120 } // filter duplicate text nodes 121 122 ctx.forEach((ctx) => { 123 const isDescendant = 124 filteredCtx.filter((contexts) => { 125 return contexts.contains(ctx); 126 }).length > 0; 127 128 if (filteredCtx.indexOf(ctx) === -1 && !isDescendant) { 129 filteredCtx.push(ctx); 130 } 131 }); 132 return filteredCtx; 133 } 134 /** 135 * @callback DOMIterator~getIframeContentsSuccessCallback 136 * @param {HTMLDocument} contents - The contentDocument of the iframe 137 */ 138 139 /** 140 * Calls the success callback function with the iframe document. If it can't 141 * be accessed it calls the error callback function 142 * @param {HTMLElement} ifr - The iframe DOM element 143 * @param {DOMIterator~getIframeContentsSuccessCallback} successFn 144 * @param {function} [errorFn] 145 * @access protected 146 */ 147 148 getIframeContents(ifr, successFn, errorFn = () => {}) { 149 let doc; 150 151 try { 152 const ifrWin = ifr.contentWindow; 153 doc = ifrWin.document; 154 155 if (!ifrWin || !doc) { 156 // no permission = null. Undefined in Phantom 157 throw new Error("iframe inaccessible"); 158 } 159 } catch (e) { 160 errorFn(); 161 } 162 163 if (doc) { 164 successFn(doc); 165 } 166 } 167 /** 168 * Checks if an iframe is empty (if about:blank is the shown page) 169 * @param {HTMLElement} ifr - The iframe DOM element 170 * @return {boolean} 171 * @access protected 172 */ 173 174 isIframeBlank(ifr) { 175 const bl = "about:blank", 176 src = ifr.getAttribute("src").trim(), 177 href = ifr.contentWindow.location.href; 178 return href === bl && src !== bl && src; 179 } 180 /** 181 * Observes the onload event of an iframe and calls the success callback or 182 * the error callback if the iframe is inaccessible. If the event isn't
183 * fired within the specified {@link DOMIterator#iframesTimeout}, then it'll 184 * call the error callback too 185 * @param {HTMLElement} ifr - The iframe DOM element 186 * @param {DOMIterator~getIframeContentsSuccessCallback} successFn 187 * @param {function} errorFn 188 * @access protected 189 */ 190 191 observeIframeLoad(ifr, successFn, errorFn) { 192 let called = false, 193 tout = null; 194 195 const listener = () => { 196 if (called) { 197 return; 198 } 199 200 called = true; 201 clearTimeout(tout); 202 203 try { 204 if (!this.isIframeBlank(ifr)) { 205 ifr.removeEventListener("load", listener); 206 this.getIframeContents(ifr, successFn, errorFn); 207 } 208 } catch (e) { 209 // isIframeBlank maybe throws throws an error 210 errorFn(); 211 } 212 }; 213 214 ifr.addEventListener("load", listener); 215 tout = setTimeout(listener, this.iframesTimeout); 216 } 217 /** 218 * Callback when the iframe is ready 219 * @callback DOMIterator~onIframeReadySuccessCallback 220 * @param {HTMLDocument} contents - The contentDocument of the iframe 221 */ 222 223 /** 224 * Callback if the iframe can't be accessed 225 * @callback DOMIterator~onIframeReadyErrorCallback 226 */ 227 228 /** 229 * Calls the callback if the specified iframe is ready for DOM access 230 * @param {HTMLElement} ifr - The iframe DOM element 231 * @param {DOMIterator~onIframeReadySuccessCallback} successFn - Success 232 * callback 233 * @param {DOMIterator~onIframeReadyErrorCallback} errorFn - Error callback 234 * @see {@link http://stackoverflow.com/a/36155560/3894981} for 235 * background information 236 * @access protected 237 */ 238 239 onIframeReady(ifr, successFn, errorFn) { 240 try { 241 if (ifr.contentWindow.document.readyState === "complete") { 242 if (this.isIframeBlank(ifr)) { 243 this.observeIframeLoad(ifr, successFn, errorFn); 244 } else { 245 this.getIframeContents(ifr, successFn, errorFn); 246 } 247 } else { 248 this.observeIframeLoad(ifr, successFn, errorFn); 249 } 250 } catch (e) { 251 // accessing document failed 252 errorFn(); 253 } 254 } 255 /** 256 * Callback when all iframes are ready for DOM access 257 * @callback DOMIterator~waitForIframesDoneCallback 258 */ 259 260 /** 261 * Iterates over all iframes and calls the done callback when all of them 262 * are ready for DOM access (including nested ones) 263 * @param {HTMLElement} ctx - The context DOM element 264 * @param {DOMIterator~waitForIframesDoneCallback} done - Done callback 265 */ 266 267 waitForIframes(ctx, done) { 268 let eachCalled = 0; 269 this.forEachIframe( 270 ctx, 271 () => true, 272 (ifr) => { 273 eachCalled++; 274 this.waitForIframes(ifr.querySelector("html"), () => { 275 if (!--eachCalled) { 276 done(); 277 } 278 }); 279 }, 280 (handled) => { 281 if (!handled) { 282 done(); 283 } 284 } 285 ); 286 } 287 /** 288 * Callback allowing to filter an iframe. Must return true when the element 289 * should remain, otherwise false 290 * @callback DOMIterator~forEachIframeFilterCallback 291 * @param {HTMLElement} iframe - The iframe DOM element 292 */ 293 294 /** 295 * Callback for each iframe content 296 * @callback DOMIterator~forEachIframeEachCallback 297 * @param {HTMLElement} content - The iframe document 298 */ 299 300 /** 301 * Callback if all iframes inside the context were handled 302 * @callback DOMIterator~forEachIframeEndCallback 303 * @param {number} handled - The number of handled iframes (those who 304 * wheren't filtered) 305 */ 306 307 /** 308 * Iterates over all iframes inside the specified context and calls the 309 * callbacks when they're ready. Filters iframes based on the instance 310 * exclusion selectors 311 * @param {HTMLElement} ctx - The context DOM element 312 * @param {DOMIterator~forEachIframeFilterCallback} filter - Filter callback 313 * @param {DOMIterator~forEachIframeEachCallback} each - Each callback 314 * @param {DOMIterator~forEachIframeEndCallback} [end] - End callback 315 * @access protected 316 */ 317 318 forEachIframe(ctx, filter, each, end = () => {}) { 319 let ifr = ctx.querySelectorAll("iframe"), 320 open = ifr.length, 321 handled = 0; 322 ifr = Array.prototype.slice.call(ifr); 323 324 const checkEnd = () => { 325 if (--open <= 0) { 326 end(handled); 327 } 328 }; 329 330 if (!open) { 331 checkEnd(); 332 } 333 334 ifr.forEach((ifr) => { 335 if (DOMIterator.matches(ifr, this.exclude)) { 336 checkEnd(); 337 } else { 338 this.onIframeReady( 339 ifr, 340 (con) => { 341 if (filter(ifr)) { 342 handled++; 343 each(con); 344 } 345 346 checkEnd(); 347 }, 348 checkEnd 349 ); 350 } 351 }); 352 } 353 /** 354 * Creates a NodeIterator on the specified context 355 * @see {@link https://developer.mozilla.org/en/docs/Web/API/NodeIterator} 356 * @param {HTMLElement} ctx - The context DOM element
357 * @param {DOMIterator~whatToShow} whatToShow 358 * @param {DOMIterator~filterCb} filter 359 * @return {NodeIterator} 360 * @access protected 361 */ 362 363 createIterator(ctx, whatToShow, filter) { 364 return document.createNodeIterator(ctx, whatToShow, filter, false); 365 } 366 /** 367 * Creates an instance of DOMIterator in an iframe 368 * @param {HTMLDocument} contents - Iframe document 369 * @return {DOMIterator} 370 * @access protected 371 */ 372 373 createInstanceOnIframe(contents) { 374 return new DOMIterator(contents.querySelector("html"), this.iframes); 375 } 376 /** 377 * Checks if an iframe occurs between two nodes, more specifically if an 378 * iframe occurs before the specified node and after the specified prevNode 379 * @param {HTMLElement} node - The node that should occur after the iframe 380 * @param {HTMLElement} prevNode - The node that should occur before the 381 * iframe 382 * @param {HTMLElement} ifr - The iframe to check against 383 * @return {boolean} 384 * @access protected 385 */ 386 387 compareNodeIframe(node, prevNode, ifr) { 388 const compCurr = node.compareDocumentPosition(ifr), 389 prev = Node.DOCUMENT_POSITION_PRECEDING; 390 391 if (compCurr & prev) { 392 if (prevNode !== null) { 393 const compPrev = prevNode.compareDocumentPosition(ifr), 394 after = Node.DOCUMENT_POSITION_FOLLOWING; 395 396 if (compPrev & after) { 397 return true; 398 } 399 } else { 400 return true; 401 } 402 } 403 404 return false; 405 } 406 /** 407 * @typedef {DOMIterator~getIteratorNodeReturn} 408 * @type {object.<string>} 409 * @property {HTMLElement} prevNode - The previous node or null if there is 410 * no 411 * @property {HTMLElement} node - The current node 412 */ 413 414 /** 415 * Returns the previous and current node of the specified iterator 416 * @param {NodeIterator} itr - The iterator 417 * @return {DOMIterator~getIteratorNodeReturn} 418 * @access protected 419 */ 420 421 getIteratorNode(itr) { 422 const prevNode = itr.previousNode(); 423 let node; 424 425 if (prevNode === null) { 426 node = itr.nextNode(); 427 } else { 428 node = itr.nextNode() && itr.nextNode(); 429 } 430 431 return { 432 prevNode, 433 node, 434 }; 435 } 436 /** 437 * An array containing objects. The object key "val" contains an iframe 438 * DOM element. The object key "handled" contains a boolean indicating if 439 * the iframe was handled already. 440 * It wouldn't be enough to save all open or all already handled iframes. 441 * The information of open iframes is necessary because they may occur after 442 * all other text nodes (and compareNodeIframe would never be true). The 443 * information of already handled iframes is necessary as otherwise they may 444 * be handled multiple times 445 * @typedef DOMIterator~checkIframeFilterIfr 446 * @type {object[]} 447 */ 448 449 /** 450 * Checks if an iframe wasn't handled already and if so, calls 451 * {@link DOMIterator#compareNodeIframe} to check if it should be handled. 452 * Information wheter an iframe was or wasn't handled is given within the 453 * <code>ifr</code> dictionary 454 * @param {HTMLElement} node - The node that should occur after the iframe 455 * @param {HTMLElement} prevNode - The node that should occur before the 456 * iframe 457 * @param {HTMLElement} currIfr - The iframe to check 458 * @param {DOMIterator~checkIframeFilterIfr} ifr - The iframe dictionary. 459 * Will be manipulated (by reference) 460 * @return {boolean} Returns true when it should be handled, otherwise false 461 * @access protected 462 */ 463 464 checkIframeFilter(node, prevNode, currIfr, ifr) { 465 let key = false, 466 // false === doesn't exist 467 handled = false; 468 ifr.forEach((ifrDict, i) => { 469 if (ifrDict.val === currIfr) { 470 key = i; 471 handled = ifrDict.handled; 472 } 473 }); 474 475 if (this.compareNodeIframe(node, prevNode, currIfr)) { 476 if (key === false && !handled) { 477 ifr.push({ 478 val: currIfr, 479 handled: true, 480 }); 481 } else if (key !== false && !handled) { 482 ifr[key].handled = true; 483 } 484 485 return true; 486 } 487 488 if (key === false) { 489 ifr.push({ 490 val: currIfr, 491 handled: false, 492 }); 493 } 494 495 return false;
496 } 497 /** 498 * Creates an iterator on all open iframes in the specified array and calls 499 * the end callback when finished 500 * @param {DOMIterator~checkIframeFilterIfr} ifr 501 * @param {DOMIterator~whatToShow} whatToShow 502 * @param {DOMIterator~forEachNodeCallback} eCb - Each callback 503 * @param {DOMIterator~filterCb} fCb 504 * @access protected 505 */ 506 507 handleOpenIframes(ifr, whatToShow, eCb, fCb) { 508 ifr.forEach((ifrDict) => { 509 if (!ifrDict.handled) { 510 this.getIframeContents(ifrDict.val, (con) => { 511 this.createInstanceOnIframe(con).forEachNode(whatToShow, eCb, fCb); 512 }); 513 } 514 }); 515 } 516 /** 517 * Iterates through all nodes in the specified context and handles iframe 518 * nodes at the correct position 519 * @param {DOMIterator~whatToShow} whatToShow 520 * @param {HTMLElement} ctx - The context 521 * @param {DOMIterator~forEachNodeCallback} eachCb - Each callback 522 * @param {DOMIterator~filterCb} filterCb - Filter callback 523 * @param {DOMIterator~forEachNodeEndCallback} doneCb - End callback 524 * @access protected 525 */ 526 527 iterateThroughNodes(whatToShow, ctx, eachCb, filterCb, doneCb) { 528 const itr = this.createIterator(ctx, whatToShow, filterCb); 529 530 let ifr = [], 531 elements = [], 532 node, 533 prevNode, 534 retrieveNodes = () => { 535 ({ prevNode, node } = this.getIteratorNode(itr)); 536 return node; 537 }; 538 539 while (retrieveNodes()) { 540 if (this.iframes) { 541 this.forEachIframe( 542 ctx, 543 (currIfr) => { 544 // note that ifr will be manipulated here 545 return this.checkIframeFilter(node, prevNode, currIfr, ifr); 546 }, 547 (con) => { 548 this.createInstanceOnIframe(con).forEachNode( 549 whatToShow, 550 (ifrNode) => elements.push(ifrNode), 551 filterCb 552 ); 553 } 554 ); 555 } // it's faster to call the each callback in an array loop 556 // than in this while loop 557 558 elements.push(node); 559 } 560 561 elements.forEach((node) => { 562 eachCb(node); 563 }); 564 565 if (this.iframes) { 566 this.handleOpenIframes(ifr, whatToShow, eachCb, filterCb); 567 } 568 569 doneCb(); 570 } 571 /** 572 * Callback for each node 573 * @callback DOMIterator~forEachNodeCallback 574 * @param {HTMLElement} node - The DOM text node element 575 */ 576 577 /** 578 * Callback if all contexts were handled 579 * @callback DOMIterator~forEachNodeEndCallback 580 */ 581 582 /** 583 * Iterates over all contexts and initializes 584 * {@link DOMIterator#iterateThroughNodes iterateThroughNodes} on them 585 * @param {DOMIterator~whatToShow} whatToShow 586 * @param {DOMIterator~forEachNodeCallback} each - Each callback 587 * @param {DOMIterator~filterCb} filter - Filter callback 588 * @param {DOMIterator~forEachNodeEndCallback} done - End callback 589 * @access public 590 */ 591 592 forEachNode(whatToShow, each, filter, done = () => {}) { 593 const contexts = this.getContexts(); 594 let open = contexts.length; 595 596 if (!open) { 597 done(); 598 } 599
600 contexts.forEach((ctx) => { 601 const ready = () => { 602 this.iterateThroughNodes(whatToShow, ctx, each, filter, () => { 603 if (--open <= 0) { 604 // call end all contexts were handled 605 done(); 606 } 607 }); 608 }; // wait for iframes to avoid recursive calls, otherwise this would 609 // perhaps reach the recursive function call limit with many nodes 610 611 if (this.iframes) { 612 this.waitForIframes(ctx, ready); 613 } else { 614 ready(); 615 } 616 }); 617 } 618 /** 619 * Callback to filter nodes. Can return e.g. NodeFilter.FILTER_ACCEPT or 620 * NodeFilter.FILTER_REJECT 621 * @see {@link http://tinyurl.com/zdczmm2} 622 * @callback DOMIterator~filterCb 623 * @param {HTMLElement} node - The node to filter 624 */ 625 626 /** 627 * @typedef DOMIterator~whatToShow 628 * @see {@link http://tinyurl.com/zfqqkx2} 629 * @type {number} 630 */ 631} 632 633/** 634 * Marks search terms in DOM elements 635 * @example 636 * new Mark(document.querySelector(".context")).mark("lorem ipsum"); 637 * @example 638 * new Mark(document.querySelector(".context")).markRegExp(/lorem/gmi); 639 */ 640 641class Mark$1 { 642 // eslint-disable-line no-unused-vars 643 644 /** 645 * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM 646 * element, an array of DOM elements, a NodeList or a selector 647 */ 648 constructor(ctx) { 649 /** 650 * The context of the instance. Either a DOM element, an array of DOM 651 * elements, a NodeList or a selector 652 * @type {HTMLElement|HTMLElement[]|NodeList|string} 653 * @access protected 654 */ 655 this.ctx = ctx; 656 /** 657 * Specifies if the current browser is a IE (necessary for the node 658 * normalization bug workaround). See {@link Mark#unwrapMatches} 659 * @type {boolean} 660 * @access protected 661 */ 662 663 this.ie = false;
664 const ua = window.navigator.userAgent; 665 666 if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) { 667 this.ie = true; 668 } 669 } 670 /** 671 * Options defined by the user. They will be initialized from one of the 672 * public methods. See {@link Mark#mark}, {@link Mark#markRegExp}, 673 * {@link Mark#markRanges} and {@link Mark#unmark} for option properties. 674 * @type {object} 675 * @param {object} [val] - An object that will be merged with defaults 676 * @access protected 677 */ 678 679 set opt(val) { 680 this._opt = Object.assign( 681 {}, 682 { 683 element: "", 684 className: "", 685 exclude: [], 686 iframes: false, 687 iframesTimeout: 5000, 688 separateWordSearch: true, 689 diacritics: true, 690 synonyms: {}, 691 accuracy: "partially", 692 acrossElements: false, 693 caseSensitive: false, 694 ignoreJoiners: false, 695 ignoreGroups: 0, 696 ignorePunctuation: [], 697 wildcards: "disabled", 698 each: () => {}, 699 noMatch: () => {}, 700 filter: () => true, 701 done: () => {}, 702 debug: false, 703 log: window.console, 704 }, 705 val 706 ); 707 } 708 709 get opt() { 710 return this._opt; 711 } 712 /** 713 * An instance of DOMIterator 714 * @type {DOMIterator} 715 * @access protected 716 */ 717 718 get iterator() { 719 // always return new instance in case there were option changes 720 return new DOMIterator( 721 this.ctx, 722 this.opt.iframes, 723 this.opt.exclude, 724 this.opt.iframesTimeout 725 ); 726 } 727 /** 728 * Logs a message if log is enabled 729 * @param {string} msg - The message to log 730 * @param {string} [level="debug"] - The log level, e.g. <code>warn</code> 731 * <code>error</code>, <code>debug</code> 732 * @access protected 733 */ 734 735 log(msg, level = "debug") { 736 const log = this.opt.log; 737 738 if (!this.opt.debug) { 739 return; 740 } 741 742 if (typeof log === "object" && typeof log[level] === "function") { 743 log[level](`mark.js: ${msg}`); 744 } 745 } 746 /** 747 * Escapes a string for usage within a regular expression 748 * @param {string} str - The string to escape 749 * @return {string} 750 * @access protected 751 */ 752 753 escapeStr(str) { 754 // eslint-disable-next-line no-useless-escape 755 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 756 } 757 /** 758 * Creates a regular expression string to match the specified search 759 * term including synonyms, diacritics and accuracy if defined 760 * @param {string} str - The search term to be used 761 * @return {string} 762 * @access protected 763 */ 764 765 createRegExp(str) { 766 if (this.opt.wildcards !== "disabled") { 767 str = this.setupWildcardsRegExp(str); 768 } 769 770 str = this.escapeStr(str); 771 772 if (Object.keys(this.opt.synonyms).length) { 773 str = this.createSynonymsRegExp(str); 774 } 775 776 if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { 777 str = this.setupIgnoreJoinersRegExp(str); 778 } 779 780 if (this.opt.diacritics) { 781 str = this.createDiacriticsRegExp(str); 782 } 783 784 str = this.createMergedBlanksRegExp(str); 785 786 if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { 787 str = this.createJoinersRegExp(str); 788 } 789 790 if (this.opt.wildcards !== "disabled") { 791 str = this.createWildcardsRegExp(str); 792 } 793 794 str = this.createAccuracyRegExp(str); 795 return str; 796 } 797 /** 798 * Creates a regular expression string to match the defined synonyms 799 * @param {string} str - The search term to be used 800 * @return {string} 801 * @access protected 802 */ 803 804 createSynonymsRegExp(str) { 805 const syn = this.opt.synonyms, 806 sens = this.opt.caseSensitive ? "" : "i", 807 // add replacement character placeholder before and after the 808 // synonym group 809 joinerPlaceholder = 810 this.opt.ignoreJoiners || this.opt.ignorePunctuation.length 811 ? "\u0000" 812 : ""; 813 814 for (let index in syn) { 815 if (syn.hasOwnProperty(index)) { 816 const value = syn[index], 817 k1 = 818 this.opt.wildcards !== "disabled" 819 ? this.setupWildcardsRegExp(index) 820 : this.escapeStr(index), 821 k2 = 822 this.opt.wildcards !== "disabled" 823 ? this.setupWildcardsRegExp(value) 824 : this.escapeStr(value); 825 826 if (k1 !== "" && k2 !== "") { 827 str = str.replace( 828 new RegExp( 829 `(${this.escapeStr(k1)}|${this.escapeStr(k2)})`, 830 `gm${sens}` 831 ), 832 joinerPlaceholder + 833 `(${this.processSynomyms(k1)}|` + 834 `${this.processSynomyms(k2)})` + 835 joinerPlaceholder 836 ); 837 } 838 } 839 } 840 841 return str; 842 } 843 /** 844 * Setup synonyms to work with ignoreJoiners and or ignorePunctuation 845 * @param {string} str - synonym key or value to process 846 * @return {string} - processed synonym string 847 */ 848 849 processSynomyms(str) { 850 if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { 851 str = this.setupIgnoreJoinersRegExp(str); 852 } 853 854 return str; 855 } 856 /** 857 * Sets up the regular expression string to allow later insertion of 858 * wildcard regular expression matches 859 * @param {string} str - The search term to be used 860 * @return {string} 861 * @access protected 862 */ 863 864 setupWildcardsRegExp(str) { 865 // replace single character wildcard with unicode 0001 866 str = str.replace(/(?:\\)*\?/g, (val) => { 867 return val.charAt(0) === "\\" ? "?" : "\u0001"; 868 }); // replace multiple character wildcard with unicode 0002 869 870 return str.replace(/(?:\\)*\*/g, (val) => { 871 return val.charAt(0) === "\\" ? "*" : "\u0002"; 872 }); 873 } 874 /** 875 * Sets up the regular expression string to allow later insertion of 876 * wildcard regular expression matches 877 * @param {string} str - The search term to be used 878 * @return {string} 879 * @access protected 880 */ 881 882 createWildcardsRegExp(str) { 883 // default to "enable" (i.e. to not include spaces)
vendor: 3,787 bytes, lines 884-991
884 // "withSpaces" uses `[\\S\\s]` instead of `.` because the latter 885 // does not match new line characters 886 let spaces = this.opt.wildcards === "withSpaces"; 887 return ( 888 str // replace unicode 0001 with a RegExp class to match any single 889 // character, or any single non-whitespace character depending 890 // on the setting 891 .replace(/\u0001/g, spaces ? "[\\S\\s]?" : "\\S?") // replace unicode 0002 with a RegExp class to match zero or 892 // more characters, or zero or more non-whitespace characters 893 // depending on the setting 894 .replace(/\u0002/g, spaces ? "[\\S\\s]*?" : "\\S*") 895 ); 896 } 897 /** 898 * Sets up the regular expression string to allow later insertion of 899 * designated characters (soft hyphens & zero width characters) 900 * @param {string} str - The search term to be used 901 * @return {string} 902 * @access protected 903 */ 904 905 setupIgnoreJoinersRegExp(str) { 906 // adding a "null" unicode character as it will not be modified by the 907 // other "create" regular expression functions 908 return str.replace(/[^(|)\\]/g, (val, indx, original) => { 909 // don't add a null after an opening "(", around a "|" or before 910 // a closing "(", or between an escapement (e.g. \+) 911 let nextChar = original.charAt(indx + 1); 912 913 if (/[(|)\\]/.test(nextChar) || nextChar === "") { 914 return val; 915 } else { 916 return val + "\u0000"; 917 } 918 }); 919 } 920 /** 921 * Creates a regular expression string to allow ignoring of designated 922 * characters (soft hyphens, zero width characters & punctuation) based on 923 * the specified option values of <code>ignorePunctuation</code> and 924 * <code>ignoreJoiners</code> 925 * @param {string} str - The search term to be used 926 * @return {string} 927 * @access protected 928 */ 929 930 createJoinersRegExp(str) { 931 let joiner = []; 932 const ignorePunctuation = this.opt.ignorePunctuation; 933 934 if (Array.isArray(ignorePunctuation) && ignorePunctuation.length) { 935 joiner.push(this.escapeStr(ignorePunctuation.join(""))); 936 } 937 938 if (this.opt.ignoreJoiners) { 939 // u+00ad = soft hyphen 940 // u+200b = zero-width space 941 // u+200c = zero-width non-joiner 942 // u+200d = zero-width joiner 943 joiner.push("\\u00ad\\u200b\\u200c\\u200d"); 944 } 945 946 return joiner.length 947 ? str.split(/\u0000+/).join(`[${joiner.join("")}]*`) 948 : str; 949 } 950 /** 951 * Creates a regular expression string to match diacritics 952 * @param {string} str - The search term to be used 953 * @return {string} 954 * @access protected 955 */ 956 957 createDiacriticsRegExp(str) { 958 const sens = this.opt.caseSensitive ? "" : "i", 959 dct = this.opt.caseSensitive 960 ? [ 961 "aà áảãạÄằắẳẵặâầấẩẫáºÃ¤Ã¥ÄÄ ", 962 "AÃÃẢÃẠÄẰẮẲẴẶÃẦẤẨẪẬÃà ÄÄ", 963 "cçÄÄ", 964 "CÃÄÄ", 965 "dÄÄ", 966 "DÄÄ", 967 "eèéẻẽẹêá»áº¿á»á» á»Ã«ÄÄÄ", 968 "EÃÃẺẼẸÃá»áº¾á»á»á»ÃÄÄÄ", 969 "iìÃá»Ä©á»Ã®Ã¯Ä«", 970 "IÃÃá»Ä¨á»ÃÃĪ", 971 "lÅ", 972 "LÅ", 973 "nñÅÅ", 974 "NÃÅÅ", 975 "oòóá»Ãµá»Ã´á»á»á»á»á»Æ¡á»á»¡á»á»á»£Ã¶Ã¸Å", 976 "OÃÃá»Ãá»Ãá»á»á»á»á»Æ á»á» á»á»á»¢ÃÃÅ", 977 "rÅ", 978 "RÅ", 979 "sÅ¡ÅÈÅ", 980 "SÅ ÅÈÅ", 981 "tÅ¥ÈÅ£", 982 "TŤÈÅ¢", 983 "uùúủũụưừứá»á»¯á»±Ã»Ã¼Å¯Å«", 984 "UÃÃỦŨỤƯỪỨỬỮỰÃÃŮŪ", 985 "yýỳỷỹỵÿ", 986 "YÃỲỶỸỴŸ", 987 "zžżź", 988 "ZŽŻŹ", 989 ] 990 : [ 991 "aà áảãạÄằắẳẵặâầấẩẫáºÃ¤Ã¥ÄÄ AÃÃẢÃá
991º ÄẰẮẲẴẶÃẦẤẨẪẬÃà ÄÄ", 992 "cçÄÄCÃÄÄ", 993 "dÄÄDÄÄ", 994 "eèéẻẽẹêá»áº¿á»á» á»Ã«ÄÄÄEÃÃẺẼẸÃá»áº¾á»á»á»ÃÄÄÄ", 995 "iìÃá»Ä©á»Ã®Ã¯Ä«IÃÃá»Ä¨á»ÃÃĪ", 996 "lÅLÅ", 997 "nñÅÅNÃÅÅ", 998 "oòóá»Ãµá»Ã´á»á»á»á»á»Æ¡á»á»¡á»á»á»£Ã¶Ã¸ÅOÃÃá»Ãá»Ãá»á»á»á»á»Æ á»á» á»á»á»¢ÃÃÅ", 999 "rÅRÅ", 1000 "sÅ¡ÅÈÅSÅ ÅÈÅ", 1001 "tÅ¥ÈÅ£TŤÈÅ¢", 1002 "uùúủũụưừứá»á»¯á»±Ã»Ã¼Å¯Å«UÃÃỦŨỤƯỪỨỬỮỰÃÃŮŪ", 1003 "yýỳỷỹỵÿYÃỲỶỸỴŸ", 1004 "zžżźZŽŻŹ", 1005 ]; 1006 let handled = []; 1007 str.split("").forEach((ch) => { 1008 dct.every((dct) => { 1009 // Check if the character is inside a diacritics list 1010 if (dct.indexOf(ch) !== -1) { 1011 // Check if the related diacritics list was not 1012 // handled yet 1013 if (handled.indexOf(dct) > -1) { 1014 return false; 1015 } // Make sure that the character OR any other 1016 // character in the diacritics list will be matched 1017 1018 str = str.replace(new RegExp(`[${dct}]`, `gm${sens}`), `[${dct}]`); 1019 handled.push(dct); 1020 } 1021 1022 return true; 1023 }); 1024 }); 1025 return str; 1026 } 1027 /** 1028 * Creates a regular expression string that merges whitespace characters 1029 * including subsequent ones into a single pattern, one or multiple 1030 * whitespaces 1031 * @param {string} str - The search term to be used 1032 * @return {string} 1033 * @access protected 1034 */ 1035 1036 createMergedBlanksRegExp(str) { 1037 return str.replace(/[\s]+/gim, "[\\s]+"); 1038 } 1039 /** 1040 * Creates a regular expression string to match the specified string with 1041 * the defined accuracy. As in the regular expression of "exactly" can be 1042 * a group containing a blank at the beginning, all regular expressions will 1043 * be created with two groups. The first group can be ignored (may contain 1044 * the said blank), the second contains the actual match 1045 * @param {string} str - The searm term to be used 1046 * @return {str} 1047 * @access protected 1048 */ 1049 1050 createAccuracyRegExp(str) { 1051 const chars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿"; 1052 let acc = this.opt.accuracy, 1053 val = typeof acc === "string" ? acc : acc.value, 1054 ls = typeof acc === "string" ? [] : acc.limiters, 1055 lsJoin = ""; 1056 ls.forEach((limiter) => { 1057 lsJoin += `|${this.escapeStr(limiter)}`; 1058 }); 1059 1060 switch (val) { 1061 case "partially": 1062 default: 1063 return `()(${str})`; 1064 1065 case "complementary": 1066 lsJoin = "\\s" + (lsJoin ? lsJoin : this.escapeStr(chars)); 1067 return `()([^${lsJoin}]*${str}[^${lsJoin}]*)`; 1068 1069 case "exactly": 1070 return `(^|\\s${lsJoin})(${str})(?=$|\\s${lsJoin})`; 1071 } 1072 } 1073 /** 1074 * @typedef Mark~separatedKeywords 1075 * @type {object.<string>} 1076 * @property {array.<string>} keywords - The list of keywords 1077 * @property {number} length - The length 1078 */ 1079 1080 /** 1081 * Returns a list of keywords dependent on whether separate word search 1082 * was defined. Also it filters empty keywords 1083 * @param {array} sv - The array of keywords 1084 * @return {Mark~separatedKeywords} 1085 * @access protected 1086 */ 1087 1088 getSeparatedKeywords(sv) { 1089 let stack = []; 1090 sv.forEach((kw) => { 1091 if (!this.opt.separateWordSearch) { 1092 if (kw.trim() && stack.indexOf(kw) === -1) { 1093 stack.push(kw); 1094 } 1095 } else { 1096 kw.split(" ").forEach((kwSplitted) => { 1097 if (kwSplitted.trim() && stack.indexOf(kwSplitted) === -1) { 1098 stack.push(kwSplitted); 1099 } 1100 }); 1101 } 1102 }); 1103 return { 1104 // sort because of https://git.io/v6USg 1105 keywords: stack.sort((a, b) => { 1106 return b.length - a.length; 1107 }), 1108 length: stack.length, 1109 }; 1110 } 1111 /** 1112 * Check if a value is a number 1113 * @param {number|string} value - the value to check; 1114 * numeric strings allowed 1115 * @return {boolean} 1116 * @access protected 1117 */ 1118 1119 isNumeric(value) { 1120 // http://stackoverflow.com/a/16655847/145346 1121 // eslint-disable-next-line eqeqeq 1122 return Number(parseFloat(value)) == value; 1123 } 1124 /** 1125 * @typedef Mark~rangeObject 1126 * @type {object} 1127 * @property {number} start - The start position within the composite value 1128 * @property {number} length - The length of the string to mark within the 1129 * composite value. 1130 */ 1131 1132 /** 1133 * @typedef Mark~setOfRanges 1134 * @type {object[]} 1135 * @property {Mark~rangeObject} 1136 */ 1137 1138 /** 1139 * Returns a processed list of integer offset indexes that do not overlap 1140 * each other, and remove any string values or additional elements 1141 * @param {Mark~setOfRanges} array - unprocessed raw array 1142 * @return {Mark~setOfRanges}
vendor: 4,808 bytes, lines 1142-1298
1142 - processed array with any invalid entries 1143 * removed 1144 * @throws Will throw an error if an array of objects is not passed 1145 * @access protected 1146 */ 1147 1148 checkRanges(array) { 1149 // start and length indexes are included in an array of objects 1150 // [{start: 0, length: 1}, {start: 4, length: 5}] 1151 // quick validity check of the first entry only 1152 if ( 1153 !Array.isArray(array) || 1154 Object.prototype.toString.call(array[0]) !== "[object Object]" 1155 ) { 1156 this.log("markRanges() will only accept an array of objects"); 1157 this.opt.noMatch(array); 1158 return []; 1159 } 1160 1161 const stack = []; 1162 let last = 0; 1163 array // acending sort to ensure there is no overlap in start & end 1164 // offsets 1165 .sort((a, b) => { 1166 return a.start - b.start; 1167 }) 1168 .forEach((item) => { 1169 let { start, end, valid } = this.callNoMatchOnInvalidRanges(item, last); 1170 1171 if (valid) { 1172 // preserve item in case there are extra key:values within 1173 item.start = start; 1174 item.length = end - start; 1175 stack.push(item); 1176 last = end; 1177 } 1178 }); 1179 return stack; 1180 } 1181 /** 1182 * @typedef Mark~validObject 1183 * @type {object} 1184 * @property {number} start - The start position within the composite value 1185 * @property {number} end - The calculated end position within the composite 1186 * value. 1187 * @property {boolean} valid - boolean value indicating that the start and 1188 * calculated end range is valid 1189 */ 1190 1191 /** 1192 * Initial validation of ranges for markRanges. Preliminary checks are done 1193 * to ensure the start and length values exist and are not zero or non- 1194 * numeric 1195 * @param {Mark~rangeObject} range - the current range object 1196 * @param {number} last - last index of range 1197 * @return {Mark~validObject} 1198 * @access protected 1199 */ 1200 1201 callNoMatchOnInvalidRanges(range, last) { 1202 let start, 1203 end, 1204 valid = false; 1205 1206 if (range && typeof range.start !== "undefined") { 1207 start = parseInt(range.start, 10); 1208 end = start + parseInt(range.length, 10); // ignore overlapping values & non-numeric entries 1209 1210 if ( 1211 this.isNumeric(range.start) && 1212 this.isNumeric(range.length) && 1213 end - last > 0 && 1214 end - start > 0 1215 ) { 1216 valid = true; 1217 } else { 1218 this.log( 1219 "Ignoring invalid or overlapping range: " + `${JSON.stringify(range)}` 1220 ); 1221 this.opt.noMatch(range); 1222 } 1223 } else { 1224 this.log(`Ignoring invalid range: ${JSON.stringify(range)}`); 1225 this.opt.noMatch(range); 1226 } 1227 1228 return { 1229 start: start, 1230 end: end, 1231 valid: valid, 1232 }; 1233 } 1234 /** 1235 * Check valid range for markRanges. Check ranges with access to the context 1236 * string. Range values are double checked, lengths that extend the mark 1237 * beyond the string length are limitied and ranges containing only 1238 * whitespace are ignored 1239 * @param {Mark~rangeObject} range - the current range object 1240 * @param {number} originalLength - original length of the context string 1241 * @param {string} string - current content string 1242 * @return {Mark~validObject} 1243 * @access protected 1244 */ 1245 1246 checkWhitespaceRanges(range, originalLength, string) { 1247 let end, 1248 valid = true, 1249 // the max value changes after the DOM is manipulated 1250 max = string.length, 1251 // adjust offset to account for wrapped text node 1252 offset = originalLength - max, 1253 start = parseInt(range.start, 10) - offset; // make sure to stop at max 1254 1255 start = start > max ? max : start; 1256 end = start + parseInt(range.length, 10); 1257 1258 if (end > max) { 1259 end = max; 1260 this.log(`End range automatically set to the max value of ${max}`); 1261 } 1262 1263 if (start < 0 || end - start < 0 || start > max || end > max) { 1264 valid = false; 1265 this.log(`Invalid range: ${JSON.stringify(range)}`); 1266 this.opt.noMatch(range); 1267 } else if (string.substring(start, end).replace(/\s+/g, "") === "") { 1268 valid = false; // whitespace only; even if wrapped it is not visible 1269 1270 this.log("Skipping whitespace only range: " + JSON.stringify(range)); 1271 this.opt.noMatch(range); 1272 } 1273 1274 return { 1275 start: start, 1276 end: end, 1277 valid: valid, 1278 }; 1279 } 1280 /** 1281 * @typedef Mark~getTextNodesDict 1282 * @type {object.<string>} 1283 * @property {string} value - The composite value of all text nodes 1284 * @property {object[]} nodes - An array of objects 1285 * @property {number} nodes.start - The start position within the composite 1286 * value 1287 * @property {number} nodes.end - The end position within the composite 1288 * value 1289 * @property {HTMLElement} nodes.node - The DOM text node element 1290 */ 1291 1292 /** 1293 * Callback 1294 * @callback Mark~getTextNodesCallback 1295 * @param {Mark~getTextNodesDict} 1296 */ 1297 1298 /**
1299 * Calls the callback with an object containing all text nodes (including 1300 * iframe text nodes) with start and end positions and the composite value 1301 * of them (string) 1302 * @param {Mark~getTextNodesCallback} cb - Callback 1303 * @access protected 1304 */ 1305 1306 getTextNodes(cb) { 1307 let val = "", 1308 nodes = []; 1309 this.iterator.forEachNode( 1310 NodeFilter.SHOW_TEXT, 1311 (node) => { 1312 nodes.push({ 1313 start: val.length, 1314 end: (val += node.textContent).length, 1315 node, 1316 }); 1317 }, 1318 (node) => { 1319 if (this.matchesExclude(node.parentNode)) { 1320 return NodeFilter.FILTER_REJECT; 1321 } else { 1322 return NodeFilter.FILTER_ACCEPT; 1323 } 1324 }, 1325 () => { 1326 cb({ 1327 value: val, 1328 nodes: nodes, 1329 }); 1330 } 1331 ); 1332 } 1333 /** 1334 * Checks if an element matches any of the specified exclude selectors. Also 1335 * it checks for elements in which no marks should be performed (e.g. 1336 * script and style tags) and optionally already marked elements 1337 * @param {HTMLElement} el - The element to check 1338 * @return {boolean} 1339 * @access protected 1340 */ 1341 1342 matchesExclude(el) { 1343 return DOMIterator.matches( 1344 el, 1345 this.opt.exclude.concat([ 1346 // ignores the elements itself, not their childrens (selector *) 1347 "script", 1348 "style", 1349 "title", 1350 "head", 1351 "html", 1352 ]) 1353 ); 1354 } 1355 /** 1356 * Wraps the instance element and class around matches that fit the start 1357 * and end positions within the node 1358 * @param {HTMLElement} node - The DOM text node 1359 * @param {number} start - The position where to start wrapping 1360 * @param {number} end - The position where to end wrapping 1361 * @return {HTMLElement} Returns the splitted text node that will appear 1362 * after the wrapped text node 1363 * @access protected 1364 */ 1365 1366 wrapRangeInTextNode(node, start, end) { 1367 const hEl = !this.opt.element ? "mark" : this.opt.element, 1368 startNode = node.splitText(start), 1369 ret = startNode.splitText(end - start); 1370 let repl = document.createElement(hEl); 1371 repl.setAttribute("data-markjs", "true"); 1372 1373 if (this.opt.className) { 1374 repl.setAttribute("class", this.opt.className); 1375 } 1376 1377 repl.textContent = startNode.textContent; 1378 startNode.parentNode.replaceChild(repl, startNode); 1379 return ret; 1380 } 1381 /** 1382 * @typedef Mark~wrapRangeInMappedTextNodeDict 1383 * @type {object.<string>} 1384 * @property {string} value - The composite value of all text nodes 1385 * @property {object[]} nodes - An array of objects 1386 * @property {number} nodes.start - The start position within the composite 1387 * value 1388 * @property {number} nodes.end - The end position within the composite 1389 * value 1390 * @property {HTMLElement} nodes.node - The DOM text node element 1391 */ 1392 1393 /** 1394 * Each callback 1395 * @callback Mark~wrapMatchesEachCallback 1396 * @param {HTMLElement} node - The wrapped DOM element 1397 * @param {number} lastIndex - The last matching position within the 1398 * composite value of text nodes 1399 */ 1400 1401 /** 1402 * Filter callback 1403 * @callback Mark~wrapMatchesFilterCallback 1404 * @param {HTMLElement} node - The matching text node DOM element 1405 */ 1406 1407 /** 1408 * Determines matches by start and end positions using the text node 1409 * dictionary even across text nodes and calls 1410 * {@link Mark#wrapRangeInTextNode} to wrap them 1411 * @param {Mark~wrapRangeInMappedTextNodeDict} dict - The dictionary 1412 * @param {number} start - The start position of the match 1413 * @param {number} end - The end position of the match 1414 * @param {Mark~wrapMatchesFilterCallback} filterCb - Filter callback 1415 * @param {Mark~wrapMatchesEachCallback} eachCb - Each callback 1416 * @access protected 1417 */ 1418 1419 wrapRangeInMappedTextNode(dict, start, end, filterCb, eachCb) { 1420 // iterate over all text nodes to find the one matching the positions 1421 dict.nodes.every((n, i) => { 1422 const sibl = dict.nodes[i + 1]; 1423 1424 if (typeof sibl === "undefined" || sibl.start > start) { 1425 if (!filterCb(n.node)) { 1426 return false; 1427 } // map range from dict.value to text node 1428 1429 const s = start - n.start, 1430 e = (end > n.end ? n.end : end) - n.start, 1431 startStr = dict.value.substr(0, n.start), 1432 endStr = dict.value.substr(e + n.start); 1433 n.node = this.wrapRangeInTextNode(n.node, s, e); // recalculate positions to also find subsequent matches in the 1434 // same text node. Necessary as the text node in dict now only 1435 // contains the splitted part after the wrapped one 1436 1437 dict.value = startStr + endStr;
vendor: 11,473 bytes, lines 1438-1833
1438 dict.nodes.forEach((k, j) => { 1439 if (j >= i) { 1440 if (dict.nodes[j].start > 0 && j !== i) { 1441 dict.nodes[j].start -= e; 1442 } 1443 1444 dict.nodes[j].end -= e; 1445 } 1446 }); 1447 end -= e; 1448 eachCb(n.node.previousSibling, n.start); 1449 1450 if (end > n.end) { 1451 start = n.end; 1452 } else { 1453 return false; 1454 } 1455 } 1456 1457 return true; 1458 }); 1459 } 1460 /** 1461 * Filter callback before each wrapping 1462 * @callback Mark~wrapMatchesFilterCallback 1463 * @param {string} match - The matching string 1464 * @param {HTMLElement} node - The text node where the match occurs 1465 */ 1466 1467 /** 1468 * Callback for each wrapped element 1469 * @callback Mark~wrapMatchesEachCallback 1470 * @param {HTMLElement} element - The marked DOM element 1471 */ 1472 1473 /** 1474 * Callback on end 1475 * @callback Mark~wrapMatchesEndCallback 1476 */ 1477 1478 /** 1479 * Wraps the instance element and class around matches within single HTML 1480 * elements in all contexts 1481 * @param {RegExp} regex - The regular expression to be searched for 1482 * @param {number} ignoreGroups - A number indicating the amount of RegExp 1483 * matching groups to ignore 1484 * @param {Mark~wrapMatchesFilterCallback} filterCb 1485 * @param {Mark~wrapMatchesEachCallback} eachCb 1486 * @param {Mark~wrapMatchesEndCallback} endCb 1487 * @access protected 1488 */ 1489 1490 wrapMatches(regex, ignoreGroups, filterCb, eachCb, endCb) { 1491 const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1; 1492 this.getTextNodes((dict) => { 1493 dict.nodes.forEach((node) => { 1494 node = node.node; 1495 let match; 1496 1497 while ( 1498 (match = regex.exec(node.textContent)) !== null && 1499 match[matchIdx] !== "" 1500 ) { 1501 if (!filterCb(match[matchIdx], node)) { 1502 continue; 1503 } 1504 1505 let pos = match.index; 1506 1507 if (matchIdx !== 0) { 1508 for (let i = 1; i < matchIdx; i++) { 1509 pos += match[i].length; 1510 } 1511 } 1512 1513 node = this.wrapRangeInTextNode( 1514 node, 1515 pos, 1516 pos + match[matchIdx].length 1517 ); 1518 eachCb(node.previousSibling); // reset index of last match as the node changed and the 1519 // index isn't valid anymore http://tinyurl.com/htsudjd 1520 1521 regex.lastIndex = 0; 1522 } 1523 }); 1524 endCb(); 1525 }); 1526 } 1527 /** 1528 * Callback for each wrapped element 1529 * @callback Mark~wrapMatchesAcrossElementsEachCallback 1530 * @param {HTMLElement} element - The marked DOM element 1531 */ 1532 1533 /** 1534 * Filter callback before each wrapping 1535 * @callback Mark~wrapMatchesAcrossElementsFilterCallback 1536 * @param {string} match - The matching string 1537 * @param {HTMLElement} node - The text node where the match occurs 1538 */ 1539 1540 /** 1541 * Callback on end 1542 * @callback Mark~wrapMatchesAcrossElementsEndCallback 1543 */ 1544 1545 /** 1546 * Wraps the instance element and class around matches across all HTML 1547 * elements in all contexts 1548 * @param {RegExp} regex - The regular expression to be searched for 1549 * @param {number} ignoreGroups - A number indicating the amount of RegExp 1550 * matching groups to ignore 1551 * @param {Mark~wrapMatchesAcrossElementsFilterCallback} filterCb 1552 * @param {Mark~wrapMatchesAcrossElementsEachCallback} eachCb 1553 * @param {Mark~wrapMatchesAcrossElementsEndCallback} endCb 1554 * @access protected 1555 */ 1556 1557 wrapMatchesAcrossElements(regex, ignoreGroups, filterCb, eachCb, endCb) { 1558 const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1; 1559 this.getTextNodes((dict) => { 1560 let match; 1561 1562 while ( 1563 (match = regex.exec(dict.value)) !== null && 1564 match[matchIdx] !== "" 1565 ) { 1566 // calculate range inside dict.value 1567 let start = match.index; 1568 1569 if (matchIdx !== 0) { 1570 for (let i = 1; i < matchIdx; i++) { 1571 start += match[i].length; 1572 } 1573 } 1574 1575 const end = start + match[matchIdx].length; // note that dict will be updated automatically, as it'll change 1576 // in the wrapping process, due to the fact that text 1577 // nodes will be splitted 1578 1579 this.wrapRangeInMappedTextNode( 1580 dict, 1581 start, 1582 end, 1583 (node) => { 1584 return filterCb(match[matchIdx], node); 1585 }, 1586 (node, lastIndex) => { 1587 regex.lastIndex = lastIndex; 1588 eachCb(node); 1589 } 1590 ); 1591 } 1592 1593 endCb(); 1594 }); 1595 } 1596 /** 1597 * Callback for each wrapped element 1598 * @callback Mark~wrapRangeFromIndexEachCallback 1599 * @param {HTMLElement} element - The marked DOM element 1600 * @param {Mark~rangeObject} range - the current range object; provided 1601 * start and length values will be numeric integers modified from the 1602 * provided original ranges. 1603 */ 1604 1605 /** 1606 * Filter callback before each wrapping 1607 * @callback Mark~wrapRangeFromIndexFilterCallback 1608 * @param {HTMLElement} node - The text node which includes the range 1609 * @param {Mark~rangeObject} range - the current range object 1610 * @param {string} match - string extracted from the matching range 1611 * @param {number} counter - A counter indicating the number of all marks 1612 */ 1613 1614 /** 1615 * Callback on end 1616 * @callback Mark~wrapRangeFromIndexEndCallback 1617 */ 1618 1619 /** 1620 * Wraps the indicated ranges across all HTML elements in all contexts 1621 * @param {Mark~setOfRanges} ranges 1622 * @param {Mark~wrapRangeFromIndexFilterCallback} filterCb 1623 * @param {Mark~wrapRangeFromIndexEachCallback} eachCb 1624 * @param {Mark~wrapRangeFromIndexEndCallback} endCb 1625 * @access protected 1626 */ 1627 1628 wrapRangeFromIndex(ranges, filterCb, eachCb, endCb) { 1629 this.getTextNodes((dict) => { 1630 const originalLength = dict.value.length; 1631 ranges.forEach((range, counter) => { 1632 let { start, end, valid } = this.checkWhitespaceRanges( 1633 range, 1634 originalLength, 1635 dict.value 1636 ); 1637 1638 if (valid) { 1639 this.wrapRangeInMappedTextNode( 1640 dict, 1641 start, 1642 end, 1643 (node) => { 1644 return filterCb( 1645 node, 1646 range, 1647 dict.value.substring(start, end), 1648 counter 1649 ); 1650 }, 1651 (node) => { 1652 eachCb(node, range); 1653 } 1654 ); 1655 } 1656 }); 1657 endCb(); 1658 }); 1659 } 1660 /** 1661 * Unwraps the specified DOM node with its content (text nodes or HTML) 1662 * without destroying possibly present events (using innerHTML) and 1663 * normalizes the parent at the end (merge splitted text nodes) 1664 * @param {HTMLElement} node - The DOM node to unwrap 1665 * @access protected 1666 */ 1667 1668 unwrapMatches(node) { 1669 const parent = node.parentNode; 1670 let docFrag = document.createDocumentFragment(); 1671 1672 while (node.firstChild) { 1673 docFrag.appendChild(node.removeChild(node.firstChild)); 1674 } 1675 1676 parent.replaceChild(docFrag, node); 1677 1678 if (!this.ie) { 1679 // use browser's normalize method 1680 parent.normalize(); 1681 } else { 1682 // custom method (needs more time) 1683 this.normalizeTextNode(parent); 1684 } 1685 } 1686 /** 1687 * Normalizes text nodes. It's a workaround for the native normalize method 1688 * that has a bug in IE (see attached link). Should only be used in IE 1689 * browsers as it's slower than the native method. 1690 * @see {@link http://tinyurl.com/z5asa8c} 1691 * @param {HTMLElement} node - The DOM node to normalize 1692 * @access protected 1693 */ 1694 1695 normalizeTextNode(node) { 1696 if (!node) { 1697 return; 1698 } 1699 1700 if (node.nodeType === 3) { 1701 while (node.nextSibling && node.nextSibling.nodeType === 3) { 1702 node.nodeValue += node.nextSibling.nodeValue; 1703 node.parentNode.removeChild(node.nextSibling); 1704 } 1705 } else { 1706 this.normalizeTextNode(node.firstChild); 1707 } 1708 1709 this.normalizeTextNode(node.nextSibling); 1710 } 1711 /** 1712 * Callback when finished 1713 * @callback Mark~commonDoneCallback 1714 * @param {number} totalMatches - The number of marked elements 1715 */ 1716 1717 /** 1718 * @typedef Mark~commonOptions 1719 * @type {object.<string>} 1720 * @property {string} [element="mark"] - HTML element tag name 1721 * @property {string} [className] - An optional class name 1722 * @property {string[]} [exclude] - An array with exclusion selectors. 1723 * Elements matching those selectors will be ignored 1724 * @property {boolean} [iframes=false] - Whether to search inside iframes 1725 * @property {Mark~commonDoneCallback} [done] 1726 * @property {boolean} [debug=false] - Wheter to log messages 1727 * @property {object} [log=window.console] - Where to log messages (only if 1728 * debug is true) 1729 */ 1730 1731 /** 1732 * Callback for each marked element 1733 * @callback Mark~markRegExpEachCallback 1734 * @param {HTMLElement} element - The marked DOM element 1735 */ 1736 1737 /** 1738 * Callback if there were no matches 1739 * @callback Mark~markRegExpNoMatchCallback 1740 * @param {RegExp} regexp - The regular expression 1741 */ 1742 1743 /** 1744 * Callback to filter matches 1745 * @callback Mark~markRegExpFilterCallback 1746 * @param {HTMLElement} textNode - The text node which includes the match 1747 * @param {string} match - The matching string for the RegExp 1748 * @param {number} counter - A counter indicating the number of all marks 1749 */ 1750 1751 /** 1752 * These options also include the common options from 1753 * {@link Mark~commonOptions} 1754 * @typedef Mark~markRegExpOptions 1755 * @type {object.<string>} 1756 * @property {Mark~markRegExpEachCallback} [each] 1757 * @property {Mark~markRegExpNoMatchCallback} [noMatch] 1758 * @property {Mark~markRegExpFilterCallback} [filter] 1759 */ 1760 1761 /** 1762 * Marks a custom regular expression 1763 * @param {RegExp} regexp - The regular expression 1764 * @param {Mark~markRegExpOptions} [opt] - Optional options object 1765 * @access public 1766 */ 1767 1768 markRegExp(regexp, opt) { 1769 this.opt = opt; 1770 this.log(`Searching with expression "${regexp}"`); 1771 let totalMatches = 0, 1772 fn = "wrapMatches"; 1773 1774 const eachCb = (element) => { 1775 totalMatches++; 1776 this.opt.each(element); 1777 }; 1778 1779 if (this.opt.acrossElements) { 1780 fn = "wrapMatchesAcrossElements"; 1781 } 1782 1783 this[fn]( 1784 regexp, 1785 this.opt.ignoreGroups, 1786 (match, node) => { 1787 return this.opt.filter(node, match, totalMatches); 1788 }, 1789 eachCb, 1790 () => { 1791 if (totalMatches === 0) { 1792 this.opt.noMatch(regexp); 1793 } 1794 1795 this.opt.done(totalMatches); 1796 } 1797 ); 1798 } 1799 /** 1800 * Callback for each marked element 1801 * @callback Mark~markEachCallback 1802 * @param {HTMLElement} element - The marked DOM element 1803 */ 1804 1805 /** 1806 * Callback if there were no matches 1807 * @callback Mark~markNoMatchCallback 1808 * @param {RegExp} term - The search term that was not found 1809 */ 1810 1811 /** 1812 * Callback to filter matches 1813 * @callback Mark~markFilterCallback 1814 * @param {HTMLElement} textNode - The text node which includes the match 1815 * @param {string} match - The matching term 1816 * @param {number} totalCounter - A counter indicating the number of all 1817 * marks 1818 * @param {number} termCounter - A counter indicating the number of marks 1819 * for the specific match 1820 */ 1821 1822 /** 1823 * @typedef Mark~markAccuracyObject 1824 * @type {object.<string>} 1825 * @property {string} value - A accuracy string value 1826 * @property {string[]} limiters - A custom array of limiters. For example 1827 * <code>["-", ","]</code> 1828 */ 1829 1830 /** 1831 * @typedef Mark~markAccuracySetting 1832 * @type {string} 1833 * @property {"partially"|"complementary"|"exactly"|Mark~markAccuracyObject}
1834 * [accuracy="partially"] - Either one of the following string values: 1835 * <ul> 1836 * <li><i>partially</i>: When searching for "lor" only "lor" inside 1837 * "lorem" will be marked</li> 1838 * <li><i>complementary</i>: When searching for "lor" the whole word 1839 * "lorem" will be marked</li> 1840 * <li><i>exactly</i>: When searching for "lor" only those exact words 1841 * will be marked. In this example nothing inside "lorem". This value 1842 * is equivalent to the previous option <i>wordBoundary</i></li> 1843 * </ul> 1844 * Or an object containing two properties: 1845 * <ul> 1846 * <li><i>value</i>: One of the above named string values</li> 1847 * <li><i>limiters</i>: A custom array of string limiters for accuracy 1848 * "exactly" or "complementary"</li> 1849 * </ul> 1850 */ 1851 1852 /** 1853 * @typedef Mark~markWildcardsSetting 1854 * @type {string} 1855 * @property {"disabled"|"enabled"|"withSpaces"} 1856 * [wildcards="disabled"] - Set to any of the following string values: 1857 * <ul> 1858 * <li><i>disabled</i>: Disable wildcard usage</li> 1859 * <li><i>enabled</i>: When searching for "lor?m", the "?" will match zero 1860 * or one non-space character (e.g. "lorm", "loram", "lor3m", etc). When 1861 * searching for "lor*m", the "*" will match zero or more non-space 1862 * characters (e.g. "lorm", "loram", "lor123m", etc).</li> 1863 * <li><i>withSpaces</i>: When searching for "lor?m", the "?" will 1864 * match zero or one space or non-space character (e.g. "lor m", "loram", 1865 * etc). When searching for "lor*m", the "*" will match zero or more space 1866 * or non-space characters (e.g. "lorm", "lore et dolor ipsum", "lor: m", 1867 * etc).</li> 1868 * </ul> 1869 */ 1870 1871 /** 1872 * @typedef Mark~markIgnorePunctuationSetting 1873 * @type {string[]} 1874 * @property {string} The strings in this setting will contain punctuation 1875 * marks that will be ignored: 1876 * <ul> 1877 * <li>These punctuation marks can be between any characters, e.g. setting 1878 * this option to <code>["'"]</code> would match "Worlds", "World's" and 1879 * "Wo'rlds"</li> 1880 * <li>One or more apostrophes between the letters would still produce a 1881 * match (e.g. "W'o''r'l'd's").</li> 1882 * <li>A typical setting for this option could be as follows: 1883 * <pre>ignorePunctuation: ":;.,-âââ_(){}[]!'\"+=".split(""),</pre> This 1884 * setting includes common punctuation as well as a minus, en-dash, 1885 * em-dash and figure-dash 1886 * ({@link https://en.wikipedia.org/wiki/Dash#Figure_dash ref}), as well 1887 * as an underscore.</li> 1888 * </ul> 1889 */ 1890 1891 /** 1892 * These options also include the common options from 1893 * {@link Mark~commonOptions} 1894 * @typedef Mark~markOptions 1895 * @type {object.<string>} 1896 * @property {boolean} [separateWordSearch=true] - Whether to search for 1897 * each word separated by a blank instead of the complete term 1898 * @property {boolean} [diacritics=true] - If diacritic characters should be 1899 * matched. ({@link https://en.wikipedia.org/wiki/Diacritic Diacritics}) 1900 * @property {object} [synonyms] - An object with synonyms. The key will be 1901 * a synonym for the value and the value for the key 1902 * @property {Mark~markAccuracySetting} [accuracy] 1903 * @property {Mark~markWildcardsSetting}
1903 [wildcards] 1904 * @property {boolean} [acrossElements=false] - Whether to find matches 1905 * across HTML elements. By default, only matches within single HTML 1906 * elements will be found 1907 * @property {boolean} [ignoreJoiners=false] - Whether to ignore word 1908 * joiners inside of key words. These include soft-hyphens, zero-width 1909 * space, zero-width non-joiners and zero-width joiners. 1910 * @property {Mark~markIgnorePunctuationSetting} [ignorePunctuation] 1911 * @property {Mark~markEachCallback} [each] 1912 * @property {Mark~markNoMatchCallback} [noMatch] 1913 * @property {Mark~markFilterCallback} [filter] 1914 */ 1915 1916 /** 1917 * Marks the specified search terms 1918 * @param {string|string[]} [sv] - Search value, either a search string or 1919 * an array containing multiple search strings 1920 * @param {Mark~markOptions} [opt] - Optional options object 1921 * @access public 1922 */ 1923 1924 mark(sv, opt) { 1925 this.opt = opt; 1926 let totalMatches = 0, 1927 fn = "wrapMatches"; 1928 1929 const { keywords: kwArr, length: kwArrLen } = this.getSeparatedKeywords( 1930 typeof sv === "string" ? [sv] : sv 1931 ), 1932 sens = this.opt.caseSensitive ? "" : "i", 1933 handler = (kw) => { 1934 // async function calls as iframes are async too 1935 let regex = new RegExp(this.createRegExp(kw), `gm${sens}`), 1936 matches = 0; 1937 this.log(`Searching with expression "${regex}"`); 1938 this[fn]( 1939 regex, 1940 1, 1941 (term, node) => { 1942 return this.opt.filter(node, kw, totalMatches, matches); 1943 }, 1944 (element) => { 1945 matches++; 1946 totalMatches++; 1947 this.opt.each(element); 1948 }, 1949 () => { 1950 if (matches === 0) { 1951 this.opt.noMatch(kw); 1952 } 1953 1954 if (kwArr[kwArrLen - 1] === kw) { 1955 this.opt.done(totalMatches); 1956 } else { 1957 handler(kwArr[kwArr.indexOf(kw) + 1]); 1958 } 1959 } 1960 ); 1961 }; 1962 1963 if (this.opt.acrossElements) { 1964 fn = "wrapMatchesAcrossElements"; 1965 } 1966 1967 if (kwArrLen === 0) { 1968 this.opt.done(totalMatches); 1969 } else { 1970 handler(kwArr[0]); 1971 } 1972 } 1973 /** 1974 * Callback for each marked element 1975 * @callback Mark~markRangesEachCallback 1976 * @param {HTMLElement} element - The marked DOM element 1977 * @param {array} range - array of range start and end points 1978 */ 1979 1980 /** 1981 * Callback if a processed range is invalid, out-of-bounds, overlaps another 1982 * range, or only matches whitespace 1983 * @callback Mark~markRangesNoMatchCallback 1984 * @param {Mark~rangeObject} range - a range object 1985 */ 1986 1987 /** 1988 * Callback to filter matches 1989 * @callback Mark~markRangesFilterCallback 1990 * @param {HTMLElement} node - The text node which includes the range 1991 * @param {array} range - array of range start and end points 1992 * @param {string} match - string extracted from the matching range 1993 * @param {number} counter - A counter indicating the number of all marks 1994 */ 1995 1996 /** 1997 * These options also include the common options from 1998 * {@link Mark~commonOptions} 1999 * @typedef Mark~markRangesOptions 2000 * @type {object.<string>} 2001 * @property {Mark~markRangesEachCallback} [each] 2002 * @property {Mark~markRangesNoMatchCallback} [noMatch] 2003 * @property {Mark~markRangesFilterCallback} [filter] 2004 */ 2005 2006 /** 2007 * Marks an array of objects containing a start with an end or length of the 2008 * string to mark 2009 * @param {Mark~setOfRanges} rawRanges - The original (preprocessed) 2010 * array of objects 2011 * @param {Mark~markRangesOptions} [opt] - Optional options object 2012 * @access public 2013 */ 2014 2015 markRanges(rawRanges, opt) { 2016 this.opt = opt; 2017 let totalMatches = 0, 2018 ranges = this.checkRanges(rawRanges); 2019 2020 if (ranges && ranges.length) { 2021 this.log( 2022 "Starting to mark with the following ranges: " + JSON.stringify(ranges) 2023 ); 2024 this.wrapRangeFromIndex( 2025 ranges, 2026 (node, range, match, counter) => { 2027 return this.opt.filter(node, range, match, counter); 2028 }, 2029 (element, range) => { 2030 totalMatches++; 2031 this.opt.each(element, range); 2032 }, 2033 () => { 2034 this.opt.done(totalMatches); 2035 } 2036 ); 2037 } else { 2038 this.opt.done(totalMatches); 2039 } 2040 } 2041 /** 2042 * Removes all marked elements inside the context with their HTML and 2043 * normalizes the parent at the end 2044 * @param {Mark~commonOptions} [opt] - Optional options object 2045 * @access public 2046 */ 2047 2048 unmark(opt) { 2049 this.opt = opt; 2050 let sel = this.opt.element ? this.opt.element : "*"; 2051 sel += "[data-markjs]"; 2052 2053 if (this.opt.className) { 2054 sel += `.${this.opt.className}`; 2055 } 2056 2057 this.log(`Removal selector "${sel}"`); 2058 this.iterator.forEachNode( 2059 NodeFilter.SHOW_ELEMENT, 2060 (node) => { 2061 this.unwrapMatches(node); 2062 }, 2063 (node) => { 2064 const matchesSel = DOMIterator.matches(node, sel), 2065 matchesExclude = this.matchesExclude(node); 2066 2067 if (!matchesSel || matchesExclude) { 2068 return NodeFilter.FILTER_REJECT; 2069 } else { 2070 return NodeFilter.FILTER_ACCEPT; 2071 } 2072 }, 2073 this.opt.done 2074 ); 2075 } 2076} 2077 2078function Mark(ctx) { 2079 const instance = new Mark$1(ctx); 2080 2081 this.mark = (sv, opt) => { 2082 instance.mark(sv, opt); 2083 return this; 2084 }; 2085
2086 this.markRegExp = (sv, opt) => { 2087 instance.markRegExp(sv, opt); 2088 return this; 2089 }; 2090 2091 this.markRanges = (sv, opt) => { 2092 instance.markRanges(sv, opt); 2093 return this; 2094 }; 2095 2096 this.unmark = (opt) => { 2097 instance.unmark(opt); 2098 return this; 2099 }; 2100 2101 return this; 2102}
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.