PageSourceSearch

https://selsingen.de/media/com_miniteklivesearch/js/highlight.js?b6d08d

js selsingen.de collected 2026-10-02 08:46:04 UTC 63,346 bytes, 2,102 lines download raw bytes

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

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