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