[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/media/system/js/ -> highlight.js (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer