Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

/**
 * Text range module for Rangy.
 * Text-based manipulation and searching of ranges and selections.
 *
 * Features
 *
 * - Ability to move range boundaries by character or word offsets
 * - Customizable word tokenizer
 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
 *   sensitivity
 * - Selection and range save/restore as text offsets within a node
 * - Methods to return visible text within a range or selection
 * - innerText method for elements
 *
 * References
 *
 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
 * http://aryeh.name/spec/innertext/innertext.html
 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
 *
 * Part of Rangy, a cross-browser JavaScript range and selection library
 * https://github.com/timdown/rangy
 *
 * Depends on Rangy core.
 *
 * Copyright 2022, Tim Down
 * Licensed under the MIT license.
 * Version: 1.3.1
 * Build date: 17 August 2022
 */

/**
 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
 *
 * First, a <br>: this is relatively simple. For the following HTML:
 *
 * 1 <br>2
 *
 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
 *   textarea, the space is present) and allow the caret to be placed after it.
 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right
 *   arrow keys show this) and includes the space in the selection.
 *
 * The other case is the line break or breaks implied by block elements. For the following HTML:
 *
 * <p>1 </p><p>2<p>
 *
 * - WebKit does not acknowledge the space in any way
 * - Firefox, IE and Opera as per <br>
 *
 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
 *
 * <p style="white-space: pre-line">1
 * 2</p>
 *
 * - Firefox and WebKit include the space in caret positions
 * - IE does not support pre-line up to and including version 9
 * - Opera ignores the space
 * - Trailing space only renders if there is a non-collapsed character in the line
 *
 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
 * feature-tested
 */
(function(factory, root) {
    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
    factory(root.rangy);
})(function(rangy) {
    rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
        var UNDEF = "undefined";
        var CHARACTER = "character", WORD = "word";
        var dom = api.dom, util = api.util;
        var extend = util.extend;
        var createOptions = util.createOptions;
        var getBody = dom.getBody;


        var spacesRegex = /^[ \t\f\r\n]+$/;
        var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
        var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
        var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
        var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;

        var defaultLanguage = "en";

        var isDirectionBackward = api.Selection.isDirectionBackward;

        // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
        // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
        var trailingSpaceInBlockCollapses = false;
        var trailingSpaceBeforeBrCollapses = false;
        var trailingSpaceBeforeBlockCollapses = false;
        var trailingSpaceBeforeLineBreakInPreLineCollapses = true;

        (function() {
            var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
            var p = el.firstChild;
            var sel = api.getSelection();
            sel.collapse(p.lastChild, 2);
            sel.setStart(p.firstChild, 0);
            trailingSpaceInBlockCollapses = ("" + sel).length == 1;

            el.innerHTML = "1 <br />";
            sel.collapse(el, 2);
            sel.setStart(el.firstChild, 0);
            trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;

            el.innerHTML = "1 <p>1</p>";
            sel.collapse(el, 2);
            sel.setStart(el.firstChild, 0);
            trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;

            dom.removeNode(el);
            sel.removeAllRanges();
        })();

        /*----------------------------------------------------------------------------------------------------------------*/

        // This function must create word and non-word tokens for the whole of the text supplied to it
        function defaultTokenizer(chars, wordOptions) {
            var word = chars.join(""), result, tokenRanges = [];

            function createTokenRange(start, end, isWord) {
                tokenRanges.push( { start: start, end: end, isWord: isWord } );
            }

            // Match words and mark characters
            var lastWordEnd = 0, wordStart, wordEnd;
            while ( (result = wordOptions.wordRegex.exec(word)) ) {
                wordStart = result.index;
                wordEnd = wordStart + result[0].length;

                // Create token for non-word characters preceding this word
                if (wordStart > lastWordEnd) {
                    createTokenRange(lastWordEnd, wordStart, false);
                }

                // Get trailing space characters for word
                if (wordOptions.includeTrailingSpace) {
                    while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
                        ++wordEnd;
                    }
                }
                createTokenRange(wordStart, wordEnd, true);
                lastWordEnd = wordEnd;
            }

            // Create token for trailing non-word characters, if any exist
            if (lastWordEnd < chars.length) {
                createTokenRange(lastWordEnd, chars.length, false);
            }

            return tokenRanges;
        }

        function convertCharRangeToToken(chars, tokenRange) {
            var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
            var token = {
                isWord: tokenRange.isWord,
                chars: tokenChars,
                toString: function() {
                    return tokenChars.join("");
                }
            };
            for (var i = 0, len = tokenChars.length; i < len; ++i) {
                tokenChars[i].token = token;
            }
            return token;
        }

        function tokenize(chars, wordOptions, tokenizer) {
            var tokenRanges = tokenizer(chars, wordOptions);
            var tokens = [];
            for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
                tokens.push( convertCharRangeToToken(chars, tokenRange) );
            }
            return tokens;
        }

        var defaultCharacterOptions = {
            includeBlockContentTrailingSpace: true,
            includeSpaceBeforeBr: true,
            includeSpaceBeforeBlock: true,
            includePreLineTrailingSpace: true,
            ignoreCharacters: ""
        };

        function normalizeIgnoredCharacters(ignoredCharacters) {
            // Check if character is ignored
            var ignoredChars = ignoredCharacters || "";

            // Normalize ignored characters into a string consisting of characters in ascending order of character code
            var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
            ignoredCharsArray.sort(function(char1, char2) {
                return char1.charCodeAt(0) - char2.charCodeAt(0);
            });

            /// Convert back to a string and remove duplicates
            return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
        }

        var defaultCaretCharacterOptions = {
            includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
            includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
            includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
            includePreLineTrailingSpace: true
        };

        var defaultWordOptions = {
            "en": {
                wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
                includeTrailingSpace: false,
                tokenizer: defaultTokenizer
            }
        };

        var defaultFindOptions = {
            caseSensitive: false,
            withinRange: null,
            wholeWordsOnly: false,
            wrap: false,
            direction: "forward",
            wordOptions: null,
            characterOptions: null
        };

        var defaultMoveOptions = {
            wordOptions: null,
            characterOptions: null
        };

        var defaultExpandOptions = {
            wordOptions: null,
            characterOptions: null,
            trim: false,
            trimStart: true,
            trimEnd: true
        };

        var defaultWordIteratorOptions = {
            wordOptions: null,
            characterOptions: null,
            direction: "forward"
        };

        function createWordOptions(options) {
            var lang, defaults;
            if (!options) {
                return defaultWordOptions[defaultLanguage];
            } else {
                lang = options.language || defaultLanguage;
                defaults = {};
                extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
                extend(defaults, options);
                return defaults;
            }
        }

        function createNestedOptions(optionsParam, defaults) {
            var options = createOptions(optionsParam, defaults);
            if (defaults.hasOwnProperty("wordOptions")) {
                options.wordOptions = createWordOptions(options.wordOptions);
            }
            if (defaults.hasOwnProperty("characterOptions")) {
                options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
            }
            return options;
        }

        /*----------------------------------------------------------------------------------------------------------------*/

        /* DOM utility functions */
        var getComputedStyleProperty = dom.getComputedStyleProperty;

        // Create cachable versions of DOM functions

        // Test for old IE's incorrect display properties
        var tableCssDisplayBlock;
        (function() {
            var table = document.createElement("table");
            var body = getBody(document);
            body.appendChild(table);
            tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
            body.removeChild(table);
        })();

        var defaultDisplayValueForTag = {
            table: "table",
            caption: "table-caption",
            colgroup: "table-column-group",
            col: "table-column",
            thead: "table-header-group",
            tbody: "table-row-group",
            tfoot: "table-footer-group",
            tr: "table-row",
            td: "table-cell",
            th: "table-cell"
        };

        // Corrects IE's "block" value for table-related elements
        function getComputedDisplay(el, win) {
            var display = getComputedStyleProperty(el, "display", win);
            var tagName = el.tagName.toLowerCase();
            return (display == "block" &&
                    tableCssDisplayBlock &&
                    defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
                defaultDisplayValueForTag[tagName] : display;
        }

        function isHidden(node) {
            var ancestors = getAncestorsAndSelf(node);
            for (var i = 0, len = ancestors.length; i < len; ++i) {
                if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
                    return true;
                }
            }

            return false;
        }

        function isVisibilityHiddenTextNode(textNode) {
            var el;
            return textNode.nodeType == 3 &&
                (el = textNode.parentNode) &&
                getComputedStyleProperty(el, "visibility") == "hidden";
        }

        /*----------------------------------------------------------------------------------------------------------------*/


        // "A block node is either an Element whose "display" property does not have
        // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
        // Document, or a DocumentFragment."
        function isBlockNode(node) {
            return node &&
                ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
                node.nodeType == 9 || node.nodeType == 11);
        }

        function getLastDescendantOrSelf(node) {
            var lastChild = node.lastChild;
            return lastChild ? getLastDescendantOrSelf(lastChild) : node;
        }

        function containsPositions(node) {
            return dom.isCharacterDataNode(node) ||
                !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
        }

        function getAncestors(node) {
            var ancestors = [];
            while (node.parentNode) {
                ancestors.unshift(node.parentNode);
                node = node.parentNode;
            }
            return ancestors;
        }

        function getAncestorsAndSelf(node) {
            return getAncestors(node).concat([node]);
        }

        function nextNodeDescendants(node) {
            while (node && !node.nextSibling) {
                node = node.parentNode;
            }
            if (!node) {
                return null;
            }
            return node.nextSibling;
        }

        function nextNode(node, excludeChildren) {
            if (!excludeChildren && node.hasChildNodes()) {
                return node.firstChild;
            }
            return nextNodeDescendants(node);
        }

        function previousNode(node) {
            var previous = node.previousSibling;
            if (previous) {
                node = previous;
                while (node.hasChildNodes()) {
                    node = node.lastChild;
                }
                return node;
            }
            var parent = node.parentNode;
            if (parent && parent.nodeType == 1) {
                return parent;
            }
            return null;
        }

        // Adpated from Aryeh's code.
        // "A whitespace node is either a Text node whose data is the empty string; or
        // a Text node whose data consists only of one or more tabs (0x0009), line
        // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
        // parent is an Element whose resolved value for "white-space" is "normal" or
        // "nowrap"; or a Text node whose data consists only of one or more tabs
        // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
        // parent is an Element whose resolved value for "white-space" is "pre-line"."
        function isWhitespaceNode(node) {
            if (!node || node.nodeType != 3) {
                return false;
            }
            var text = node.data;
            if (text === "") {
                return true;
            }
            var parent = node.parentNode;
            if (!parent || parent.nodeType != 1) {
                return false;
            }
            var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");

            return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
                (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
        }

        // Adpated from Aryeh's code.
        // "node is a collapsed whitespace node if the following algorithm returns
        // true:"
        function isCollapsedWhitespaceNode(node) {
            // "If node's data is the empty string, return true."
            if (node.data === "") {
                return true;
            }

            // "If node is not a whitespace node, return false."
            if (!isWhitespaceNode(node)) {
                return false;
            }

            // "Let ancestor be node's parent."
            var ancestor = node.parentNode;

            // "If ancestor is null, return true."
            if (!ancestor) {
                return true;
            }

            // "If the "display" property of some ancestor of node has resolved value "none", return true."
            if (isHidden(node)) {
                return true;
            }

            return false;
        }

        function isCollapsedNode(node) {
            var type = node.nodeType;
            return type == 7 /* PROCESSING_INSTRUCTION */ ||
                type == 8 /* COMMENT */ ||
                isHidden(node) ||
                /^(script|style)$/i.test(node.nodeName) ||
                isVisibilityHiddenTextNode(node) ||
                isCollapsedWhitespaceNode(node);
        }

        function isIgnoredNode(node, win) {
            var type = node.nodeType;
            return type == 7 /* PROCESSING_INSTRUCTION */ ||
                type == 8 /* COMMENT */ ||
                (type == 1 && getComputedDisplay(node, win) == "none");
        }

        /*----------------------------------------------------------------------------------------------------------------*/

        // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down

        function Cache() {
            this.store = {};
        }

        Cache.prototype = {
            get: function(key) {
                return this.store.hasOwnProperty(key) ? this.store[key] : null;
            },

            set: function(key, value) {
                return this.store[key] = value;
            }
        };

        var cachedCount = 0, uncachedCount = 0;

        function createCachingGetter(methodName, func, objProperty) {
            return function(args) {
                var cache = this.cache;
                if (cache.hasOwnProperty(methodName)) {
                    cachedCount++;
                    return cache[methodName];
                } else {
                    uncachedCount++;
                    var value = func.call(this, objProperty ? this[objProperty] : this, args);
                    cache[methodName] = value;
                    return value;
                }
            };
        }

        /*----------------------------------------------------------------------------------------------------------------*/

        function NodeWrapper(node, session) {
            this.node = node;
            this.session = session;
            this.cache = new Cache();
            this.positions = new Cache();
        }

        var nodeProto = {
            getPosition: function(offset) {
                var positions = this.positions;
                return positions.get(offset) || positions.set(offset, new Position(this, offset));
            },

            toString: function() {
                return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
            }
        };

        NodeWrapper.prototype = nodeProto;

        var EMPTY = "EMPTY",
            NON_SPACE = "NON_SPACE",
            UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
            COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
            TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
            TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
            TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
            PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
            TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
            INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";

        extend(nodeProto, {
            isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
            getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
            getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
            containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
            isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
            isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
            getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
            isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
            isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
            next: createCachingGetter("nextPos", nextNode, "node"),
            previous: createCachingGetter("previous", previousNode, "node"),

            getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
                var spaceRegex = null, collapseSpaces = false;
                var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
                var preLine = (cssWhitespace == "pre-line");
                if (preLine) {
                    spaceRegex = spacesMinusLineBreaksRegex;
                    collapseSpaces = true;
                } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
                    spaceRegex = spacesRegex;
                    collapseSpaces = true;
                }

                return {
                    node: textNode,
                    text: textNode.data,
                    spaceRegex: spaceRegex,
                    collapseSpaces: collapseSpaces,
                    preLine: preLine
                };
            }, "node"),

            hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
                var session = this.session;
                var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
                var firstPosInEl = session.getPosition(el, 0);

                var pos = backward ? posAfterEl : firstPosInEl;
                var endPos = backward ? firstPosInEl : posAfterEl;

                /*
                 <body><p>X  </p><p>Y</p></body>

                 Positions:

                 body:0:""
                 p:0:""
                 text:0:""
                 text:1:"X"
                 text:2:TRAILING_SPACE_IN_BLOCK
                 text:3:COLLAPSED_SPACE
                 p:1:""
                 body:1:"\n"
                 p:0:""
                 text:0:""
                 text:1:"Y"

                 A character is a TRAILING_SPACE_IN_BLOCK iff:

                 - There is no uncollapsed character after it within the visible containing block element

                 A character is a TRAILING_SPACE_BEFORE_BR iff:

                 - There is no uncollapsed character after it preceding a <br> element

                 An element has inner text iff

                 - It is not hidden
                 - It contains an uncollapsed character

                 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
                 */

                while (pos !== endPos) {
                    pos.prepopulateChar();
                    if (pos.isDefinitelyNonEmpty()) {
                        return true;
                    }
                    pos = backward ? pos.previousVisible() : pos.nextVisible();
                }

                return false;
            }, "node"),

            isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
                // Ensure that a block element containing a <br> is considered to have inner text
                var brs = el.getElementsByTagName("br");
                for (var i = 0, len = brs.length; i < len; ++i) {
                    if (!isCollapsedNode(brs[i])) {
                        return true;
                    }
                }
                return this.hasInnerText();
            }, "node"),

            getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
                if (el.tagName.toLowerCase() == "br") {
                    return "";
                } else {
                    switch (this.getComputedDisplay()) {
                        case "inline":
                            var child = el.lastChild;
                            while (child) {
                                if (!isIgnoredNode(child)) {
                                    return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
                                }
                                child = child.previousSibling;
                            }
                            break;
                        case "inline-block":
                        case "inline-table":
                        case "none":
                        case "table-column":
                        case "table-column-group":
                            break;
                        case "table-cell":
                            return "\t";
                        default:
                            return this.isRenderedBlock(true) ? "\n" : "";
                    }
                }
                return "";
            }, "node"),

            getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
                switch (this.getComputedDisplay()) {
                    case "inline":
                    case "inline-block":
                    case "inline-table":
                    case "none":
                    case "table-column":
                    case "table-column-group":
                    case "table-cell":
                        break;
                    default:
                        return this.isRenderedBlock(false) ? "\n" : "";
                }
                return "";
            }, "node")
        });

        /*----------------------------------------------------------------------------------------------------------------*/

        function Position(nodeWrapper, offset) {
            this.offset = offset;
            this.nodeWrapper = nodeWrapper;
            this.node = nodeWrapper.node;
            this.session = nodeWrapper.session;
            this.cache = new Cache();
        }

        function inspectPosition() {
            return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
        }

        var positionProto = {
            character: "",
            characterType: EMPTY,
            isBr: false,

            /*
            This method:
            - Fully populates positions that have characters that can be determined independently of any other characters.
            - Populates most types of space positions with a provisional character. The character is finalized later.
             */
            prepopulateChar: function() {
                var pos = this;
                if (!pos.prepopulatedChar) {
                    var node = pos.node, offset = pos.offset;
                    var visibleChar = "", charType = EMPTY;
                    var finalizedChar = false;
                    if (offset > 0) {
                        if (node.nodeType == 3) {
                            var text = node.data;
                            var textChar = text.charAt(offset - 1);

                            var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
                            var spaceRegex = nodeInfo.spaceRegex;
                            if (nodeInfo.collapseSpaces) {
                                if (spaceRegex.test(textChar)) {
                                    // "If the character at position is from set, append a single space (U+0020) to newdata and advance
                                    // position until the character at position is not from set."

                                    // We also need to check for the case where we're in a pre-line and we have a space preceding a
                                    // line break, because such spaces are collapsed in some browsers
                                    if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
                                    } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
                                        visibleChar = " ";
                                        charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
                                    } else {
                                        visibleChar = " ";
                                        //pos.checkForFollowingLineBreak = true;
                                        charType = COLLAPSIBLE_SPACE;
                                    }
                                } else {
                                    visibleChar = textChar;
                                    charType = NON_SPACE;
                                    finalizedChar = true;
                                }
                            } else {
                                visibleChar = textChar;
                                charType = UNCOLLAPSIBLE_SPACE;
                                finalizedChar = true;
                            }
                        } else {
                            var nodePassed = node.childNodes[offset - 1];
                            if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
                                if (nodePassed.tagName.toLowerCase() == "br") {
                                    visibleChar = "\n";
                                    pos.isBr = true;
                                    charType = COLLAPSIBLE_SPACE;
                                    finalizedChar = false;
                                } else {
                                    pos.checkForTrailingSpace = true;
                                }
                            }

                            // Check the leading space of the next node for the case when a block element follows an inline
                            // element or text node. In that case, there is an implied line break between the two nodes.
                            if (!visibleChar) {
                                var nextNode = node.childNodes[offset];
                                if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
                                    pos.checkForLeadingSpace = true;
                                }
                            }
                        }
                    }

                    pos.prepopulatedChar = true;
                    pos.character = visibleChar;
                    pos.characterType = charType;
                    pos.isCharInvariant = finalizedChar;
                }
            },

            isDefinitelyNonEmpty: function() {
                var charType = this.characterType;
                return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
            },

            // Resolve leading and trailing spaces, which may involve prepopulating other positions
            resolveLeadingAndTrailingSpaces: function() {
                if (!this.prepopulatedChar) {
                    this.prepopulateChar();
                }
                if (this.checkForTrailingSpace) {
                    var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
                    if (trailingSpace) {
                        this.isTrailingSpace = true;
                        this.character = trailingSpace;
                        this.characterType = COLLAPSIBLE_SPACE;
                    }
                    this.checkForTrailingSpace = false;
                }
                if (this.checkForLeadingSpace) {
                    var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
                    if (leadingSpace) {
                        this.isLeadingSpace = true;
                        this.character = leadingSpace;
                        this.characterType = COLLAPSIBLE_SPACE;
                    }
                    this.checkForLeadingSpace = false;
                }
            },

            getPrecedingUncollapsedPosition: function(characterOptions) {
                var pos = this, character;
                while ( (pos = pos.previousVisible()) ) {
                    character = pos.getCharacter(characterOptions);
                    if (character !== "") {
                        return pos;
                    }
                }

                return null;
            },

            getCharacter: function(characterOptions) {
                this.resolveLeadingAndTrailingSpaces();

                var thisChar = this.character, returnChar;

                // Check if character is ignored
                var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
                var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);

                // Check if this position's  character is invariant (i.e. not dependent on character options) and return it
                // if so
                if (this.isCharInvariant) {
                    returnChar = isIgnoredCharacter ? "" : thisChar;
                    return returnChar;
                }

                var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
                var cachedChar = this.cache.get(cacheKey);
                if (cachedChar !== null) {
                    return cachedChar;
                }

                // We need to actually get the character now
                var character = "";
                var collapsible = (this.characterType == COLLAPSIBLE_SPACE);

                var nextPos, previousPos;
                var gotPreviousPos = false;
                var pos = this;

                function getPreviousPos() {
                    if (!gotPreviousPos) {
                        previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
                        gotPreviousPos = true;
                    }
                    return previousPos;
                }

                // Disallow a collapsible space that is followed by a line break or is the last character
                if (collapsible) {
                    // Allow a trailing space that we've previously determined should be included
                    if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
                        character = "\n";
                    }
                    // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
                    // or follows a collapsible included space
                    else if (thisChar == " " &&
                            (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
                    }
                    // Allow a leading line break unless it follows a line break
                    else if (thisChar == "\n" && this.isLeadingSpace) {
                        if (getPreviousPos() && previousPos.character != "\n") {
                            character = "\n";
                        } else {
                        }
                    } else {
                        nextPos = this.nextUncollapsed();
                        if (nextPos) {
                            if (nextPos.isBr) {
                                this.type = TRAILING_SPACE_BEFORE_BR;
                            } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
                                this.type = TRAILING_SPACE_IN_BLOCK;
                            } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
                                this.type = TRAILING_SPACE_BEFORE_BLOCK;
                            }

                            if (nextPos.character == "\n") {
                                if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
                                } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
                                } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
                                } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
                                } else if (thisChar == "\n") {
                                    if (nextPos.isTrailingSpace) {
                                        if (this.isTrailingSpace) {
                                        } else if (this.isBr) {
                                            nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;

                                            if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
                                                nextPos.character = "";
                                            } else {
                                                nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
                                            }
                                        }
                                    } else {
                                        character = "\n";
                                    }
                                } else if (thisChar == " ") {
                                    character = " ";
                                } else {
                                }
                            } else {
                                character = thisChar;
                            }
                        } else {
                        }
                    }
                }

                if (ignoredChars.indexOf(character) > -1) {
                    character = "";
                }


                this.cache.set(cacheKey, character);

                return character;
            },

            equals: function(pos) {
                return !!pos && this.node === pos.node && this.offset === pos.offset;
            },

            inspect: inspectPosition,

            toString: function() {
                return this.character;
            }
        };

        Position.prototype = positionProto;

        extend(positionProto, {
            next: createCachingGetter("nextPos", function(pos) {
                var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
                if (!node) {
                    return null;
                }
                var nextNode, nextOffset, child;
                if (offset == nodeWrapper.getLength()) {
                    // Move onto the next node
                    nextNode = node.parentNode;
                    nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
                } else {
                    if (nodeWrapper.isCharacterDataNode()) {
                        nextNode = node;
                        nextOffset = offset + 1;
                    } else {
                        child = node.childNodes[offset];
                        // Go into the children next, if children there are
                        if (session.getNodeWrapper(child).containsPositions()) {
                            nextNode = child;
                            nextOffset = 0;
                        } else {
                            nextNode = node;
                            nextOffset = offset + 1;
                        }
                    }
                }

                return nextNode ? session.getPosition(nextNode, nextOffset) : null;
            }),

            previous: createCachingGetter("previous", function(pos) {
                var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
                var previousNode, previousOffset, child;
                if (offset == 0) {
                    previousNode = node.parentNode;
                    previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
                } else {
                    if (nodeWrapper.isCharacterDataNode()) {
                        previousNode = node;
                        previousOffset = offset - 1;
                    } else {
                        child = node.childNodes[offset - 1];
                        // Go into the children next, if children there are
                        if (session.getNodeWrapper(child).containsPositions()) {
                            previousNode = child;
                            previousOffset = dom.getNodeLength(child);
                        } else {
                            previousNode = node;
                            previousOffset = offset - 1;
                        }
                    }
                }
                return previousNode ? session.getPosition(previousNode, previousOffset) : null;
            }),

            /*
             Next and previous position moving functions that filter out

             - Hidden (CSS visibility/display) elements
             - Script and style elements
             */
            nextVisible: createCachingGetter("nextVisible", function(pos) {
                var next = pos.next();
                if (!next) {
                    return null;
                }
                var nodeWrapper = next.nodeWrapper, node = next.node;
                var newPos = next;
                if (nodeWrapper.isCollapsed()) {
                    // We're skipping this node and all its descendants
                    newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
                }
                return newPos;
            }),

            nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
                var nextPos = pos;
                while ( (nextPos = nextPos.nextVisible()) ) {
                    nextPos.resolveLeadingAndTrailingSpaces();
                    if (nextPos.character !== "") {
                        return nextPos;
                    }
                }
                return null;
            }),

            previousVisible: createCachingGetter("previousVisible", function(pos) {
                var previous = pos.previous();
                if (!previous) {
                    return null;
                }
                var nodeWrapper = previous.nodeWrapper, node = previous.node;
                var newPos = previous;
                if (nodeWrapper.isCollapsed()) {
                    // We're skipping this node and all its descendants
                    newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
                }
                return newPos;
            })
        });

        /*----------------------------------------------------------------------------------------------------------------*/

        var currentSession = null;

        var Session = (function() {
            function createWrapperCache(nodeProperty) {
                var cache = new Cache();

                return {
                    get: function(node) {
                        var wrappersByProperty = cache.get(node[nodeProperty]);
                        if (wrappersByProperty) {
                            for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
                                if (wrapper.node === node) {
                                    return wrapper;
                                }
                            }
                        }
                        return null;
                    },

                    set: function(nodeWrapper) {
                        var property = nodeWrapper.node[nodeProperty];
                        var wrappersByProperty = cache.get(property) || cache.set(property, []);
                        wrappersByProperty.push(nodeWrapper);
                    }
                };
            }

            var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");

            function Session() {
                this.initCaches();
            }

            Session.prototype = {
                initCaches: function() {
                    this.elementCache = uniqueIDSupported ? (function() {
                        var elementsCache = new Cache();

                        return {
                            get: function(el) {
                                return elementsCache.get(el.uniqueID);
                            },

                            set: function(elWrapper) {
                                elementsCache.set(elWrapper.node.uniqueID, elWrapper);
                            }
                        };
                    })() : createWrapperCache("tagName");

                    // Store text nodes keyed by data, although we may need to truncate this
                    this.textNodeCache = createWrapperCache("data");
                    this.otherNodeCache = createWrapperCache("nodeName");
                },

                getNodeWrapper: function(node) {
                    var wrapperCache;
                    switch (node.nodeType) {
                        case 1:
                            wrapperCache = this.elementCache;
                            break;
                        case 3:
                            wrapperCache = this.textNodeCache;
                            break;
                        default:
                            wrapperCache = this.otherNodeCache;
                            break;
                    }

                    var wrapper = wrapperCache.get(node);
                    if (!wrapper) {
                        wrapper = new NodeWrapper(node, this);
                        wrapperCache.set(wrapper);
                    }
                    return wrapper;
                },

                getPosition: function(node, offset) {
                    return this.getNodeWrapper(node).getPosition(offset);
                },

                getRangeBoundaryPosition: function(range, isStart) {
                    var prefix = isStart ? "start" : "end";
                    return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
                },

                detach: function() {
                    this.elementCache = this.textNodeCache = this.otherNodeCache = null;
                }
            };

            return Session;
        })();

        /*----------------------------------------------------------------------------------------------------------------*/

        function startSession() {
            endSession();
            return (currentSession = new Session());
        }

        function getSession() {
            return currentSession || startSession();
        }

        function endSession() {
            if (currentSession) {
                currentSession.detach();
            }
            currentSession = null;
        }

        /*----------------------------------------------------------------------------------------------------------------*/

        // Extensions to the rangy.dom utility object

        extend(dom, {
            nextNode: nextNode,
            previousNode: previousNode
        });

        /*----------------------------------------------------------------------------------------------------------------*/

        function createCharacterIterator(startPos, backward, endPos, characterOptions) {

            // Adjust the end position to ensure that it is actually reached
            if (endPos) {
                if (backward) {
                    if (isCollapsedNode(endPos.node)) {
                        endPos = startPos.previousVisible();
                    }
                } else {
                    if (isCollapsedNode(endPos.node)) {
                        endPos = endPos.nextVisible();
                    }
                }
            }

            var pos = startPos, finished = false;

            function next() {
                var charPos = null;
                if (backward) {
                    charPos = pos;
                    if (!finished) {
                        pos = pos.previousVisible();
                        finished = !pos || (endPos && pos.equals(endPos));
                    }
                } else {
                    if (!finished) {
                        charPos = pos = pos.nextVisible();
                        finished = !pos || (endPos && pos.equals(endPos));
                    }
                }
                if (finished) {
                    pos = null;
                }
                return charPos;
            }

            var previousTextPos, returnPreviousTextPos = false;

            return {
                next: function() {
                    if (returnPreviousTextPos) {
                        returnPreviousTextPos = false;
                        return previousTextPos;
                    } else {
                        var pos, character;
                        while ( (pos = next()) ) {
                            character = pos.getCharacter(characterOptions);
                            if (character) {
                                previousTextPos = pos;
                                return pos;
                            }
                        }
                        return null;
                    }
                },

                rewind: function() {
                    if (previousTextPos) {
                        returnPreviousTextPos = true;
                    } else {
                        throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
                    }
                },

                dispose: function() {
                    startPos = endPos = null;
                }
            };
        }

        var arrayIndexOf = Array.prototype.indexOf ?
            function(arr, val) {
                return arr.indexOf(val);
            } :
            function(arr, val) {
                for (var i = 0, len = arr.length; i < len; ++i) {
                    if (arr[i] === val) {
                        return i;
                    }
                }
                return -1;
            };

        // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
        // is called and there is no more tokenized text
        function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
            var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
            var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
            var tokenizer = wordOptions.tokenizer;

            // Consumes a word and the whitespace beyond it
            function consumeWord(forward) {
                var pos, textChar;
                var newChars = [], it = forward ? forwardIterator : backwardIterator;

                var passedWordBoundary = false, insideWord = false;

                while ( (pos = it.next()) ) {
                    textChar = pos.character;


                    if (allWhiteSpaceRegex.test(textChar)) {
                        if (insideWord) {
                            insideWord = false;
                            passedWordBoundary = true;
                        }
                    } else {
                        if (passedWordBoundary) {
                            it.rewind();
                            break;
                        } else {
                            insideWord = true;
                        }
                    }
                    newChars.push(pos);
                }


                return newChars;
            }

            // Get initial word surrounding initial position and tokenize it
            var forwardChars = consumeWord(true);
            var backwardChars = consumeWord(false).reverse();
            var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);

            // Create initial token buffers
            var forwardTokensBuffer = forwardChars.length ?
                tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];

            var backwardTokensBuffer = backwardChars.length ?
                tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];

            function inspectBuffer(buffer) {
                var textPositions = ["[" + buffer.length + "]"];
                for (var i = 0; i < buffer.length; ++i) {
                    textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
                }
                return textPositions;
            }


            return {
                nextEndToken: function() {
                    var lastToken, forwardChars;

                    // If we're down to the last token, consume character chunks until we have a word or run out of
                    // characters to consume
                    while ( forwardTokensBuffer.length == 1 &&
                        !(lastToken = forwardTokensBuffer[0]).isWord &&
                        (forwardChars = consumeWord(true)).length > 0) {

                        // Merge trailing non-word into next word and tokenize
                        forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
                    }

                    return forwardTokensBuffer.shift();
                },

                previousStartToken: function() {
                    var lastToken, backwardChars;

                    // If we're down to the last token, consume character chunks until we have a word or run out of
                    // characters to consume
                    while ( backwardTokensBuffer.length == 1 &&
                        !(lastToken = backwardTokensBuffer[0]).isWord &&
                        (backwardChars = consumeWord(false)).length > 0) {

                        // Merge leading non-word into next word and tokenize
                        backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
                    }

                    return backwardTokensBuffer.pop();
                },

                dispose: function() {
                    forwardIterator.dispose();
                    backwardIterator.dispose();
                    forwardTokensBuffer = backwardTokensBuffer = null;
                }
            };
        }

        function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
            var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
            if (count !== 0) {
                var backward = (count < 0);

                switch (unit) {
                    case CHARACTER:
                        charIterator = createCharacterIterator(pos, backward, null, characterOptions);
                        while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
                            ++unitsMoved;
                            newPos = currentPos;
                        }
                        nextPos = currentPos;
                        charIterator.dispose();
                        break;
                    case WORD:
                        var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
                        var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;

                        while ( (token = next()) && unitsMoved < absCount ) {
                            if (token.isWord) {
                                ++unitsMoved;
                                newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
                            }
                        }
                        break;
                    default:
                        throw new Error("movePositionBy: unit '" + unit + "' not implemented");
                }

                // Perform any necessary position tweaks
                if (backward) {
                    newPos = newPos.previousVisible();
                    unitsMoved = -unitsMoved;
                } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
                    // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
                    // before a block element (for example, the line break between "1" and "2" in the following HTML:
                    // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
                    // corresponds with a different selection position in most browsers from the one we want (i.e. at the
                    // start of the contents of the block element). We get round this by advancing the position returned to
                    // the last possible equivalent visible position.
                    if (unit == WORD) {
                        charIterator = createCharacterIterator(pos, false, null, characterOptions);
                        nextPos = charIterator.next();
                        charIterator.dispose();
                    }
                    if (nextPos) {
                        newPos = nextPos.previousVisible();
                    }
                }
            }


            return {
                position: newPos,
                unitsMoved: unitsMoved
            };
        }

        function createRangeCharacterIterator(session, range, characterOptions, backward) {
            var rangeStart = session.getRangeBoundaryPosition(range, true);
            var rangeEnd = session.getRangeBoundaryPosition(range, false);
            var itStart = backward ? rangeEnd : rangeStart;
            var itEnd = backward ? rangeStart : rangeEnd;

            return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
        }

        function getRangeCharacters(session, range, characterOptions) {

            var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
            while ( (pos = it.next()) ) {
                chars.push(pos);
            }

            it.dispose();
            return chars;
        }

        function isWholeWord(startPos, endPos, wordOptions) {
            var range = api.createRange(startPos.node);
            range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
            return !range.expand("word", { wordOptions: wordOptions });
        }

        function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
            var backward = isDirectionBackward(findOptions.direction);
            var it = createCharacterIterator(
                initialPos,
                backward,
                initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
                findOptions.characterOptions
            );
            var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
            var result, insideRegexMatch;
            var returnValue = null;

            function handleMatch(startIndex, endIndex) {
                var startPos = chars[startIndex].previousVisible();
                var endPos = chars[endIndex - 1];
                var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));

                return {
                    startPos: startPos,
                    endPos: endPos,
                    valid: valid
                };
            }

            while ( (pos = it.next()) ) {
                currentChar = pos.character;
                if (!isRegex && !findOptions.caseSensitive) {
                    currentChar = currentChar.toLowerCase();
                }

                if (backward) {
                    chars.unshift(pos);
                    text = currentChar + text;
                } else {
                    chars.push(pos);
                    text += currentChar;
                }

                if (isRegex) {
                    result = searchTerm.exec(text);
                    if (result) {
                        matchStartIndex = result.index;
                        matchEndIndex = matchStartIndex + result[0].length;
                        if (insideRegexMatch) {
                            // Check whether the match is now over
                            if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
                                returnValue = handleMatch(matchStartIndex, matchEndIndex);
                                break;
                            }
                        } else {
                            insideRegexMatch = true;
                        }
                    }
                } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
                    returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
                    break;
                }
            }

            // Check whether regex match extends to the end of the range
            if (insideRegexMatch) {
                returnValue = handleMatch(matchStartIndex, matchEndIndex);
            }
            it.dispose();

            return returnValue;
        }

        function createEntryPointFunction(func) {
            return function() {
                var sessionRunning = !!currentSession;
                var session = getSession();
                var args = [session].concat( util.toArray(arguments) );
                var returnValue = func.apply(this, args);
                if (!sessionRunning) {
                    endSession();
                }
                return returnValue;
            };
        }

        /*----------------------------------------------------------------------------------------------------------------*/

        // Extensions to the Rangy Range object

        function createRangeBoundaryMover(isStart, collapse) {
            /*
             Unit can be "character" or "word"
             Options:

             - includeTrailingSpace
             - wordRegex
             - tokenizer
             - collapseSpaceBeforeLineBreak
             */
            return createEntryPointFunction(
                function(session, unit, count, moveOptions) {
                    if (typeof count == UNDEF) {
                        count = unit;
                        unit = CHARACTER;
                    }
                    moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);

                    var boundaryIsStart = isStart;
                    if (collapse) {
                        boundaryIsStart = (count >= 0);
                        this.collapse(!boundaryIsStart);
                    }
                    var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
                    var newPos = moveResult.position;
                    this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
                    return moveResult.unitsMoved;
                }
            );
        }

        function createRangeTrimmer(isStart) {
            return createEntryPointFunction(
                function(session, characterOptions) {
                    characterOptions = createOptions(characterOptions, defaultCharacterOptions);
                    var pos;
                    var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
                    var trimCharCount = 0;
                    while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
                        ++trimCharCount;
                    }
                    it.dispose();
                    var trimmed = (trimCharCount > 0);
                    if (trimmed) {
                        this[isStart ? "moveStart" : "moveEnd"](
                            "character",
                            isStart ? trimCharCount : -trimCharCount,
                            { characterOptions: characterOptions }
                        );
                    }
                    return trimmed;
                }
            );
        }

        extend(api.rangePrototype, {
            moveStart: createRangeBoundaryMover(true, false),

            moveEnd: createRangeBoundaryMover(false, false),

            move: createRangeBoundaryMover(true, true),

            trimStart: createRangeTrimmer(true),

            trimEnd: createRangeTrimmer(false),

            trim: createEntryPointFunction(
                function(session, characterOptions) {
                    var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
                    return startTrimmed || endTrimmed;
                }
            ),

            expand: createEntryPointFunction(
                function(session, unit, expandOptions) {
                    var moved = false;
                    expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
                    var characterOptions = expandOptions.characterOptions;
                    if (!unit) {
                        unit = CHARACTER;
                    }
                    if (unit == WORD) {
                        var wordOptions = expandOptions.wordOptions;
                        var startPos = session.getRangeBoundaryPosition(this, true);
                        var endPos = session.getRangeBoundaryPosition(this, false);

                        var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
                        var startToken = startTokenizedTextProvider.nextEndToken();
                        var newStartPos = startToken.chars[0].previousVisible();
                        var endToken, newEndPos;

                        if (this.collapsed) {
                            endToken = startToken;
                        } else {
                            var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
                            endToken = endTokenizedTextProvider.previousStartToken();
                        }
                        newEndPos = endToken.chars[endToken.chars.length - 1];

                        if (!newStartPos.equals(startPos)) {
                            this.setStart(newStartPos.node, newStartPos.offset);
                            moved = true;
                        }
                        if (newEndPos && !newEndPos.equals(endPos)) {
                            this.setEnd(newEndPos.node, newEndPos.offset);
                            moved = true;
                        }

                        if (expandOptions.trim) {
                            if (expandOptions.trimStart) {
                                moved = this.trimStart(characterOptions) || moved;
                            }
                            if (expandOptions.trimEnd) {
                                moved = this.trimEnd(characterOptions) || moved;
                            }
                        }

                        return moved;
                    } else {
                        return this.moveEnd(CHARACTER, 1, expandOptions);
                    }
                }
            ),

            text: createEntryPointFunction(
                function(session, characterOptions) {
                    return this.collapsed ?
                        "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
                }
            ),

            selectCharacters: createEntryPointFunction(
                function(session, containerNode, startIndex, endIndex, characterOptions) {
                    var moveOptions = { characterOptions: characterOptions };
                    if (!containerNode) {
                        containerNode = getBody( this.getDocument() );
                    }
                    this.selectNodeContents(containerNode);
                    this.collapse(true);
                    this.moveStart("character", startIndex, moveOptions);
                    this.collapse(true);
                    this.moveEnd("character", endIndex - startIndex, moveOptions);
                }
            ),

            // Character indexes are relative to the start of node
            toCharacterRange: createEntryPointFunction(
                function(session, containerNode, characterOptions) {
                    if (!containerNode) {
                        containerNode = getBody( this.getDocument() );
                    }
                    var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
                    var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
                    var rangeBetween = this.cloneRange();
                    var startIndex, endIndex;
                    if (rangeStartsBeforeNode) {
                        rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
                        startIndex = -rangeBetween.text(characterOptions).length;
                    } else {
                        rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
                        startIndex = rangeBetween.text(characterOptions).length;
                    }
                    endIndex = startIndex + this.text(characterOptions).length;

                    return {
                        start: startIndex,
                        end: endIndex
                    };
                }
            ),

            findText: createEntryPointFunction(
                function(session, searchTermParam, findOptions) {
                    // Set up options
                    findOptions = createNestedOptions(findOptions, defaultFindOptions);

                    // Create word options if we're matching whole words only
                    if (findOptions.wholeWordsOnly) {
                        // We don't ever want trailing spaces for search results
                        findOptions.wordOptions.includeTrailingSpace = false;
                    }

                    var backward = isDirectionBackward(findOptions.direction);

                    // Create a range representing the search scope if none was provided
                    var searchScopeRange = findOptions.withinRange;
                    if (!searchScopeRange) {
                        searchScopeRange = api.createRange();
                        searchScopeRange.selectNodeContents(this.getDocument());
                    }

                    // Examine and prepare the search term
                    var searchTerm = searchTermParam, isRegex = false;
                    if (typeof searchTerm == "string") {
                        if (!findOptions.caseSensitive) {
                            searchTerm = searchTerm.toLowerCase();
                        }
                    } else {
                        isRegex = true;
                    }

                    var initialPos = session.getRangeBoundaryPosition(this, !backward);

                    // Adjust initial position if it lies outside the search scope
                    var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);

                    if (comparison === -1) {
                        initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
                    } else if (comparison === 1) {
                        initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
                    }

                    var pos = initialPos;
                    var wrappedAround = false;

                    // Try to find a match and ignore invalid ones
                    var findResult;
                    while (true) {
                        findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);

                        if (findResult) {
                            if (findResult.valid) {
                                this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
                                return true;
                            } else {
                                // We've found a match that is not a whole word, so we carry on searching from the point immediately
                                // after the match
                                pos = backward ? findResult.startPos : findResult.endPos;
                            }
                        } else if (findOptions.wrap && !wrappedAround) {
                            // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
                            searchScopeRange = searchScopeRange.cloneRange();
                            pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
                            searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
                            wrappedAround = true;
                        } else {
                            // Nothing found and we can't wrap around, so we're done
                            return false;
                        }
                    }
                }
            ),

            pasteHtml: function(html) {
                this.deleteContents();
                if (html) {
                    var frag = this.createContextualFragment(html);
                    var lastChild = frag.lastChild;
                    this.insertNode(frag);
                    this.collapseAfter(lastChild);
                }
            }
        });

        /*----------------------------------------------------------------------------------------------------------------*/

        // Extensions to the Rangy Selection object

        function createSelectionTrimmer(methodName) {
            return createEntryPointFunction(
                function(session, characterOptions) {
                    var trimmed = false;
                    this.changeEachRange(function(range) {
                        trimmed = range[methodName](characterOptions) || trimmed;
                    });
                    return trimmed;
                }
            );
        }

        extend(api.selectionPrototype, {
            expand: createEntryPointFunction(
                function(session, unit, expandOptions) {
                    this.changeEachRange(function(range) {
                        range.expand(unit, expandOptions);
                    });
                }
            ),

            move: createEntryPointFunction(
                function(session, unit, count, options) {
                    var unitsMoved = 0;
                    if (this.focusNode) {
                        this.collapse(this.focusNode, this.focusOffset);
                        var range = this.getRangeAt(0);
                        if (!options) {
                            options = {};
                        }
                        options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
                        unitsMoved = range.move(unit, count, options);
                        this.setSingleRange(range);
                    }
                    return unitsMoved;
                }
            ),

            trimStart: createSelectionTrimmer("trimStart"),
            trimEnd: createSelectionTrimmer("trimEnd"),
            trim: createSelectionTrimmer("trim"),

            selectCharacters: createEntryPointFunction(
                function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
                    var range = api.createRange(containerNode);
                    range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
                    this.setSingleRange(range, direction);
                }
            ),

            saveCharacterRanges: createEntryPointFunction(
                function(session, containerNode, characterOptions) {
                    var ranges = this.getAllRanges(), rangeCount = ranges.length;
                    var rangeInfos = [];

                    var backward = rangeCount == 1 && this.isBackward();

                    for (var i = 0, len = ranges.length; i < len; ++i) {
                        rangeInfos[i] = {
                            characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
                            backward: backward,
                            characterOptions: characterOptions
                        };
                    }

                    return rangeInfos;
                }
            ),

            restoreCharacterRanges: createEntryPointFunction(
                function(session, containerNode, saved) {
                    this.removeAllRanges();
                    for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
                        rangeInfo = saved[i];
                        characterRange = rangeInfo.characterRange;
                        range = api.createRange(containerNode);
                        range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
                        this.addRange(range, rangeInfo.backward);
                    }
                }
            ),

            text: createEntryPointFunction(
                function(session, characterOptions) {
                    var rangeTexts = [];
                    for (var i = 0, len = this.rangeCount; i < len; ++i) {
                        rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
                    }
                    return rangeTexts.join("");
                }
            )
        });

        /*----------------------------------------------------------------------------------------------------------------*/

        // Extensions to the core rangy object

        api.innerText = function(el, characterOptions) {
            var range = api.createRange(el);
            range.selectNodeContents(el);
            var text = range.text(characterOptions);
            return text;
        };

        api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
            var session = getSession();
            iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
            var startPos = session.getPosition(startNode, startOffset);
            var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
            var backward = isDirectionBackward(iteratorOptions.direction);

            return {
                next: function() {
                    return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
                },

                dispose: function() {
                    tokenizedTextProvider.dispose();
                    this.next = function() {};
                }
            };
        };

        /*----------------------------------------------------------------------------------------------------------------*/

        api.noMutation = function(func) {
            var session = getSession();
            func(session);
            endSession();
        };

        api.noMutation.createEntryPointFunction = createEntryPointFunction;

        api.textRange = {
            isBlockNode: isBlockNode,
            isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,

            createPosition: createEntryPointFunction(
                function(session, node, offset) {
                    return session.getPosition(node, offset);
                }
            )
        };
    });

    return rangy;
}, this);