# Source: TextHighlighter.js

    (function (global) {
        'use strict';

        var
            /**
             * Attribute added by default to every highlight.
             * @type {string}
             */
            DATA_ATTR = 'data-highlighted',

            /**
             * Attribute used to group highlight wrappers.
             * @type {string}
             */
            TIMESTAMP_ATTR = 'data-timestamp',

            NODE_TYPE = {
                ELEMENT_NODE: 1,
                TEXT_NODE: 3
            },

            /**
             * Don't highlight content of these tags.
             * @type {string[]}
             */
            IGNORE_TAGS = [
                'SCRIPT', 'STYLE', 'SELECT', 'OPTION', 'BUTTON', 'OBJECT', 'APPLET', 'VIDEO', 'AUDIO', 'CANVAS', 'EMBED',
                'PARAM', 'METER', 'PROGRESS'
            ];

        /**
         * Returns true if elements a i b have the same color.
         * @param {Node} a
         * @param {Node} b
         * @returns {boolean}
         */
        function haveSameColor(a, b) {
            return dom(a).color() === dom(b).color();
        }

        /**
         * Fills undefined values in obj with default properties with the same name from source object.
         * @param {object} obj - target object
         * @param {object} source - source object with default values
         * @returns {object}
         */
        function defaults(obj, source) {
            obj = obj || {};

            for (var prop in source) {
                if (source.hasOwnProperty(prop) && obj[prop] === void 0) {
                    obj[prop] = source[prop];
                }
            }

            return obj;
        }

        /**
         * Returns array without duplicated values.
         * @param {Array} arr
         * @returns {Array}
         */
        function unique(arr) {
            return arr.filter(function (value, idx, self) {
                return self.indexOf(value) === idx;
            });
        }

        /**
         * Takes range object as parameter and refines it boundaries
         * @param range
         * @returns {object} refined boundaries and initial state of highlighting algorithm.
         */
        function refineRangeBoundaries(range) {
            var startContainer = range.startContainer,
                endContainer = range.endContainer,
                ancestor = range.commonAncestorContainer,
                goDeeper = true;

            if (range.endOffset === 0) {
                while (!endContainer.previousSibling && endContainer.parentNode !== ancestor) {
                    endContainer = endContainer.parentNode;
                }
                endContainer = endContainer.previousSibling;
            } else if (endContainer.nodeType === NODE_TYPE.TEXT_NODE) {
                if (range.endOffset < endContainer.nodeValue.length) {
                    endContainer.splitText(range.endOffset);
                }
            } else if (range.endOffset > 0) {
                endContainer = endContainer.childNodes.item(range.endOffset - 1);
            }

            if (startContainer.nodeType === NODE_TYPE.TEXT_NODE) {
                if (range.startOffset === startContainer.nodeValue.length) {
                    goDeeper = false;
                } else if (range.startOffset > 0) {
                    startContainer = startContainer.splitText(range.startOffset);
                    if (endContainer === startContainer.previousSibling) {
                        endContainer = startContainer;
                    }
                }
            } else if (range.startOffset < startContainer.childNodes.length) {
                startContainer = startContainer.childNodes.item(range.startOffset);
            } else {
                startContainer = startContainer.nextSibling;
            }

            return {
                startContainer: startContainer,
                endContainer: endContainer,
                goDeeper: goDeeper
            };
        }

        /**
         * Sorts array of DOM elements by its depth in DOM tree.
         * @param {HTMLElement[]} arr - array to sort.
         * @param {boolean} descending - order of sort.
         */
        function sortByDepth(arr, descending) {
            arr.sort(function (a, b) {
                return dom(descending ? b : a).parents().length - dom(descending ? a : b).parents().length;
            });
        }

        /**
         * Groups given highlights by timestamp.
         * @param {Array} highlights
         * @returns {Array} Grouped highlights.
         */
        function groupHighlights(highlights) {
            var order = [],
                chunks = {},
                grouped = [];

            highlights.forEach(function (hl) {
                var timestamp = hl.getAttribute(TIMESTAMP_ATTR);

                if (typeof chunks[timestamp] === 'undefined') {
                    chunks[timestamp] = [];
                    order.push(timestamp);
                }

                chunks[timestamp].push(hl);
            });

            order.forEach(function (timestamp) {
                var group = chunks[timestamp];

                grouped.push({
                    chunks: group,
                    timestamp: timestamp,
                    toString: function () {
                        return group.map(function (h) {
                            return h.textContent;
                        }).join('');
                    }
                });
            });

            return grouped;
        }

        /**
         * Utility functions to make DOM manipulation easier.
         * @param {Node|HTMLElement} [el] - base DOM element to manipulate
         * @returns {object}
         */
        var dom = function (el) {

            return /** @lends dom **/ {

                /**
                 * Adds class to element.
                 * @param {string} className
                 */
                addClass: function (className) {
                    if (el.classList) {
                        el.classList.add(className);
                    } else {
                        el.className += ' ' + className;
                    }
                },

                /**
                 * Removes class from element.
                 * @param {string} className
                 */
                removeClass: function (className) {
                    if (el.classList) {
                        el.classList.remove(className);
                    } else {
                        el.className = el.className.replace(
                            new RegExp('(^|\\b)' + className + '(\\b|$)', 'gi'), ' '
                        );
                    }
                },

                /**
                 * Prepends child nodes to base element.
                 * @param {Node[]} nodesToPrepend
                 */
                prepend: function (nodesToPrepend) {
                    var nodes = Array.prototype.slice.call(nodesToPrepend),
                        i = nodes.length;

                    while (i--) {
                        el.insertBefore(nodes[i], el.firstChild);
                    }
                },

                /**
                 * Appends child nodes to base element.
                 * @param {Node[]} nodesToAppend
                 */
                append: function (nodesToAppend) {
                    var nodes = Array.prototype.slice.call(nodesToAppend);

                    for (var i = 0, len = nodes.length; i < len; ++i) {
                        el.appendChild(nodes[i]);
                    }
                },

                /**
                 * Inserts base element after refEl.
                 * @param {Node} refEl - node after which base element will be inserted
                 * @returns {Node} - inserted element
                 */
                insertAfter: function (refEl) {
                    return refEl.parentNode.insertBefore(el, refEl.nextSibling);
                },

                /**
                 * Inserts base element before refEl.
                 * @param {Node} refEl - node before which base element will be inserted
                 * @returns {Node} - inserted element
                 */
                insertBefore: function (refEl) {
                    return refEl.parentNode.insertBefore(el, refEl);
                },

                /**
                 * Removes base element from DOM.
                 */
                remove: function () {
                    el.parentNode.removeChild(el);
                    el = null;
                },

                /**
                 * Returns true if base element contains given child.
                 * @param {Node|HTMLElement} child
                 * @returns {boolean}
                 */
                contains: function (child) {
                    return el !== child && el.contains(child);
                },

                /**
                 * Wraps base element in wrapper element.
                 * @param {HTMLElement} wrapper
                 * @returns {HTMLElement} wrapper element
                 */
                wrap: function (wrapper) {
                    if (el.parentNode) {
                        el.parentNode.insertBefore(wrapper, el);
                    }

                    wrapper.appendChild(el);
                    return wrapper;
                },

                /**
                 * Unwraps base element.
                 * @returns {Node[]} - child nodes of unwrapped element.
                 */
                unwrap: function () {
                    var nodes = Array.prototype.slice.call(el.childNodes),
                        wrapper;

                    nodes.forEach(function (node) {
                        wrapper = node.parentNode;
                        dom(node).insertBefore(node.parentNode);
                        dom(wrapper).remove();
                    });

                    return nodes;
                },

                /**
                 * Returns array of base element parents.
                 * @returns {HTMLElement[]}
                 */
                parents: function () {
                    var parent, path = [];

                    while (!!(parent = el.parentNode)) {
                        path.push(parent);
                        el = parent;
                    }

                    return path;
                },

                /**
                 * Normalizes text nodes within base element, ie. merges sibling text nodes and assures that every
                 * element node has only one text node.
                 * It should does the same as standard element.normalize, but IE implements it incorrectly.
                 */
                normalizeTextNodes: function () {
                    if (!el) {
                        return;
                    }

                    if (el.nodeType === NODE_TYPE.TEXT_NODE) {
                        while (el.nextSibling && el.nextSibling.nodeType === NODE_TYPE.TEXT_NODE) {
                            el.nodeValue += el.nextSibling.nodeValue;
                            el.parentNode.removeChild(el.nextSibling);
                        }
                    } else {
                        dom(el.firstChild).normalizeTextNodes();
                    }
                    dom(el.nextSibling).normalizeTextNodes();
                },

                /**
                 * Returns element background color.
                 * @returns {CSSStyleDeclaration.backgroundColor}
                 */
                color: function () {
                    return el.style.backgroundColor;
                },

                /**
                 * Creates dom element from given html string.
                 * @param {string} html
                 * @returns {NodeList}
                 */
                fromHTML: function (html) {
                    var div = document.createElement('div');
                    div.innerHTML = html;
                    return div.childNodes;
                },

                /**
                 * Returns first range of the window of base element.
                 * @returns {Range}
                 */
                getRange: function () {
                    var selection = dom(el).getSelection(),
                        range;

                    if (selection.rangeCount > 0) {
                        range = selection.getRangeAt(0);
                    }

                    return range;
                },

                /**
                 * Removes all ranges of the window of base element.
                 */
                removeAllRanges: function () {
                    var selection = dom(el).getSelection();
                    selection.removeAllRanges();
                },

                /**
                 * Returns selection object of the window of base element.
                 * @returns {Selection}
                 */
                getSelection: function () {
                    return dom(el).getWindow().getSelection();
                },

                /**
                 * Returns window of the base element.
                 * @returns {Window}
                 */
                getWindow: function () {
                    return dom(el).getDocument().defaultView;
                },

                /**
                 * Returns document of the base element.
                 * @returns {HTMLDocument}
                 */
                getDocument: function () {
                    // if ownerDocument is null then el is the document itself.
                    return el.ownerDocument || el;
                }

            };
        };

        function bindEvents(el, scope) {
            el.addEventListener('mouseup', scope.highlightHandler.bind(scope));
            el.addEventListener('touchend', scope.highlightHandler.bind(scope));
        }

        function unbindEvents(el, scope) {
            el.removeEventListener('mouseup', scope.highlightHandler.bind(scope));
            el.removeEventListener('touchend', scope.highlightHandler.bind(scope));
        }

        /**
         * Creates TextHighlighter instance and binds to given DOM elements.
         * @param {HTMLElement} element - DOM element to which highlighted will be applied.
         * @param {object} [options] - additional options.
         * @param {string} options.color - highlight color.
         * @param {string} options.highlightedClass - class added to highlight, 'highlighted' by default.
         * @param {string} options.contextClass - class added to element to which highlighter is applied,
         *  'highlighter-context' by default.
         * @param {function} options.onRemoveHighlight - function called before highlight is removed. Highlight is
         *  passed as param. Function should return true if highlight should be removed, or false - to prevent removal.
         * @param {function} options.onBeforeHighlight - function called before highlight is created. Range object is
         *  passed as param. Function should return true to continue processing, or false - to prevent highlighting.
         * @param {function} options.onAfterHighlight - function called after highlight is created. Array of created
         * wrappers is passed as param.
         * @class TextHighlighter
         */
        function TextHighlighter(element, options) {
            if (!element) {
                throw 'Missing anchor element';
            }

            this.el = element;
            this.options = defaults(options, {
                color: '#ffff7b',
                highlightedClass: 'highlighted',
                contextClass: 'highlighter-context',
                onRemoveHighlight: function () { return true; },
                onBeforeHighlight: function () { return true; },
                onAfterHighlight: function () { }
            });

            dom(this.el).addClass(this.options.contextClass);
            bindEvents(this.el, this);
        }

        /**
         * Permanently disables highlighting.
         * Unbinds events and remove context element class.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.destroy = function () {
            unbindEvents(this.el, this);
            dom(this.el).removeClass(this.options.contextClass);
        };

        TextHighlighter.prototype.highlightHandler = function () {
            this.doHighlight();
        };

        /**
         * Highlights current range.
         * @param {boolean} keepRange - Don't remove range after highlighting. Default: false.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.doHighlight = function (keepRange) {
            var range = dom(this.el).getRange(),
                wrapper,
                createdHighlights,
                normalizedHighlights,
                timestamp;

            if (!range || range.collapsed) {
                return;
            }

            if (this.options.onBeforeHighlight(range) === true) {
                timestamp = +new Date();
                wrapper = TextHighlighter.createWrapper(this.options);
                wrapper.setAttribute(TIMESTAMP_ATTR, timestamp);

                createdHighlights = this.highlightRange(range, wrapper);
                normalizedHighlights = this.normalizeHighlights(createdHighlights);

                this.options.onAfterHighlight(range, normalizedHighlights, timestamp);
            }

            if (!keepRange) {
                dom(this.el).removeAllRanges();
            }
        };

        /**
         * Highlights range.
         * Wraps text of given range object in wrapper element.
         * @param {Range} range
         * @param {HTMLElement} wrapper
         * @returns {Array} - array of created highlights.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.highlightRange = function (range, wrapper) {
            if (!range || range.collapsed) {
                return [];
            }

            var result = refineRangeBoundaries(range),
                startContainer = result.startContainer,
                endContainer = result.endContainer,
                goDeeper = result.goDeeper,
                done = false,
                node = startContainer,
                highlights = [],
                highlight,
                wrapperClone,
                nodeParent;

            do {
                if (goDeeper && node.nodeType === NODE_TYPE.TEXT_NODE) {

                    if (IGNORE_TAGS.indexOf(node.parentNode.tagName) === -1 && node.nodeValue.trim() !== '') {
                        wrapperClone = wrapper.cloneNode(true);
                        wrapperClone.setAttribute(DATA_ATTR, true);
                        nodeParent = node.parentNode;

                        // highlight if a node is inside the el
                        if (dom(this.el).contains(nodeParent) || nodeParent === this.el) {
                            highlight = dom(node).wrap(wrapperClone);
                            highlights.push(highlight);
                        }
                    }

                    goDeeper = false;
                }
                if (node === endContainer && !(endContainer.hasChildNodes() && goDeeper)) {
                    done = true;
                }

                if (node.tagName && IGNORE_TAGS.indexOf(node.tagName) > -1) {

                    if (endContainer.parentNode === node) {
                        done = true;
                    }
                    goDeeper = false;
                }
                if (goDeeper && node.hasChildNodes()) {
                    node = node.firstChild;
                } else if (node.nextSibling) {
                    node = node.nextSibling;
                    goDeeper = true;
                } else {
                    node = node.parentNode;
                    goDeeper = false;
                }
            } while (!done);

            return highlights;
        };

        /**
         * Normalizes highlights. Ensures that highlighting is done with use of the smallest possible number of
         * wrapping HTML elements.
         * Flattens highlights structure and merges sibling highlights. Normalizes text nodes within highlights.
         * @param {Array} highlights - highlights to normalize.
         * @returns {Array} - array of normalized highlights. Order and number of returned highlights may be different than
         * input highlights.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.normalizeHighlights = function (highlights) {
            var normalizedHighlights;

            this.flattenNestedHighlights(highlights);
            this.mergeSiblingHighlights(highlights);

            // omit removed nodes
            normalizedHighlights = highlights.filter(function (hl) {
                return hl.parentElement ? hl : null;
            });

            normalizedHighlights = unique(normalizedHighlights);
            normalizedHighlights.sort(function (a, b) {
                return a.offsetTop - b.offsetTop || a.offsetLeft - b.offsetLeft;
            });

            return normalizedHighlights;
        };

        /**
         * Flattens highlights structure.
         * Note: this method changes input highlights - their order and number after calling this method may change.
         * @param {Array} highlights - highlights to flatten.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.flattenNestedHighlights = function (highlights) {
            var again,
                self = this;

            sortByDepth(highlights, true);

            function flattenOnce() {
                var again = false;

                highlights.forEach(function (hl, i) {
                    var parent = hl.parentElement,
                        parentPrev = parent.previousSibling,
                        parentNext = parent.nextSibling;

                    if (self.isHighlight(parent)) {

                        if (!haveSameColor(parent, hl)) {

                            if (!hl.nextSibling) {
                                dom(hl).insertBefore(parentNext || parent);
                                again = true;
                            }

                            if (!hl.previousSibling) {
                                dom(hl).insertAfter(parentPrev || parent);
                                again = true;
                            }

                            if (!parent.hasChildNodes()) {
                                dom(parent).remove();
                            }

                        } else {
                            parent.replaceChild(hl.firstChild, hl);
                            highlights[i] = parent;
                            again = true;
                        }

                    }

                });

                return again;
            }

            do {
                again = flattenOnce();
            } while (again);
        };

        /**
         * Merges sibling highlights and normalizes descendant text nodes.
         * Note: this method changes input highlights - their order and number after calling this method may change.
         * @param highlights
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.mergeSiblingHighlights = function (highlights) {
            var self = this;

            function shouldMerge(current, node) {
                return node && node.nodeType === NODE_TYPE.ELEMENT_NODE &&
                    haveSameColor(current, node) &&
                    self.isHighlight(node);
            }

            highlights.forEach(function (highlight) {
                var prev = highlight.previousSibling,
                    next = highlight.nextSibling;

                if (shouldMerge(highlight, prev)) {
                    dom(highlight).prepend(prev.childNodes);
                    dom(prev).remove();
                }
                if (shouldMerge(highlight, next)) {
                    dom(highlight).append(next.childNodes);
                    dom(next).remove();
                }

                dom(highlight).normalizeTextNodes();
            });
        };

        /**
         * Sets highlighting color.
         * @param {string} color - valid CSS color.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.setColor = function (color) {
            this.options.color = color;
        };

        /**
         * Returns highlighting color.
         * @returns {string}
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.getColor = function () {
            return this.options.color;
        };

        /**
         * Removes highlights from element. If element is a highlight itself, it is removed as well.
         * If no element is given, all highlights all removed.
         * @param {HTMLElement} [element] - element to remove highlights from
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.removeHighlights = function (element) {
            var container = element || this.el,
                highlights = this.getHighlights({ container: container }),
                self = this;

            function mergeSiblingTextNodes(textNode) {
                var prev = textNode.previousSibling,
                    next = textNode.nextSibling;

                if (prev && prev.nodeType === NODE_TYPE.TEXT_NODE) {
                    textNode.nodeValue = prev.nodeValue + textNode.nodeValue;
                    dom(prev).remove();
                }
                if (next && next.nodeType === NODE_TYPE.TEXT_NODE) {
                    textNode.nodeValue = textNode.nodeValue + next.nodeValue;
                    dom(next).remove();
                }
            }

            function removeHighlight(highlight) {
                var textNodes = dom(highlight).unwrap();

                textNodes.forEach(function (node) {
                    mergeSiblingTextNodes(node);
                });
            }

            sortByDepth(highlights, true);

            highlights.forEach(function (hl) {
                if (self.options.onRemoveHighlight(hl) === true) {
                    removeHighlight(hl);
                }
            });
        };

        /**
         * Returns highlights from given container.
         * @param params
         * @param {HTMLElement} [params.container] - return highlights from this element. Default: the element the
         * highlighter is applied to.
         * @param {boolean} [params.andSelf] - if set to true and container is a highlight itself, add container to
         * returned results. Default: true.
         * @param {boolean} [params.grouped] - if set to true, highlights are grouped in logical groups of highlights added
         * in the same moment. Each group is an object which has got array of highlights, 'toString' method and 'timestamp'
         * property. Default: false.
         * @returns {Array} - array of highlights.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.getHighlights = function (params) {
            params = defaults(params, {
                container: this.el,
                andSelf: true,
                grouped: false
            });

            var nodeList = params.container.querySelectorAll('[' + DATA_ATTR + ']'),
                highlights = Array.prototype.slice.call(nodeList);

            if (params.andSelf === true && params.container.hasAttribute(DATA_ATTR)) {
                highlights.push(params.container);
            }

            if (params.grouped) {
                highlights = groupHighlights(highlights);
            }

            return highlights;
        };

        /**
         * Returns true if element is a highlight.
         * All highlights have 'data-highlighted' attribute.
         * @param el - element to check.
         * @returns {boolean}
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.isHighlight = function (el) {
            return el && el.nodeType === NODE_TYPE.ELEMENT_NODE && el.hasAttribute(DATA_ATTR);
        };

        /**
         * Serializes all highlights in the element the highlighter is applied to.
         * @returns {string} - stringified JSON with highlights definition
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.serializeHighlights = function () {
            var highlights = this.getHighlights(),
                refEl = this.el,
                hlDescriptors = [];

            function getElementPath(el, refElement) {
                var path = [],
                    childNodes;

                do {
                    childNodes = Array.prototype.slice.call(el.parentNode.childNodes);
                    path.unshift(childNodes.indexOf(el));
                    el = el.parentNode;
                } while (el !== refElement || !el);

                return path;
            }

            sortByDepth(highlights, false);

            highlights.forEach(function (highlight) {
                var offset = 0, // Hl offset from previous sibling within parent node.
                    length = highlight.textContent.length,
                    hlPath = getElementPath(highlight, refEl),
                    wrapper = highlight.cloneNode(true);

                wrapper.innerHTML = '';
                wrapper = wrapper.outerHTML;

                if (highlight.previousSibling && highlight.previousSibling.nodeType === NODE_TYPE.TEXT_NODE) {
                    offset = highlight.previousSibling.length;
                }

                hlDescriptors.push([
                    wrapper,
                    highlight.textContent,
                    hlPath.join(':'),
                    offset,
                    length
                ]);
            });

            return JSON.stringify(hlDescriptors);
        };

        /**
         * Deserializes highlights.
         * @throws exception when can't parse JSON or JSON has invalid structure.
         * @param {object} json - JSON object with highlights definition.
         * @returns {Array} - array of deserialized highlights.
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.deserializeHighlights = function (json) {
            var hlDescriptors,
                highlights = [],
                self = this;

            if (!json) {
                return highlights;
            }

            try {
                hlDescriptors = JSON.parse(json);
            } catch (e) {
                throw "Can't parse JSON: " + e;
            }

            function deserializationFn(hlDescriptor) {
                var hl = {
                        wrapper: hlDescriptor[0],
                        text: hlDescriptor[1],
                        path: hlDescriptor[2].split(':'),
                        offset: hlDescriptor[3],
                        length: hlDescriptor[4]
                    },
                    elIndex = hl.path.pop(),
                    node = self.el,
                    hlNode,
                    highlight,
                    idx;

                while (!!(idx = hl.path.shift())) {
                    node = node.childNodes[idx];
                }

                if (node.childNodes[elIndex-1] && node.childNodes[elIndex-1].nodeType === NODE_TYPE.TEXT_NODE) {
                    elIndex -= 1;
                }

                node = node.childNodes[elIndex];
                hlNode = node.splitText(hl.offset);
                hlNode.splitText(hl.length);

                if (hlNode.nextSibling && !hlNode.nextSibling.nodeValue) {
                    dom(hlNode.nextSibling).remove();
                }

                if (hlNode.previousSibling && !hlNode.previousSibling.nodeValue) {
                    dom(hlNode.previousSibling).remove();
                }

                highlight = dom(hlNode).wrap(dom().fromHTML(hl.wrapper)[0]);
                highlights.push(highlight);
            }

            hlDescriptors.forEach(function (hlDescriptor) {
                try {
                    deserializationFn(hlDescriptor);
                } catch (e) {
                    if (console && console.warn) {
                        console.warn("Can't deserialize highlight descriptor. Cause: " + e);
                    }
                }
            });

            return highlights;
        };

        /**
         * Finds and highlights given text.
         * @param {string} text - text to search for
         * @param {boolean} [caseSensitive] - if set to true, performs case sensitive search (default: true)
         * @memberof TextHighlighter
         */
        TextHighlighter.prototype.find = function (text, caseSensitive) {
            var wnd = dom(this.el).getWindow(),
                scrollX = wnd.scrollX,
                scrollY = wnd.scrollY,
                caseSens = (typeof caseSensitive === 'undefined' ? true : caseSensitive);

            dom(this.el).removeAllRanges();

            if (wnd.find) {
                while (wnd.find(text, caseSens)) {
                    this.doHighlight(true);
                }
            } else if (wnd.document.body.createTextRange) {
                var textRange = wnd.document.body.createTextRange();
                textRange.moveToElementText(this.el);
                while (textRange.findText(text, 1, caseSens ? 4 : 0)) {
                    if (!dom(this.el).contains(textRange.parentElement()) && textRange.parentElement() !== this.el) {
                        break;
                    }

                    textRange.select();
                    this.doHighlight(true);
                    textRange.collapse(false);
                }
            }

            dom(this.el).removeAllRanges();
            wnd.scrollTo(scrollX, scrollY);
        };

        /**
         * Creates wrapper for highlights.
         * TextHighlighter instance calls this method each time it needs to create highlights and pass options retrieved
         * in constructor.
         * @param {object} options - the same object as in TextHighlighter constructor.
         * @returns {HTMLElement}
         * @memberof TextHighlighter
         * @static
         */
        TextHighlighter.createWrapper = function (options) {
            var span = document.createElement('span');
            span.style.backgroundColor = options.color;
            span.className = options.highlightedClass;
            return span;
        };

        global.TextHighlighter = TextHighlighter;
    })(window);