Merge branch 'MDL-51586-master' of git://github.com/xow/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 12 Oct 2015 09:47:13 +0000 (10:47 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 12 Oct 2015 09:47:13 +0000 (10:47 +0100)
80 files changed:
admin/auth_config.php
files/renderer.php
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js
lib/editor/atto/plugins/rtl/yui/src/button/js/button.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js
lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js
lib/editor/atto/plugins/subscript/yui/src/button/js/button.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js
lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js
lib/editor/atto/plugins/superscript/yui/src/button/js/button.js
lib/editor/atto/readme_moodle.txt
lib/editor/atto/thirdpartylibs.xml
lib/editor/atto/upgrade.txt
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js
lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy.js
lib/editor/atto/yui/src/editor/js/styling.js
lib/editor/atto/yui/src/rangy/build.json
lib/editor/atto/yui/src/rangy/js/rangy-classapplier.js [new file with mode: 0644]
lib/editor/atto/yui/src/rangy/js/rangy-core.js
lib/editor/atto/yui/src/rangy/js/rangy-cssclassapplier.js [deleted file]
lib/editor/atto/yui/src/rangy/js/rangy-highlighter.js [new file with mode: 0644]
lib/editor/atto/yui/src/rangy/js/rangy-selectionsaverestore.js
lib/editor/atto/yui/src/rangy/js/rangy-serializer.js
lib/editor/atto/yui/src/rangy/js/rangy-textrange.js [new file with mode: 0644]
lib/html2text.php [deleted file]
lib/html2text/Html2Text.php [new file with mode: 0644]
lib/html2text/lib.php [new file with mode: 0644]
lib/html2text/readme_moodle.txt [new file with mode: 0644]
lib/html2text_readme.txt [deleted file]
lib/tests/fixtures/messageinbound/ios.test
lib/tests/fixtures/messageinbound/outlook.test
lib/tests/html2text_test.php
lib/tests/weblib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js
lib/yui/src/lockscroll/js/lockscroll.js
mod/forum/classes/output/email/renderer.php [new file with mode: 0644]
mod/forum/classes/output/email/renderer_textemail.php [new file with mode: 0644]
mod/forum/classes/output/emaildigestbasic/renderer.php [new file with mode: 0644]
mod/forum/classes/output/emaildigestbasic/renderer_textemail.php [new file with mode: 0644]
mod/forum/classes/output/emaildigestfull/renderer.php [new file with mode: 0644]
mod/forum/classes/output/emaildigestfull/renderer_textemail.php [new file with mode: 0644]
mod/forum/classes/output/forum_post.php [new file with mode: 0644]
mod/forum/classes/output/forum_post_email.php [new file with mode: 0644]
mod/forum/deprecatedlib.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/renderer.php
mod/forum/templates/forum_post_email_htmlemail.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_email_textemail.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_emaildigestbasic_htmlemail.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_emaildigestbasic_textemail.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_emaildigestfull_htmlemail.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_emaildigestfull_textemail.mustache [new file with mode: 0644]
mod/forum/tests/lib_test.php
mod/forum/upgrade.txt
mod/forum/version.php
repository/filepicker.php
theme/bootstrapbase/less/bootstrap/bootstrap.less
theme/bootstrapbase/less/bootstrap/dropdowns.less
theme/bootstrapbase/less/bootstrap/mixins.less
theme/bootstrapbase/less/bootstrap/navs.less
theme/bootstrapbase/less/bootstrap/responsive.less
theme/bootstrapbase/less/bootstrap/type.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/thirdpartylibs.xml

index e1a0fb0..0c955f4 100644 (file)
@@ -64,6 +64,8 @@ echo "<form id=\"authmenu\" method=\"post\" action=\"auth_config.php\">\n";
 echo "<div>\n";
 echo "<input type=\"hidden\" name=\"sesskey\" value=\"".sesskey()."\" />\n";
 echo "<input type=\"hidden\" name=\"auth\" value=\"".$auth."\" />\n";
+// HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+echo prevent_form_autofill_password();
 
 // auth plugin description
 echo $OUTPUT->box_start();
index e878fa6..7060a67 100644 (file)
@@ -1015,7 +1015,10 @@ class core_files_renderer extends plugin_renderer_base {
                     <label class="control-label"></label>
 
                     <div class="controls"><select></select></div>
-                </div>
+                </div>';
+        // HACK to prevent browsers from automatically inserting the user's password into the wrong fields.
+        $rv .= prevent_form_autofill_password();
+        $rv .= '
                 <div class="fp-login-input control-group clearfix">
                     <label class="control-label"></label>
                     <div class="controls"><input/></div>
index 21c7aa6..ba14c13 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js differ
index e13cd66..51d0be4 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js differ
index 21c7aa6..ba14c13 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js differ
index b7d96f0..2fb1543 100644 (file)
@@ -63,7 +63,12 @@ Y.namespace('M.atto_rtl').Button = Y.Base.create('button', Y.M.editor_atto.Edito
      */
     _toggleRTL: function(e, direction) {
         var host = this.get('host'),
-            selection = host.getSelection();
+            sourceSelection = rangy.saveSelection(),
+            selection = host.getSelection(),
+            newDirection = {
+                rtl: 'ltr',
+                ltr: 'rtl'
+            };
         if (selection) {
             // Format the selection to be sure it has a tag parent (not the contenteditable).
             var parentNode = host.formatSelectionBlock(),
@@ -71,11 +76,13 @@ Y.namespace('M.atto_rtl').Button = Y.Base.create('button', Y.M.editor_atto.Edito
 
             var currentDirection = parentDOMNode.getAttribute('dir');
             if (currentDirection === direction) {
-                parentDOMNode.removeAttribute("dir");
+                parentDOMNode.setAttribute("dir", newDirection[direction]);
             } else {
                 parentDOMNode.setAttribute("dir", direction);
             }
 
+            // Change selection from the containing paragraph to the original one.
+            rangy.restoreSelection(sourceSelection);
             // Mark the text as having been updated.
             this.markUpdated();
         }
index 43a8d5e..cf1939e 100644 (file)
Binary files a/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js and b/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-debug.js differ
index e92ce23..40cecf9 100644 (file)
Binary files a/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js and b/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button-min.js differ
index 43a8d5e..cf1939e 100644 (file)
Binary files a/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js and b/lib/editor/atto/plugins/subscript/yui/build/moodle-atto_subscript-button/moodle-atto_subscript-button.js differ
index 07dc361..880db8e 100644 (file)
@@ -60,8 +60,8 @@ Y.namespace('M.atto_subscript').Button = Y.Base.create('button', Y.M.editor_atto
             // Watch the following tags and add/remove highlighting as appropriate:
             tags: 'sub'
         });
-        this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
-        this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
+        this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
+        this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
     },
 
     /**
index 0ed8e7c..890f2d6 100644 (file)
Binary files a/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js and b/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-debug.js differ
index 399aeae..b28ba74 100644 (file)
Binary files a/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js and b/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button-min.js differ
index 0ed8e7c..890f2d6 100644 (file)
Binary files a/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js and b/lib/editor/atto/plugins/superscript/yui/build/moodle-atto_superscript-button/moodle-atto_superscript-button.js differ
index fce14a1..9e36f4a 100644 (file)
@@ -60,8 +60,8 @@ Y.namespace('M.atto_superscript').Button = Y.Base.create('button', Y.M.editor_at
             // Watch the following tags and add/remove highlighting as appropriate:
             tags: 'sup'
         });
-        this._subscriptApplier = window.rangy.createCssClassApplier("editor-subscript");
-        this._superscriptApplier = window.rangy.createCssClassApplier("editor-superscript");
+        this._subscriptApplier = window.rangy.createClassApplier("editor-subscript");
+        this._superscriptApplier = window.rangy.createClassApplier("editor-superscript");
     },
 
     /**
index 6867eb7..fc977de 100644 (file)
@@ -2,9 +2,19 @@ Description of the import of libraries associated with the Atto editor.
 
 1)  Rangy (version 1.2.3)
     * Download the latest stable release;
+    * Delete all files in yui/src/rangy/js
     * Copy the content of the 'currentrelease/uncompressed' folder into yui/src/rangy/js
+    * Patch out the AMD / module support from rangy (because we are loading it with YUI)
+      To do this - change the code start of each js file to look like (just delete the other lines):
+
+(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) {
+
     * Run shifter against yui/src/rangy
 
+
     Notes:
     * We have patched 1.2.3 with a backport fix from the next release of Rangy which addresses an incompatibility
       between Rangy and HTML5Shiv which is used in the bootstrapclean theme. See MDL-44798 for further information.
index 06b6014..36309cd 100644 (file)
@@ -4,7 +4,7 @@
     <location>yui/src/rangy/js/*.*</location>
     <name>Rangy</name>
     <license>MIT</license>
-    <version>1.2.3</version>
+    <version>1.3.0</version>
     <licenseversion></licenseversion>
   </library>
 </libraries>
index d5267f6..f52c1f5 100644 (file)
@@ -1,4 +1,7 @@
 This files describes API changes in the editor_atto code.
+=== 3.0 ===
+* Rangy updated to 1.3.0.
+  Rangy has deprecated the createCssClassApplier function in this version (it is now createClassApplier).
 
 === 2.9 ===
 
index f5d2fea..e089c1c 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index e89ccd1..f68b5f1 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index a51598d..1a8ea70 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 76a4b39..9ed2584 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-debug.js differ
index 1433c09..32fb0b3 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy-min.js differ
index 76a4b39..9ed2584 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy.js and b/lib/editor/atto/yui/build/moodle-editor_atto-rangy/moodle-editor_atto-rangy.js differ
index 4795cd8..6c31d81 100644 (file)
@@ -87,7 +87,7 @@ EditorStyling.prototype = {
      */
     toggleInlineSelectionClass: function(toggleclasses) {
         var classname = toggleclasses.join(" ");
-        var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
+        var cssApplier = rangy.createClassApplier(classname, {normalize: true});
 
         cssApplier.toggleSelection();
     },
@@ -102,7 +102,7 @@ EditorStyling.prototype = {
      */
     formatSelectionInlineStyle: function(styles) {
         var classname = this.PLACEHOLDER_CLASS;
-        var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
+        var cssApplier = rangy.createClassApplier(classname, {normalize: true});
 
         cssApplier.applyToSelection();
 
index 1f62748..bee5bc4 100644 (file)
@@ -4,9 +4,11 @@
     "moodle-editor_atto-rangy": {
       "prependfiles": [
         "rangy-core.js",
-        "rangy-cssclassapplier.js",
         "rangy-selectionsaverestore.js",
-        "rangy-serializer.js"
+        "rangy-serializer.js",
+        "rangy-classapplier.js",
+        "rangy-highlighter.js",
+        "rangy-textrange.js"
       ],
       "jsfiles": [
         "init.js"
diff --git a/lib/editor/atto/yui/src/rangy/js/rangy-classapplier.js b/lib/editor/atto/yui/src/rangy/js/rangy-classapplier.js
new file mode 100644 (file)
index 0000000..73d670f
--- /dev/null
@@ -0,0 +1,1095 @@
+/**\r
+ * Class Applier module for Rangy.\r
+ * Adds, removes and toggles classes on Ranges and Selections\r
+ *\r
+ * Part of Rangy, a cross-browser JavaScript range and selection library\r
+ * https://github.com/timdown/rangy\r
+ *\r
+ * Depends on Rangy core.\r
+ *\r
+ * Copyright 2015, Tim Down\r
+ * Licensed under the MIT license.\r
+ * Version: 1.3.0\r
+ * Build date: 10 May 2015\r
+ */\r
+(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("ClassApplier", ["WrappedSelection"], function(api, module) {
+        var dom = api.dom;
+        var DomPosition = dom.DomPosition;
+        var contains = dom.arrayContains;
+        var util = api.util;
+        var forEach = util.forEach;
+
+
+        var defaultTagName = "span";
+        var createElementNSSupported = util.isHostMethod(document, "createElementNS");
+
+        function each(obj, func) {
+            for (var i in obj) {
+                if (obj.hasOwnProperty(i)) {
+                    if (func(i, obj[i]) === false) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+
+        function trim(str) {
+            return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
+        }
+
+        function classNameContainsClass(fullClassName, className) {
+            return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
+        }
+
+        // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
+        function hasClass(el, className) {
+            if (typeof el.classList == "object") {
+                return el.classList.contains(className);
+            } else {
+                var classNameSupported = (typeof el.className == "string");
+                var elClass = classNameSupported ? el.className : el.getAttribute("class");
+                return classNameContainsClass(elClass, className);
+            }
+        }
+
+        function addClass(el, className) {
+            if (typeof el.classList == "object") {
+                el.classList.add(className);
+            } else {
+                var classNameSupported = (typeof el.className == "string");
+                var elClass = classNameSupported ? el.className : el.getAttribute("class");
+                if (elClass) {
+                    if (!classNameContainsClass(elClass, className)) {
+                        elClass += " " + className;
+                    }
+                } else {
+                    elClass = className;
+                }
+                if (classNameSupported) {
+                    el.className = elClass;
+                } else {
+                    el.setAttribute("class", elClass);
+                }
+            }
+        }
+
+        var removeClass = (function() {
+            function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
+                return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
+            }
+
+            return function(el, className) {
+                if (typeof el.classList == "object") {
+                    el.classList.remove(className);
+                } else {
+                    var classNameSupported = (typeof el.className == "string");
+                    var elClass = classNameSupported ? el.className : el.getAttribute("class");
+                    elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
+                    if (classNameSupported) {
+                        el.className = elClass;
+                    } else {
+                        el.setAttribute("class", elClass);
+                    }
+                }
+            };
+        })();
+
+        function getClass(el) {
+            var classNameSupported = (typeof el.className == "string");
+            return classNameSupported ? el.className : el.getAttribute("class");
+        }
+
+        function sortClassName(className) {
+            return className && className.split(/\s+/).sort().join(" ");
+        }
+
+        function getSortedClassName(el) {
+            return sortClassName( getClass(el) );
+        }
+
+        function haveSameClasses(el1, el2) {
+            return getSortedClassName(el1) == getSortedClassName(el2);
+        }
+
+        function hasAllClasses(el, className) {
+            var classes = className.split(/\s+/);
+            for (var i = 0, len = classes.length; i < len; ++i) {
+                if (!hasClass(el, trim(classes[i]))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        function canTextBeStyled(textNode) {
+            var parent = textNode.parentNode;
+            return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
+        }
+
+        function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
+            var posNode = position.node, posOffset = position.offset;
+            var newNode = posNode, newOffset = posOffset;
+
+            if (posNode == newParent && posOffset > newIndex) {
+                ++newOffset;
+            }
+
+            if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
+                newNode = newParent;
+                newOffset += newIndex - oldIndex;
+            }
+
+            if (posNode == oldParent && posOffset > oldIndex + 1) {
+                --newOffset;
+            }
+
+            position.node = newNode;
+            position.offset = newOffset;
+        }
+
+        function movePositionWhenRemovingNode(position, parentNode, index) {
+            if (position.node == parentNode && position.offset > index) {
+                --position.offset;
+            }
+        }
+
+        function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
+            // For convenience, allow newIndex to be -1 to mean "insert at the end".
+            if (newIndex == -1) {
+                newIndex = newParent.childNodes.length;
+            }
+
+            var oldParent = node.parentNode;
+            var oldIndex = dom.getNodeIndex(node);
+
+            forEach(positionsToPreserve, function(position) {
+                movePosition(position, oldParent, oldIndex, newParent, newIndex);
+            });
+
+            // Now actually move the node.
+            if (newParent.childNodes.length == newIndex) {
+                newParent.appendChild(node);
+            } else {
+                newParent.insertBefore(node, newParent.childNodes[newIndex]);
+            }
+        }
+
+        function removePreservingPositions(node, positionsToPreserve) {
+
+            var oldParent = node.parentNode;
+            var oldIndex = dom.getNodeIndex(node);
+
+            forEach(positionsToPreserve, function(position) {
+                movePositionWhenRemovingNode(position, oldParent, oldIndex);
+            });
+
+            dom.removeNode(node);
+        }
+
+        function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
+            var child, children = [];
+            while ( (child = node.firstChild) ) {
+                movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
+                children.push(child);
+            }
+            if (removeNode) {
+                removePreservingPositions(node, positionsToPreserve);
+            }
+            return children;
+        }
+
+        function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
+            return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
+        }
+
+        function rangeSelectsAnyText(range, textNode) {
+            var textNodeRange = range.cloneRange();
+            textNodeRange.selectNodeContents(textNode);
+
+            var intersectionRange = textNodeRange.intersection(range);
+            var text = intersectionRange ? intersectionRange.toString() : "";
+
+            return text != "";
+        }
+
+        function getEffectiveTextNodes(range) {
+            var nodes = range.getNodes([3]);
+
+            // Optimization as per issue 145
+
+            // Remove non-intersecting text nodes from the start of the range
+            var start = 0, node;
+            while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
+                ++start;
+            }
+
+            // Remove non-intersecting text nodes from the start of the range
+            var end = nodes.length - 1;
+            while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
+                --end;
+            }
+
+            return nodes.slice(start, end + 1);
+        }
+
+        function elementsHaveSameNonClassAttributes(el1, el2) {
+            if (el1.attributes.length != el2.attributes.length) return false;
+            for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
+                attr1 = el1.attributes[i];
+                name = attr1.name;
+                if (name != "class") {
+                    attr2 = el2.attributes.getNamedItem(name);
+                    if ( (attr1 === null) != (attr2 === null) ) return false;
+                    if (attr1.specified != attr2.specified) return false;
+                    if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
+                }
+            }
+            return true;
+        }
+
+        function elementHasNonClassAttributes(el, exceptions) {
+            for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
+                attrName = el.attributes[i].name;
+                if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        var getComputedStyleProperty = dom.getComputedStyleProperty;
+        var isEditableElement = (function() {
+            var testEl = document.createElement("div");
+            return typeof testEl.isContentEditable == "boolean" ?
+                function (node) {
+                    return node && node.nodeType == 1 && node.isContentEditable;
+                } :
+                function (node) {
+                    if (!node || node.nodeType != 1 || node.contentEditable == "false") {
+                        return false;
+                    }
+                    return node.contentEditable == "true" || isEditableElement(node.parentNode);
+                };
+        })();
+
+        function isEditingHost(node) {
+            var parent;
+            return node && node.nodeType == 1 &&
+                (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
+                (isEditableElement(node) && !isEditableElement(node.parentNode)));
+        }
+
+        function isEditable(node) {
+            return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
+        }
+
+        var inlineDisplayRegex = /^inline(-block|-table)?$/i;
+
+        function isNonInlineElement(node) {
+            return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
+        }
+
+        // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
+        var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
+
+        function isUnrenderedWhiteSpaceNode(node) {
+            if (node.data.length == 0) {
+                return true;
+            }
+            if (htmlNonWhiteSpaceRegex.test(node.data)) {
+                return false;
+            }
+            var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
+            switch (cssWhiteSpace) {
+                case "pre":
+                case "pre-wrap":
+                case "-moz-pre-wrap":
+                    return false;
+                case "pre-line":
+                    if (/[\r\n]/.test(node.data)) {
+                        return false;
+                    }
+            }
+
+            // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
+            // non-inline element, it will not be rendered. This seems to be a good enough definition.
+            return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
+        }
+
+        function getRangeBoundaries(ranges) {
+            var positions = [], i, range;
+            for (i = 0; range = ranges[i++]; ) {
+                positions.push(
+                    new DomPosition(range.startContainer, range.startOffset),
+                    new DomPosition(range.endContainer, range.endOffset)
+                );
+            }
+            return positions;
+        }
+
+        function updateRangesFromBoundaries(ranges, positions) {
+            for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
+                range = ranges[i];
+                start = positions[i * 2];
+                end = positions[i * 2 + 1];
+                range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
+            }
+        }
+
+        function isSplitPoint(node, offset) {
+            if (dom.isCharacterDataNode(node)) {
+                if (offset == 0) {
+                    return !!node.previousSibling;
+                } else if (offset == node.length) {
+                    return !!node.nextSibling;
+                } else {
+                    return true;
+                }
+            }
+
+            return offset > 0 && offset < node.childNodes.length;
+        }
+
+        function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
+            var newNode, parentNode;
+            var splitAtStart = (descendantOffset == 0);
+
+            if (dom.isAncestorOf(descendantNode, node)) {
+                return node;
+            }
+
+            if (dom.isCharacterDataNode(descendantNode)) {
+                var descendantIndex = dom.getNodeIndex(descendantNode);
+                if (descendantOffset == 0) {
+                    descendantOffset = descendantIndex;
+                } else if (descendantOffset == descendantNode.length) {
+                    descendantOffset = descendantIndex + 1;
+                } else {
+                    throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
+                        descendantOffset + " in " + descendantNode.data);
+                }
+                descendantNode = descendantNode.parentNode;
+            }
+
+            if (isSplitPoint(descendantNode, descendantOffset)) {
+                // descendantNode is now guaranteed not to be a text or other character node
+                newNode = descendantNode.cloneNode(false);
+                parentNode = descendantNode.parentNode;
+                if (newNode.id) {
+                    newNode.removeAttribute("id");
+                }
+                var child, newChildIndex = 0;
+
+                while ( (child = descendantNode.childNodes[descendantOffset]) ) {
+                    movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
+                }
+                movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
+                return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
+            } else if (node != descendantNode) {
+                newNode = descendantNode.parentNode;
+
+                // Work out a new split point in the parent node
+                var newNodeIndex = dom.getNodeIndex(descendantNode);
+
+                if (!splitAtStart) {
+                    newNodeIndex++;
+                }
+                return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
+            }
+            return node;
+        }
+
+        function areElementsMergeable(el1, el2) {
+            return el1.namespaceURI == el2.namespaceURI &&
+                el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
+                haveSameClasses(el1, el2) &&
+                elementsHaveSameNonClassAttributes(el1, el2) &&
+                getComputedStyleProperty(el1, "display") == "inline" &&
+                getComputedStyleProperty(el2, "display") == "inline";
+        }
+
+        function createAdjacentMergeableTextNodeGetter(forward) {
+            var siblingPropName = forward ? "nextSibling" : "previousSibling";
+
+            return function(textNode, checkParentElement) {
+                var el = textNode.parentNode;
+                var adjacentNode = textNode[siblingPropName];
+                if (adjacentNode) {
+                    // Can merge if the node's previous/next sibling is a text node
+                    if (adjacentNode && adjacentNode.nodeType == 3) {
+                        return adjacentNode;
+                    }
+                } else if (checkParentElement) {
+                    // Compare text node parent element with its sibling
+                    adjacentNode = el[siblingPropName];
+                    if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
+                        var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
+                        if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
+                            return adjacentNodeChild;
+                        }
+                    }
+                }
+                return null;
+            };
+        }
+
+        var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
+            getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
+
+    
+        function Merge(firstNode) {
+            this.isElementMerge = (firstNode.nodeType == 1);
+            this.textNodes = [];
+            var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
+            if (firstTextNode) {
+                this.textNodes[0] = firstTextNode;
+            }
+        }
+
+        Merge.prototype = {
+            doMerge: function(positionsToPreserve) {
+                var textNodes = this.textNodes;
+                var firstTextNode = textNodes[0];
+                if (textNodes.length > 1) {
+                    var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
+                    var textParts = [], combinedTextLength = 0, textNode, parent;
+                    forEach(textNodes, function(textNode, i) {
+                        parent = textNode.parentNode;
+                        if (i > 0) {
+                            parent.removeChild(textNode);
+                            if (!parent.hasChildNodes()) {
+                                dom.removeNode(parent);
+                            }
+                            if (positionsToPreserve) {
+                                forEach(positionsToPreserve, function(position) {
+                                    // Handle case where position is inside the text node being merged into a preceding node
+                                    if (position.node == textNode) {
+                                        position.node = firstTextNode;
+                                        position.offset += combinedTextLength;
+                                    }
+                                    // Handle case where both text nodes precede the position within the same parent node
+                                    if (position.node == parent && position.offset > firstTextNodeIndex) {
+                                        --position.offset;
+                                        if (position.offset == firstTextNodeIndex + 1 && i < len - 1) {
+                                            position.node = firstTextNode;
+                                            position.offset = combinedTextLength;
+                                        }
+                                    }
+                                });
+                            }
+                        }
+                        textParts[i] = textNode.data;
+                        combinedTextLength += textNode.data.length;
+                    });
+                    firstTextNode.data = textParts.join("");
+                }
+                return firstTextNode.data;
+            },
+
+            getLength: function() {
+                var i = this.textNodes.length, len = 0;
+                while (i--) {
+                    len += this.textNodes[i].length;
+                }
+                return len;
+            },
+
+            toString: function() {
+                var textParts = [];
+                forEach(this.textNodes, function(textNode, i) {
+                    textParts[i] = "'" + textNode.data + "'";
+                });
+                return "[Merge(" + textParts.join(",") + ")]";
+            }
+        };
+
+        var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
+            "removeEmptyElements", "onElementCreate"];
+
+        // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
+        var attrNamesForProperties = {};
+
+        function ClassApplier(className, options, tagNames) {
+            var normalize, i, len, propName, applier = this;
+            applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
+
+            var elementPropertiesFromOptions = null, elementAttributes = {};
+
+            // Initialize from options object
+            if (typeof options == "object" && options !== null) {
+                if (typeof options.elementTagName !== "undefined") {
+                    options.elementTagName = options.elementTagName.toLowerCase();
+                }
+                tagNames = options.tagNames;
+                elementPropertiesFromOptions = options.elementProperties;
+                elementAttributes = options.elementAttributes;
+
+                for (i = 0; propName = optionProperties[i++]; ) {
+                    if (options.hasOwnProperty(propName)) {
+                        applier[propName] = options[propName];
+                    }
+                }
+                normalize = options.normalize;
+            } else {
+                normalize = options;
+            }
+
+            // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
+            applier.normalize = (typeof normalize == "undefined") ? true : normalize;
+
+            // Initialize element properties and attribute exceptions
+            applier.attrExceptions = [];
+            var el = document.createElement(applier.elementTagName);
+            applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
+            each(elementAttributes, function(attrName, attrValue) {
+                applier.attrExceptions.push(attrName);
+                // Ensure each attribute value is a string
+                elementAttributes[attrName] = "" + attrValue;
+            });
+            applier.elementAttributes = elementAttributes;
+
+            applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
+                sortClassName(applier.elementProperties.className + " " + className) : className;
+
+            // Initialize tag names
+            applier.applyToAnyTagName = false;
+            var type = typeof tagNames;
+            if (type == "string") {
+                if (tagNames == "*") {
+                    applier.applyToAnyTagName = true;
+                } else {
+                    applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
+                }
+            } else if (type == "object" && typeof tagNames.length == "number") {
+                applier.tagNames = [];
+                for (i = 0, len = tagNames.length; i < len; ++i) {
+                    if (tagNames[i] == "*") {
+                        applier.applyToAnyTagName = true;
+                    } else {
+                        applier.tagNames.push(tagNames[i].toLowerCase());
+                    }
+                }
+            } else {
+                applier.tagNames = [applier.elementTagName];
+            }
+        }
+
+        ClassApplier.prototype = {
+            elementTagName: defaultTagName,
+            elementProperties: {},
+            elementAttributes: {},
+            ignoreWhiteSpace: true,
+            applyToEditableOnly: false,
+            useExistingElements: true,
+            removeEmptyElements: true,
+            onElementCreate: null,
+
+            copyPropertiesToElement: function(props, el, createCopy) {
+                var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
+
+                for (var p in props) {
+                    if (props.hasOwnProperty(p)) {
+                        propValue = props[p];
+                        elPropValue = el[p];
+
+                        // Special case for class. The copied properties object has the applier's class as well as its own
+                        // to simplify checks when removing styling elements
+                        if (p == "className") {
+                            addClass(el, propValue);
+                            addClass(el, this.className);
+                            el[p] = sortClassName(el[p]);
+                            if (createCopy) {
+                                elProps[p] = propValue;
+                            }
+                        }
+
+                        // Special case for style
+                        else if (p == "style") {
+                            elStyle = elPropValue;
+                            if (createCopy) {
+                                elProps[p] = elPropsStyle = {};
+                            }
+                            for (s in props[p]) {
+                                if (props[p].hasOwnProperty(s)) {
+                                    elStyle[s] = propValue[s];
+                                    if (createCopy) {
+                                        elPropsStyle[s] = elStyle[s];
+                                    }
+                                }
+                            }
+                            this.attrExceptions.push(p);
+                        } else {
+                            el[p] = propValue;
+                            // Copy the property back from the dummy element so that later comparisons to check whether
+                            // elements may be removed are checking against the right value. For example, the href property
+                            // of an element returns a fully qualified URL even if it was previously assigned a relative
+                            // URL.
+                            if (createCopy) {
+                                elProps[p] = el[p];
+
+                                // Not all properties map to identically-named attributes
+                                attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
+                                this.attrExceptions.push(attrName);
+                            }
+                        }
+                    }
+                }
+
+                return createCopy ? elProps : "";
+            },
+
+            copyAttributesToElement: function(attrs, el) {
+                for (var attrName in attrs) {
+                    if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
+                        el.setAttribute(attrName, attrs[attrName]);
+                    }
+                }
+            },
+
+            appliesToElement: function(el) {
+                return contains(this.tagNames, el.tagName.toLowerCase());
+            },
+
+            getEmptyElements: function(range) {
+                var applier = this;
+                return range.getNodes([1], function(el) {
+                    return applier.appliesToElement(el) && !el.hasChildNodes();
+                });
+            },
+
+            hasClass: function(node) {
+                return node.nodeType == 1 &&
+                    (this.applyToAnyTagName || this.appliesToElement(node)) &&
+                    hasClass(node, this.className);
+            },
+
+            getSelfOrAncestorWithClass: function(node) {
+                while (node) {
+                    if (this.hasClass(node)) {
+                        return node;
+                    }
+                    node = node.parentNode;
+                }
+                return null;
+            },
+
+            isModifiable: function(node) {
+                return !this.applyToEditableOnly || isEditable(node);
+            },
+
+            // White space adjacent to an unwrappable node can be ignored for wrapping
+            isIgnorableWhiteSpaceNode: function(node) {
+                return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
+            },
+
+            // Normalizes nodes after applying a class to a Range.
+            postApply: function(textNodes, range, positionsToPreserve, isUndo) {
+                var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
+
+                var merges = [], currentMerge;
+
+                var rangeStartNode = firstNode, rangeEndNode = lastNode;
+                var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
+
+                var textNode, precedingTextNode;
+
+                // Check for every required merge and create a Merge object for each
+                forEach(textNodes, function(textNode) {
+                    precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
+                    if (precedingTextNode) {
+                        if (!currentMerge) {
+                            currentMerge = new Merge(precedingTextNode);
+                            merges.push(currentMerge);
+                        }
+                        currentMerge.textNodes.push(textNode);
+                        if (textNode === firstNode) {
+                            rangeStartNode = currentMerge.textNodes[0];
+                            rangeStartOffset = rangeStartNode.length;
+                        }
+                        if (textNode === lastNode) {
+                            rangeEndNode = currentMerge.textNodes[0];
+                            rangeEndOffset = currentMerge.getLength();
+                        }
+                    } else {
+                        currentMerge = null;
+                    }
+                });
+
+                // Test whether the first node after the range needs merging
+                var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
+
+                if (nextTextNode) {
+                    if (!currentMerge) {
+                        currentMerge = new Merge(lastNode);
+                        merges.push(currentMerge);
+                    }
+                    currentMerge.textNodes.push(nextTextNode);
+                }
+
+                // Apply the merges
+                if (merges.length) {
+                    for (i = 0, len = merges.length; i < len; ++i) {
+                        merges[i].doMerge(positionsToPreserve);
+                    }
+
+                    // Set the range boundaries
+                    range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
+                }
+            },
+
+            createContainer: function(parentNode) {
+                var doc = dom.getDocument(parentNode);
+                var namespace;
+                var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
+                    doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
+                    doc.createElement(this.elementTagName);
+
+                this.copyPropertiesToElement(this.elementProperties, el, false);
+                this.copyAttributesToElement(this.elementAttributes, el);
+                addClass(el, this.className);
+                if (this.onElementCreate) {
+                    this.onElementCreate(el, this);
+                }
+                return el;
+            },
+
+            elementHasProperties: function(el, props) {
+                var applier = this;
+                return each(props, function(p, propValue) {
+                    if (p == "className") {
+                        // For checking whether we should reuse an existing element, we just want to check that the element
+                        // has all the classes specified in the className property. When deciding whether the element is
+                        // removable when unapplying a class, there is separate special handling to check whether the
+                        // element has extra classes so the same simple check will do.
+                        return hasAllClasses(el, propValue);
+                    } else if (typeof propValue == "object") {
+                        if (!applier.elementHasProperties(el[p], propValue)) {
+                            return false;
+                        }
+                    } else if (el[p] !== propValue) {
+                        return false;
+                    }
+                });
+            },
+
+            elementHasAttributes: function(el, attrs) {
+                return each(attrs, function(name, value) {
+                    if (el.getAttribute(name) !== value) {
+                        return false;
+                    }
+                });
+            },
+
+            applyToTextNode: function(textNode, positionsToPreserve) {
+
+                // Check whether the text node can be styled. Text within a <style> or <script> element, for example,
+                // should not be styled. See issue 283.
+                if (canTextBeStyled(textNode)) {
+                    var parent = textNode.parentNode;
+                    if (parent.childNodes.length == 1 &&
+                        this.useExistingElements &&
+                        this.appliesToElement(parent) &&
+                        this.elementHasProperties(parent, this.elementProperties) &&
+                        this.elementHasAttributes(parent, this.elementAttributes)) {
+
+                        addClass(parent, this.className);
+                    } else {
+                        var textNodeParent = textNode.parentNode;
+                        var el = this.createContainer(textNodeParent);
+                        textNodeParent.insertBefore(el, textNode);
+                        el.appendChild(textNode);
+                    }
+                }
+
+            },
+
+            isRemovable: function(el) {
+                return el.tagName.toLowerCase() == this.elementTagName &&
+                    getSortedClassName(el) == this.elementSortedClassName &&
+                    this.elementHasProperties(el, this.elementProperties) &&
+                    !elementHasNonClassAttributes(el, this.attrExceptions) &&
+                    this.elementHasAttributes(el, this.elementAttributes) &&
+                    this.isModifiable(el);
+            },
+
+            isEmptyContainer: function(el) {
+                var childNodeCount = el.childNodes.length;
+                return el.nodeType == 1 &&
+                    this.isRemovable(el) &&
+                    (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
+            },
+
+            removeEmptyContainers: function(range) {
+                var applier = this;
+                var nodesToRemove = range.getNodes([1], function(el) {
+                    return applier.isEmptyContainer(el);
+                });
+
+                var rangesToPreserve = [range];
+                var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
+
+                forEach(nodesToRemove, function(node) {
+                    removePreservingPositions(node, positionsToPreserve);
+                });
+
+                // Update the range from the preserved boundary positions
+                updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
+            },
+
+            undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
+                if (!range.containsNode(ancestorWithClass)) {
+                    // Split out the portion of the ancestor from which we can remove the class
+                    //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
+                    var ancestorRange = range.cloneRange();
+                    ancestorRange.selectNode(ancestorWithClass);
+                    if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
+                        splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
+                        range.setEndAfter(ancestorWithClass);
+                    }
+                    if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
+                        ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
+                    }
+                }
+
+                if (this.isRemovable(ancestorWithClass)) {
+                    replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
+                } else {
+                    removeClass(ancestorWithClass, this.className);
+                }
+            },
+
+            splitAncestorWithClass: function(container, offset, positionsToPreserve) {
+                var ancestorWithClass = this.getSelfOrAncestorWithClass(container);
+                if (ancestorWithClass) {
+                    splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve);
+                }
+            },
+
+            undoToAncestor: function(ancestorWithClass, positionsToPreserve) {
+                if (this.isRemovable(ancestorWithClass)) {
+                    replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
+                } else {
+                    removeClass(ancestorWithClass, this.className);
+                }
+            },
+
+            applyToRange: function(range, rangesToPreserve) {
+                var applier = this;
+                rangesToPreserve = rangesToPreserve || [];
+
+                // Create an array of range boundaries to preserve
+                var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
+
+                range.splitBoundariesPreservingPositions(positionsToPreserve);
+
+                // Tidy up the DOM by removing empty containers
+                if (applier.removeEmptyElements) {
+                    applier.removeEmptyContainers(range);
+                }
+
+                var textNodes = getEffectiveTextNodes(range);
+
+                if (textNodes.length) {
+                    forEach(textNodes, function(textNode) {
+                        if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) &&
+                                applier.isModifiable(textNode)) {
+                            applier.applyToTextNode(textNode, positionsToPreserve);
+                        }
+                    });
+                    var lastTextNode = textNodes[textNodes.length - 1];
+                    range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
+                    if (applier.normalize) {
+                        applier.postApply(textNodes, range, positionsToPreserve, false);
+                    }
+
+                    // Update the ranges from the preserved boundary positions
+                    updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
+                }
+
+                // Apply classes to any appropriate empty elements
+                var emptyElements = applier.getEmptyElements(range);
+
+                forEach(emptyElements, function(el) {
+                    addClass(el, applier.className);
+                });
+            },
+
+            applyToRanges: function(ranges) {
+
+                var i = ranges.length;
+                while (i--) {
+                    this.applyToRange(ranges[i], ranges);
+                }
+
+
+                return ranges;
+            },
+
+            applyToSelection: function(win) {
+                var sel = api.getSelection(win);
+                sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
+            },
+
+            undoToRange: function(range, rangesToPreserve) {
+                var applier = this;
+                // Create an array of range boundaries to preserve
+                rangesToPreserve = rangesToPreserve || [];
+                var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
+
+
+                range.splitBoundariesPreservingPositions(positionsToPreserve);
+
+                // Tidy up the DOM by removing empty containers
+                if (applier.removeEmptyElements) {
+                    applier.removeEmptyContainers(range, positionsToPreserve);
+                }
+
+                var textNodes = getEffectiveTextNodes(range);
+                var textNode, ancestorWithClass;
+                var lastTextNode = textNodes[textNodes.length - 1];
+
+                if (textNodes.length) {
+                    applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve);
+                    applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve);
+                    for (var i = 0, len = textNodes.length; i < len; ++i) {
+                        textNode = textNodes[i];
+                        ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode);
+                        if (ancestorWithClass && applier.isModifiable(textNode)) {
+                            applier.undoToAncestor(ancestorWithClass, positionsToPreserve);
+                        }
+                    }
+                    // Ensure the range is still valid
+                    range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
+
+
+                    if (applier.normalize) {
+                        applier.postApply(textNodes, range, positionsToPreserve, true);
+                    }
+
+                    // Update the ranges from the preserved boundary positions
+                    updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
+                }
+
+                // Remove class from any appropriate empty elements
+                var emptyElements = applier.getEmptyElements(range);
+
+                forEach(emptyElements, function(el) {
+                    removeClass(el, applier.className);
+                });
+            },
+
+            undoToRanges: function(ranges) {
+                // Get ranges returned in document order
+                var i = ranges.length;
+
+                while (i--) {
+                    this.undoToRange(ranges[i], ranges);
+                }
+
+                return ranges;
+            },
+
+            undoToSelection: function(win) {
+                var sel = api.getSelection(win);
+                var ranges = api.getSelection(win).getAllRanges();
+                this.undoToRanges(ranges);
+                sel.setRanges(ranges);
+            },
+
+            isAppliedToRange: function(range) {
+                if (range.collapsed || range.toString() == "") {
+                    return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
+                } else {
+                    var textNodes = range.getNodes( [3] );
+                    if (textNodes.length)
+                    for (var i = 0, textNode; textNode = textNodes[i++]; ) {
+                        if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) &&
+                                this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
+                            return false;
+                        }
+                    }
+                    return true;
+                }
+            },
+
+            isAppliedToRanges: function(ranges) {
+                var i = ranges.length;
+                if (i == 0) {
+                    return false;
+                }
+                while (i--) {
+                    if (!this.isAppliedToRange(ranges[i])) {
+                        return false;
+                    }
+                }
+                return true;
+            },
+
+            isAppliedToSelection: function(win) {
+                var sel = api.getSelection(win);
+                return this.isAppliedToRanges(sel.getAllRanges());
+            },
+
+            toggleRange: function(range) {
+                if (this.isAppliedToRange(range)) {
+                    this.undoToRange(range);
+                } else {
+                    this.applyToRange(range);
+                }
+            },
+
+            toggleSelection: function(win) {
+                if (this.isAppliedToSelection(win)) {
+                    this.undoToSelection(win);
+                } else {
+                    this.applyToSelection(win);
+                }
+            },
+
+            getElementsWithClassIntersectingRange: function(range) {
+                var elements = [];
+                var applier = this;
+                range.getNodes([3], function(textNode) {
+                    var el = applier.getSelfOrAncestorWithClass(textNode);
+                    if (el && !contains(elements, el)) {
+                        elements.push(el);
+                    }
+                });
+                return elements;
+            },
+
+            detach: function() {}
+        };
+
+        function createClassApplier(className, options, tagNames) {
+            return new ClassApplier(className, options, tagNames);
+        }
+
+        ClassApplier.util = {
+            hasClass: hasClass,
+            addClass: addClass,
+            removeClass: removeClass,
+            getClass: getClass,
+            hasSameClasses: haveSameClasses,
+            hasAllClasses: hasAllClasses,
+            replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
+            elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
+            elementHasNonClassAttributes: elementHasNonClassAttributes,
+            splitNodeAt: splitNodeAt,
+            isEditableElement: isEditableElement,
+            isEditingHost: isEditingHost,
+            isEditable: isEditable
+        };
+
+        api.CssClassApplier = api.ClassApplier = ClassApplier;
+        api.createClassApplier = createClassApplier;
+        util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module);
+    });
+    
+    return rangy;
+}, this);\r
index 865597f..b48aa53 100644 (file)
@@ -1,20 +1,26 @@
 /**\r
- * @license Rangy, a cross-browser JavaScript range and selection library\r
- * http://code.google.com/p/rangy/\r
+ * Rangy, a cross-browser JavaScript range and selection library\r
+ * https://github.com/timdown/rangy\r
  *\r
- * Copyright 2012, Tim Down\r
+ * Copyright 2015, Tim Down\r
  * Licensed under the MIT license.\r
- * Version: 1.2.3\r
- * Build date: 26 February 2012\r
+ * Version: 1.3.0\r
+ * Build date: 10 May 2015\r
  */\r
-window['rangy'] = (function() {\r
 \r
+(function(factory, root) {\r
+    // No AMD or CommonJS support so we place Rangy in (probably) the global variable\r
+    root.rangy = factory();\r
+})(function() {\r
 \r
     var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";\r
 \r
+    // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START\r
+    // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.\r
     var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",\r
-        "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];\r
+        "commonAncestorContainer"];\r
 \r
+    // Minimal set of methods required for DOM Level 2 Range compliance\r
     var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",\r
         "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",\r
         "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];\r
@@ -22,8 +28,8 @@ window['rangy'] = (function() {
     var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];\r
 \r
     // Subset of TextRange's full set of methods that we're interested in\r
-    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",\r
-        "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];\r
+    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",\r
+        "setEndPoint", "getBoundingClientRect"];\r
 \r
     /*----------------------------------------------------------------------------------------------------------------*/\r
 \r
@@ -64,67 +70,177 @@ window['rangy'] = (function() {
         return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);\r
     }\r
 \r
+    function getBody(doc) {\r
+        return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];\r
+    }\r
+\r
+    var forEach = [].forEach ?\r
+        function(arr, func) {\r
+            arr.forEach(func);\r
+        } :\r
+        function(arr, func) {\r
+            for (var i = 0, len = arr.length; i < len; ++i) {\r
+                func(arr[i], i);\r
+            }\r
+        };\r
+\r
+    var modules = {};\r
+\r
+    var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);\r
+\r
+    var util = {\r
+        isHostMethod: isHostMethod,\r
+        isHostObject: isHostObject,\r
+        isHostProperty: isHostProperty,\r
+        areHostMethods: areHostMethods,\r
+        areHostObjects: areHostObjects,\r
+        areHostProperties: areHostProperties,\r
+        isTextRange: isTextRange,\r
+        getBody: getBody,\r
+        forEach: forEach\r
+    };\r
+\r
     var api = {\r
-        version: "1.2.3",\r
+        version: "1.3.0",\r
         initialized: false,\r
+        isBrowser: isBrowser,\r
         supported: true,\r
-\r
-        util: {\r
-            isHostMethod: isHostMethod,\r
-            isHostObject: isHostObject,\r
-            isHostProperty: isHostProperty,\r
-            areHostMethods: areHostMethods,\r
-            areHostObjects: areHostObjects,\r
-            areHostProperties: areHostProperties,\r
-            isTextRange: isTextRange\r
-        },\r
-\r
+        util: util,\r
         features: {},\r
-\r
-        modules: {},\r
+        modules: modules,\r
         config: {\r
+            alertOnFail: false,\r
             alertOnWarn: false,\r
-            preferTextRange: false\r
+            preferTextRange: false,\r
+            autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize\r
         }\r
     };\r
 \r
+    function consoleLog(msg) {\r
+        if (typeof console != UNDEFINED && isHostMethod(console, "log")) {\r
+            console.log(msg);\r
+        }\r
+    }\r
+\r
+    function alertOrLog(msg, shouldAlert) {\r
+        if (isBrowser && shouldAlert) {\r
+            alert(msg);\r
+        } else  {\r
+            consoleLog(msg);\r
+        }\r
+    }\r
+\r
     function fail(reason) {\r
-        window.alert("Rangy not supported in your browser. Reason: " + reason);\r
         api.initialized = true;\r
         api.supported = false;\r
+        alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);\r
     }\r
 \r
     api.fail = fail;\r
 \r
     function warn(msg) {\r
-        var warningMessage = "Rangy warning: " + msg;\r
-        if (api.config.alertOnWarn) {\r
-            window.alert(warningMessage);\r
-        } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {\r
-            window.console.log(warningMessage);\r
-        }\r
+        alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);\r
     }\r
 \r
     api.warn = warn;\r
 \r
+    // Add utility extend() method\r
+    var extend;\r
     if ({}.hasOwnProperty) {\r
-        api.util.extend = function(o, props) {\r
+        util.extend = extend = function(obj, props, deep) {\r
+            var o, p;\r
             for (var i in props) {\r
                 if (props.hasOwnProperty(i)) {\r
-                    o[i] = props[i];\r
+                    o = obj[i];\r
+                    p = props[i];\r
+                    if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {\r
+                        extend(o, p, true);\r
+                    }\r
+                    obj[i] = p;\r
                 }\r
             }\r
+            // Special case for toString, which does not show up in for...in loops in IE <= 8\r
+            if (props.hasOwnProperty("toString")) {\r
+                obj.toString = props.toString;\r
+            }\r
+            return obj;\r
+        };\r
+\r
+        util.createOptions = function(optionsParam, defaults) {\r
+            var options = {};\r
+            extend(options, defaults);\r
+            if (optionsParam) {\r
+                extend(options, optionsParam);\r
+            }\r
+            return options;\r
         };\r
     } else {\r
         fail("hasOwnProperty not supported");\r
     }\r
 \r
+    // Test whether we're in a browser and bail out if not\r
+    if (!isBrowser) {\r
+        fail("Rangy can only run in a browser");\r
+    }\r
+\r
+    // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not\r
+    (function() {\r
+        var toArray;\r
+\r
+        if (isBrowser) {\r
+            var el = document.createElement("div");\r
+            el.appendChild(document.createElement("span"));\r
+            var slice = [].slice;\r
+            try {\r
+                if (slice.call(el.childNodes, 0)[0].nodeType == 1) {\r
+                    toArray = function(arrayLike) {\r
+                        return slice.call(arrayLike, 0);\r
+                    };\r
+                }\r
+            } catch (e) {}\r
+        }\r
+\r
+        if (!toArray) {\r
+            toArray = function(arrayLike) {\r
+                var arr = [];\r
+                for (var i = 0, len = arrayLike.length; i < len; ++i) {\r
+                    arr[i] = arrayLike[i];\r
+                }\r
+                return arr;\r
+            };\r
+        }\r
+\r
+        util.toArray = toArray;\r
+    })();\r
+\r
+    // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or\r
+    // normalization of event properties\r
+    var addListener;\r
+    if (isBrowser) {\r
+        if (isHostMethod(document, "addEventListener")) {\r
+            addListener = function(obj, eventType, listener) {\r
+                obj.addEventListener(eventType, listener, false);\r
+            };\r
+        } else if (isHostMethod(document, "attachEvent")) {\r
+            addListener = function(obj, eventType, listener) {\r
+                obj.attachEvent("on" + eventType, listener);\r
+            };\r
+        } else {\r
+            fail("Document does not have required addEventListener or attachEvent method");\r
+        }\r
+\r
+        util.addListener = addListener;\r
+    }\r
+\r
     var initListeners = [];\r
-    var moduleInitializers = [];\r
+\r
+    function getErrorDesc(ex) {\r
+        return ex.message || ex.description || String(ex);\r
+    }\r
 \r
     // Initialization\r
     function init() {\r
-        if (api.initialized) {\r
+        if (!isBrowser || api.initialized) {\r
             return;\r
         }\r
         var testRange;\r
@@ -137,10 +253,13 @@ window['rangy'] = (function() {
             if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {\r
                 implementsDomRange = true;\r
             }\r
-            testRange.detach();\r
         }\r
 \r
-        var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];\r
+        var body = getBody(document);\r
+        if (!body || body.nodeName.toLowerCase() != "body") {\r
+            fail("No body element found");\r
+            return;\r
+        }\r
 \r
         if (body && isHostMethod(body, "createTextRange")) {\r
             testRange = body.createTextRange();\r
@@ -150,7 +269,8 @@ window['rangy'] = (function() {
         }\r
 \r
         if (!implementsDomRange && !implementsTextRange) {\r
-            fail("Neither Range nor TextRange are implemented");\r
+            fail("Neither Range nor TextRange are available");\r
+            return;\r
         }\r
 \r
         api.initialized = true;\r
@@ -159,20 +279,43 @@ window['rangy'] = (function() {
             implementsTextRange: implementsTextRange\r
         };\r
 \r
-        // Initialize modules and call init listeners\r
-        var allListeners = moduleInitializers.concat(initListeners);\r
-        for (var i = 0, len = allListeners.length; i < len; ++i) {\r
+        // Initialize modules\r
+        var module, errorMessage;\r
+        for (var moduleName in modules) {\r
+            if ( (module = modules[moduleName]) instanceof Module ) {\r
+                module.init(module, api);\r
+            }\r
+        }\r
+\r
+        // Call init listeners\r
+        for (var i = 0, len = initListeners.length; i < len; ++i) {\r
             try {\r
-                allListeners[i](api);\r
+                initListeners[i](api);\r
             } catch (ex) {\r
-                if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {\r
-                    window.console.log("Init listener threw an exception. Continuing.", ex);\r
-                }\r
-\r
+                errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);\r
+                consoleLog(errorMessage);\r
             }\r
         }\r
     }\r
 \r
+    function deprecationNotice(deprecated, replacement, module) {\r
+        if (module) {\r
+            deprecated += " in module " + module.name;\r
+        }\r
+        api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +\r
+        replacement + " instead.");\r
+    }\r
+\r
+    function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {\r
+        owner[deprecated] = function() {\r
+            deprecationNotice(deprecated, replacement, module);\r
+            return owner[replacement].apply(owner, util.toArray(arguments));\r
+        };\r
+    }\r
+\r
+    util.deprecationNotice = deprecationNotice;\r
+    util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;\r
+\r
     // Allow external scripts to initialize this library in case it's loaded after the document has loaded\r
     api.init = init;\r
 \r
@@ -185,3045 +328,3510 @@ window['rangy'] = (function() {
         }\r
     };\r
 \r
-    var createMissingNativeApiListeners = [];\r
+    var shimListeners = [];\r
 \r
-    api.addCreateMissingNativeApiListener = function(listener) {\r
-        createMissingNativeApiListeners.push(listener);\r
+    api.addShimListener = function(listener) {\r
+        shimListeners.push(listener);\r
     };\r
 \r
-    function createMissingNativeApi(win) {\r
+    function shim(win) {\r
         win = win || window;\r
         init();\r
 \r
         // Notify listeners\r
-        for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {\r
-            createMissingNativeApiListeners[i](win);\r
+        for (var i = 0, len = shimListeners.length; i < len; ++i) {\r
+            shimListeners[i](win);\r
         }\r
     }\r
 \r
-    api.createMissingNativeApi = createMissingNativeApi;\r
+    if (isBrowser) {\r
+        api.shim = api.createMissingNativeApi = shim;\r
+        createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");\r
+    }\r
 \r
-    /**\r
-     * @constructor\r
-     */\r
-    function Module(name) {\r
+    function Module(name, dependencies, initializer) {\r
         this.name = name;\r
+        this.dependencies = dependencies;\r
         this.initialized = false;\r
         this.supported = false;\r
+        this.initializer = initializer;\r
     }\r
 \r
-    Module.prototype.fail = function(reason) {\r
-        this.initialized = true;\r
-        this.supported = false;\r
-\r
-        throw new Error("Module '" + this.name + "' failed to load: " + reason);\r
-    };\r
-\r
-    Module.prototype.warn = function(msg) {\r
-        api.warn("Module " + this.name + ": " + msg);\r
-    };\r
-\r
-    Module.prototype.createError = function(msg) {\r
-        return new Error("Error in Rangy " + this.name + " module: " + msg);\r
-    };\r
-\r
-    api.createModule = function(name, initFunc) {\r
-        var module = new Module(name);\r
-        api.modules[name] = module;\r
-\r
-        moduleInitializers.push(function(api) {\r
-            initFunc(api, module);\r
-            module.initialized = true;\r
-            module.supported = true;\r
-        });\r
-    };\r
-\r
-    api.requireModules = function(modules) {\r
-        for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {\r
-            moduleName = modules[i];\r
-            module = api.modules[moduleName];\r
-            if (!module || !(module instanceof Module)) {\r
-                throw new Error("Module '" + moduleName + "' not found");\r
-            }\r
-            if (!module.supported) {\r
-                throw new Error("Module '" + moduleName + "' not supported");\r
-            }\r
-        }\r
-    };\r
-\r
-    /*----------------------------------------------------------------------------------------------------------------*/\r
-\r
-    // Wait for document to load before running tests\r
-\r
-    var docReady = false;\r
-\r
-    var loadHandler = function(e) {\r
-\r
-        if (!docReady) {\r
-            docReady = true;\r
-            if (!api.initialized) {\r
-                init();\r
-            }\r
-        }\r
-    };\r
-\r
-    // Test whether we have window and document objects that we will need\r
-    if (typeof window == UNDEFINED) {\r
-        fail("No window found");\r
-        return;\r
-    }\r
-    if (typeof document == UNDEFINED) {\r
-        fail("No document found");\r
-        return;\r
-    }\r
-\r
-    if (isHostMethod(document, "addEventListener")) {\r
-        document.addEventListener("DOMContentLoaded", loadHandler, false);\r
-    }\r
-\r
-    // Add a fallback in case the DOMContentLoaded event isn't supported\r
-    if (isHostMethod(window, "addEventListener")) {\r
-        window.addEventListener("load", loadHandler, false);\r
-    } else if (isHostMethod(window, "attachEvent")) {\r
-        window.attachEvent("onload", loadHandler);\r
-    } else {\r
-        fail("Window does not have required addEventListener or attachEvent method");\r
-    }\r
-\r
-    return api;\r
-})();\r
-rangy.createModule("DomUtil", function(api, module) {\r
-\r
-    var UNDEF = "undefined";\r
-    var util = api.util;\r
-\r
-    // Perform feature tests\r
-    if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {\r
-        module.fail("document missing a Node creation method");\r
-    }\r
-\r
-    if (!util.isHostMethod(document, "getElementsByTagName")) {\r
-        module.fail("document missing getElementsByTagName method");\r
-    }\r
-\r
-    var el = document.createElement("div");\r
-    if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||\r
-            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {\r
-        module.fail("Incomplete Element implementation");\r
-    }\r
-\r
-    // innerHTML is required for Range's createContextualFragment method\r
-    if (!util.isHostProperty(el, "innerHTML")) {\r
-        module.fail("Element is missing innerHTML property");\r
-    }\r
-\r
-    var textNode = document.createTextNode("test");\r
-    if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||\r
-            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||\r
-            !util.areHostProperties(textNode, ["data"]))) {\r
-        module.fail("Incomplete Text Node implementation");\r
-    }\r
-\r
-    /*----------------------------------------------------------------------------------------------------------------*/\r
-\r
-    // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been\r
-    // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that\r
-    // contains just the document as a single element and the value searched for is the document.\r
-    var arrayContains = /*Array.prototype.indexOf ?\r
-        function(arr, val) {\r
-            return arr.indexOf(val) > -1;\r
-        }:*/\r
+    Module.prototype = {\r
+        init: function() {\r
+            var requiredModuleNames = this.dependencies || [];\r
+            for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {\r
+                moduleName = requiredModuleNames[i];\r
 \r
-        function(arr, val) {\r
-            var i = arr.length;\r
-            while (i--) {\r
-                if (arr[i] === val) {\r
-                    return true;\r
+                requiredModule = modules[moduleName];\r
+                if (!requiredModule || !(requiredModule instanceof Module)) {\r
+                    throw new Error("required module '" + moduleName + "' not found");\r
                 }\r
-            }\r
-            return false;\r
-        };\r
-\r
-    // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI\r
-    function isHtmlNamespace(node) {\r
-        var ns;\r
-        return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");\r
-    }\r
-\r
-    function parentElement(node) {\r
-        var parent = node.parentNode;\r
-        return (parent.nodeType == 1) ? parent : null;\r
-    }\r
-\r
-    function getNodeIndex(node) {\r
-        var i = 0;\r
-        while( (node = node.previousSibling) ) {\r
-            i++;\r
-        }\r
-        return i;\r
-    }\r
-\r
-    function getNodeLength(node) {\r
-        var childNodes;\r
-        return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);\r
-    }\r
-\r
-    function getCommonAncestor(node1, node2) {\r
-        var ancestors = [], n;\r
-        for (n = node1; n; n = n.parentNode) {\r
-            ancestors.push(n);\r
-        }\r
-\r
-        for (n = node2; n; n = n.parentNode) {\r
-            if (arrayContains(ancestors, n)) {\r
-                return n;\r
-            }\r
-        }\r
-\r
-        return null;\r
-    }\r
 \r
-    function isAncestorOf(ancestor, descendant, selfIsAncestor) {\r
-        var n = selfIsAncestor ? descendant : descendant.parentNode;\r
-        while (n) {\r
-            if (n === ancestor) {\r
-                return true;\r
-            } else {\r
-                n = n.parentNode;\r
-            }\r
-        }\r
-        return false;\r
-    }\r
+                requiredModule.init();\r
 \r
-    function getClosestAncestorIn(node, ancestor, selfIsAncestor) {\r
-        var p, n = selfIsAncestor ? node : node.parentNode;\r
-        while (n) {\r
-            p = n.parentNode;\r
-            if (p === ancestor) {\r
-                return n;\r
+                if (!requiredModule.supported) {\r
+                    throw new Error("required module '" + moduleName + "' not supported");\r
+                }\r
             }\r
-            n = p;\r
-        }\r
-        return null;\r
-    }\r
-\r
-    function isCharacterDataNode(node) {\r
-        var t = node.nodeType;\r
-        return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment\r
-    }\r
-\r
-    function insertAfter(node, precedingNode) {\r
-        var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;\r
-        if (nextNode) {\r
-            parent.insertBefore(node, nextNode);\r
-        } else {\r
-            parent.appendChild(node);\r
-        }\r
-        return node;\r
-    }\r
-\r
-    // Note that we cannot use splitText() because it is bugridden in IE 9.\r
-    function splitDataNode(node, index) {\r
-        var newNode = node.cloneNode(false);\r
-        newNode.deleteData(0, index);\r
-        node.deleteData(index, node.length - index);\r
-        insertAfter(newNode, node);\r
-        return newNode;\r
-    }\r
-\r
-    function getDocument(node) {\r
-        if (node.nodeType == 9) {\r
-            return node;\r
-        } else if (typeof node.ownerDocument != UNDEF) {\r
-            return node.ownerDocument;\r
-        } else if (typeof node.document != UNDEF) {\r
-            return node.document;\r
-        } else if (node.parentNode) {\r
-            return getDocument(node.parentNode);\r
-        } else {\r
-            throw new Error("getDocument: no document found for node");\r
-        }\r
-    }\r
 \r
-    function getWindow(node) {\r
-        var doc = getDocument(node);\r
-        if (typeof doc.defaultView != UNDEF) {\r
-            return doc.defaultView;\r
-        } else if (typeof doc.parentWindow != UNDEF) {\r
-            return doc.parentWindow;\r
-        } else {\r
-            throw new Error("Cannot get a window object for node");\r
-        }\r
-    }\r
+            // Now run initializer\r
+            this.initializer(this);\r
+        },\r
 \r
-    function getIframeDocument(iframeEl) {\r
-        if (typeof iframeEl.contentDocument != UNDEF) {\r
-            return iframeEl.contentDocument;\r
-        } else if (typeof iframeEl.contentWindow != UNDEF) {\r
-            return iframeEl.contentWindow.document;\r
-        } else {\r
-            throw new Error("getIframeWindow: No Document object found for iframe element");\r
-        }\r
-    }\r
+        fail: function(reason) {\r
+            this.initialized = true;\r
+            this.supported = false;\r
+            throw new Error(reason);\r
+        },\r
 \r
-    function getIframeWindow(iframeEl) {\r
-        if (typeof iframeEl.contentWindow != UNDEF) {\r
-            return iframeEl.contentWindow;\r
-        } else if (typeof iframeEl.contentDocument != UNDEF) {\r
-            return iframeEl.contentDocument.defaultView;\r
-        } else {\r
-            throw new Error("getIframeWindow: No Window object found for iframe element");\r
-        }\r
-    }\r
+        warn: function(msg) {\r
+            api.warn("Module " + this.name + ": " + msg);\r
+        },\r
 \r
-    function getBody(doc) {\r
-        return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];\r
-    }\r
+        deprecationNotice: function(deprecated, replacement) {\r
+            api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +\r
+                replacement + " instead");\r
+        },\r
 \r
-    function getRootContainer(node) {\r
-        var parent;\r
-        while ( (parent = node.parentNode) ) {\r
-            node = parent;\r
+        createError: function(msg) {\r
+            return new Error("Error in Rangy " + this.name + " module: " + msg);\r
         }\r
-        return node;\r
-    }\r
-\r
-    function comparePoints(nodeA, offsetA, nodeB, offsetB) {\r
-        // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing\r
-        var nodeC, root, childA, childB, n;\r
-        if (nodeA == nodeB) {\r
-\r
-            // Case 1: nodes are the same\r
-            return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;\r
-        } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {\r
-\r
-            // Case 2: node C (container B or an ancestor) is a child node of A\r
-            return offsetA <= getNodeIndex(nodeC) ? -1 : 1;\r
-        } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {\r
-\r
-            // Case 3: node C (container A or an ancestor) is a child node of B\r
-            return getNodeIndex(nodeC) < offsetB  ? -1 : 1;\r
-        } else {\r
+    };\r
 \r
-            // Case 4: containers are siblings or descendants of siblings\r
-            root = getCommonAncestor(nodeA, nodeB);\r
-            childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);\r
-            childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);\r
-\r
-            if (childA === childB) {\r
-                // This shouldn't be possible\r
-\r
-                throw new Error("comparePoints got to case 4 and childA and childB are the same!");\r
-            } else {\r
-                n = root.firstChild;\r
-                while (n) {\r
-                    if (n === childA) {\r
-                        return -1;\r
-                    } else if (n === childB) {\r
-                        return 1;\r
+    function createModule(name, dependencies, initFunc) {\r
+        var newModule = new Module(name, dependencies, function(module) {\r
+            if (!module.initialized) {\r
+                module.initialized = true;\r
+                try {\r
+                    initFunc(api, module);\r
+                    module.supported = true;\r
+                } catch (ex) {\r
+                    var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);\r
+                    consoleLog(errorMessage);\r
+                    if (ex.stack) {\r
+                        consoleLog(ex.stack);\r
                     }\r
-                    n = n.nextSibling;\r
                 }\r
-                throw new Error("Should not be here!");\r
             }\r
-        }\r
-    }\r
-\r
-    function fragmentFromNodeChildren(node) {\r
-        var fragment = getDocument(node).createDocumentFragment(), child;\r
-        while ( (child = node.firstChild) ) {\r
-            fragment.appendChild(child);\r
-        }\r
-        return fragment;\r
+        });\r
+        modules[name] = newModule;\r
+        return newModule;\r
     }\r
 \r
-    function inspectNode(node) {\r
-        if (!node) {\r
-            return "[No node]";\r
-        }\r
-        if (isCharacterDataNode(node)) {\r
-            return '"' + node.data + '"';\r
-        } else if (node.nodeType == 1) {\r
-            var idAttr = node.id ? ' id="' + node.id + '"' : "";\r
-            return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";\r
+    api.createModule = function(name) {\r
+        // Allow 2 or 3 arguments (second argument is an optional array of dependencies)\r
+        var initFunc, dependencies;\r
+        if (arguments.length == 2) {\r
+            initFunc = arguments[1];\r
+            dependencies = [];\r
         } else {\r
-            return node.nodeName;\r
+            initFunc = arguments[2];\r
+            dependencies = arguments[1];\r
         }\r
-    }\r
-\r
-    /**\r
-     * @constructor\r
-     */\r
-    function NodeIterator(root) {\r
-        this.root = root;\r
-        this._next = root;\r
-    }\r
 \r
-    NodeIterator.prototype = {\r
-        _current: null,\r
-\r
-        hasNext: function() {\r
-            return !!this._next;\r
-        },\r
-\r
-        next: function() {\r
-            var n = this._current = this._next;\r
-            var child, next;\r
-            if (this._current) {\r
-                child = n.firstChild;\r
-                if (child) {\r
-                    this._next = child;\r
-                } else {\r
-                    next = null;\r
-                    while ((n !== this.root) && !(next = n.nextSibling)) {\r
-                        n = n.parentNode;\r
-                    }\r
-                    this._next = next;\r
-                }\r
-            }\r
-            return this._current;\r
-        },\r
+        var module = createModule(name, dependencies, initFunc);\r
 \r
-        detach: function() {\r
-            this._current = this._next = this.root = null;\r
+        // Initialize the module immediately if the core is already initialized\r
+        if (api.initialized && api.supported) {\r
+            module.init();\r
         }\r
     };\r
 \r
-    function createIterator(root) {\r
-        return new NodeIterator(root);\r
-    }\r
-\r
-    /**\r
-     * @constructor\r
-     */\r
-    function DomPosition(node, offset) {\r
-        this.node = node;\r
-        this.offset = offset;\r
-    }\r
-\r
-    DomPosition.prototype = {\r
-        equals: function(pos) {\r
-            return this.node === pos.node & this.offset == pos.offset;\r
-        },\r
-\r
-        inspect: function() {\r
-            return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";\r
-        }\r
+    api.createCoreModule = function(name, dependencies, initFunc) {\r
+        createModule(name, dependencies, initFunc);\r
     };\r
 \r
-    /**\r
-     * @constructor\r
-     */\r
-    function DOMException(codeName) {\r
-        this.code = this[codeName];\r
-        this.codeName = codeName;\r
-        this.message = "DOMException: " + this.codeName;\r
-    }\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
 \r
-    DOMException.prototype = {\r
-        INDEX_SIZE_ERR: 1,\r
-        HIERARCHY_REQUEST_ERR: 3,\r
-        WRONG_DOCUMENT_ERR: 4,\r
-        NO_MODIFICATION_ALLOWED_ERR: 7,\r
-        NOT_FOUND_ERR: 8,\r
-        NOT_SUPPORTED_ERR: 9,\r
-        INVALID_STATE_ERR: 11\r
-    };\r
+    // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately\r
 \r
-    DOMException.prototype.toString = function() {\r
-        return this.message;\r
-    };\r
+    function RangePrototype() {}\r
+    api.RangePrototype = RangePrototype;\r
+    api.rangePrototype = new RangePrototype();\r
 \r
-    api.dom = {\r
-        arrayContains: arrayContains,\r
-        isHtmlNamespace: isHtmlNamespace,\r
-        parentElement: parentElement,\r
-        getNodeIndex: getNodeIndex,\r
-        getNodeLength: getNodeLength,\r
-        getCommonAncestor: getCommonAncestor,\r
-        isAncestorOf: isAncestorOf,\r
-        getClosestAncestorIn: getClosestAncestorIn,\r
-        isCharacterDataNode: isCharacterDataNode,\r
-        insertAfter: insertAfter,\r
-        splitDataNode: splitDataNode,\r
-        getDocument: getDocument,\r
-        getWindow: getWindow,\r
-        getIframeWindow: getIframeWindow,\r
-        getIframeDocument: getIframeDocument,\r
-        getBody: getBody,\r
-        getRootContainer: getRootContainer,\r
-        comparePoints: comparePoints,\r
-        inspectNode: inspectNode,\r
-        fragmentFromNodeChildren: fragmentFromNodeChildren,\r
-        createIterator: createIterator,\r
-        DomPosition: DomPosition\r
-    };\r
+    function SelectionPrototype() {}\r
+    api.selectionPrototype = new SelectionPrototype();\r
 \r
-    api.DOMException = DOMException;\r
-});rangy.createModule("DomRange", function(api, module) {
-    api.requireModules( ["DomUtil"] );
-
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    // DOM utility methods used by Rangy
+    api.createCoreModule("DomUtil", [], function(api, module) {
+        var UNDEF = "undefined";
+        var util = api.util;
+        var getBody = util.getBody;
 
-    var dom = api.dom;
-    var DomPosition = dom.DomPosition;
-    var DOMException = api.DOMException;
-    
-    /*----------------------------------------------------------------------------------------------------------------*/
+        // Perform feature tests
+        if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
+            module.fail("document missing a Node creation method");
+        }
 
-    // Utility functions
+        if (!util.isHostMethod(document, "getElementsByTagName")) {
+            module.fail("document missing getElementsByTagName method");
+        }
 
-    function isNonTextPartiallySelected(node, range) {
-        return (node.nodeType != 3) &&
-               (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
-    }
+        var el = document.createElement("div");
+        if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
+                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
+            module.fail("Incomplete Element implementation");
+        }
 
-    function getRangeDocument(range) {
-        return dom.getDocument(range.startContainer);
-    }
+        // innerHTML is required for Range's createContextualFragment method
+        if (!util.isHostProperty(el, "innerHTML")) {
+            module.fail("Element is missing innerHTML property");
+        }
 
-    function dispatchEvent(range, type, args) {
-        var listeners = range._listeners[type];
-        if (listeners) {
-            for (var i = 0, len = listeners.length; i < len; ++i) {
-                listeners[i].call(range, {target: range, args: args});
-            }
+        var textNode = document.createTextNode("test");
+        if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
+                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
+                !util.areHostProperties(textNode, ["data"]))) {
+            module.fail("Incomplete Text Node implementation");
         }
-    }
 
-    function getBoundaryBeforeNode(node) {
-        return new DomPosition(node.parentNode, dom.getNodeIndex(node));
-    }
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
+        // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
+        // contains just the document as a single element and the value searched for is the document.
+        var arrayContains = /*Array.prototype.indexOf ?
+            function(arr, val) {
+                return arr.indexOf(val) > -1;
+            }:*/
+
+            function(arr, val) {
+                var i = arr.length;
+                while (i--) {
+                    if (arr[i] === val) {
+                        return true;
+                    }
+                }
+                return false;
+            };
 
-    function getBoundaryAfterNode(node) {
-        return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
-    }
+        // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
+        function isHtmlNamespace(node) {
+            var ns;
+            return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
+        }
 
-    function insertNodeAtPosition(node, n, o) {
-        var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
-        if (dom.isCharacterDataNode(n)) {
-            if (o == n.length) {
-                dom.insertAfter(node, n);
-            } else {
-                n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
+        function parentElement(node) {
+            var parent = node.parentNode;
+            return (parent.nodeType == 1) ? parent : null;
+        }
+
+        function getNodeIndex(node) {
+            var i = 0;
+            while( (node = node.previousSibling) ) {
+                ++i;
             }
-        } else if (o >= n.childNodes.length) {
-            n.appendChild(node);
-        } else {
-            n.insertBefore(node, n.childNodes[o]);
+            return i;
         }
-        return firstNodeInserted;
-    }
 
-    function cloneSubtree(iterator) {
-        var partiallySelected;
-        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
-            partiallySelected = iterator.isPartiallySelectedSubtree();
+        function getNodeLength(node) {
+            switch (node.nodeType) {
+                case 7:
+                case 10:
+                    return 0;
+                case 3:
+                case 8:
+                    return node.length;
+                default:
+                    return node.childNodes.length;
+            }
+        }
 
-            node = node.cloneNode(!partiallySelected);
-            if (partiallySelected) {
-                subIterator = iterator.getSubtreeIterator();
-                node.appendChild(cloneSubtree(subIterator));
-                subIterator.detach(true);
+        function getCommonAncestor(node1, node2) {
+            var ancestors = [], n;
+            for (n = node1; n; n = n.parentNode) {
+                ancestors.push(n);
             }
 
-            if (node.nodeType == 10) { // DocumentType
-                throw new DOMException("HIERARCHY_REQUEST_ERR");
+            for (n = node2; n; n = n.parentNode) {
+                if (arrayContains(ancestors, n)) {
+                    return n;
+                }
             }
-            frag.appendChild(node);
+
+            return null;
         }
-        return frag;
-    }
 
-    function iterateSubtree(rangeIterator, func, iteratorState) {
-        var it, n;
-        iteratorState = iteratorState || { stop: false };
-        for (var node, subRangeIterator; node = rangeIterator.next(); ) {
-            //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
-            if (rangeIterator.isPartiallySelectedSubtree()) {
-                // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
-                // node selected by the Range.
-                if (func(node) === false) {
-                    iteratorState.stop = true;
-                    return;
+        function isAncestorOf(ancestor, descendant, selfIsAncestor) {
+            var n = selfIsAncestor ? descendant : descendant.parentNode;
+            while (n) {
+                if (n === ancestor) {
+                    return true;
                 } else {
-                    subRangeIterator = rangeIterator.getSubtreeIterator();
-                    iterateSubtree(subRangeIterator, func, iteratorState);
-                    subRangeIterator.detach(true);
-                    if (iteratorState.stop) {
-                        return;
-                    }
-                }
-            } else {
-                // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
-                // descendant
-                it = dom.createIterator(node);
-                while ( (n = it.next()) ) {
-                    if (func(n) === false) {
-                        iteratorState.stop = true;
-                        return;
-                    }
+                    n = n.parentNode;
                 }
             }
+            return false;
         }
-    }
 
-    function deleteSubtree(iterator) {
-        var subIterator;
-        while (iterator.next()) {
-            if (iterator.isPartiallySelectedSubtree()) {
-                subIterator = iterator.getSubtreeIterator();
-                deleteSubtree(subIterator);
-                subIterator.detach(true);
-            } else {
-                iterator.remove();
-            }
+        function isOrIsAncestorOf(ancestor, descendant) {
+            return isAncestorOf(ancestor, descendant, true);
         }
-    }
 
-    function extractSubtree(iterator) {
+        function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
+            var p, n = selfIsAncestor ? node : node.parentNode;
+            while (n) {
+                p = n.parentNode;
+                if (p === ancestor) {
+                    return n;
+                }
+                n = p;
+            }
+            return null;
+        }
 
-        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+        function isCharacterDataNode(node) {
+            var t = node.nodeType;
+            return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
+        }
 
+        function isTextOrCommentNode(node) {
+            if (!node) {
+                return false;
+            }
+            var t = node.nodeType;
+            return t == 3 || t == 8 ; // Text or Comment
+        }
 
-            if (iterator.isPartiallySelectedSubtree()) {
-                node = node.cloneNode(false);
-                subIterator = iterator.getSubtreeIterator();
-                node.appendChild(extractSubtree(subIterator));
-                subIterator.detach(true);
+        function insertAfter(node, precedingNode) {
+            var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
+            if (nextNode) {
+                parent.insertBefore(node, nextNode);
             } else {
-                iterator.remove();
+                parent.appendChild(node);
             }
-            if (node.nodeType == 10) { // DocumentType
-                throw new DOMException("HIERARCHY_REQUEST_ERR");
-            }
-            frag.appendChild(node);
+            return node;
         }
-        return frag;
-    }
 
-    function getNodesInRange(range, nodeTypes, filter) {
-        //log.info("getNodesInRange, " + nodeTypes.join(","));
-        var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
-        var filterExists = !!filter;
-        if (filterNodeTypes) {
-            regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
-        }
-
-        var nodes = [];
-        iterateSubtree(new RangeIterator(range, false), function(node) {
-            if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
-                nodes.push(node);
+        // Note that we cannot use splitText() because it is bugridden in IE 9.
+        function splitDataNode(node, index, positionsToPreserve) {
+            var newNode = node.cloneNode(false);
+            newNode.deleteData(0, index);
+            node.deleteData(index, node.length - index);
+            insertAfter(newNode, node);
+
+            // Preserve positions
+            if (positionsToPreserve) {
+                for (var i = 0, position; position = positionsToPreserve[i++]; ) {
+                    // Handle case where position was inside the portion of node after the split point
+                    if (position.node == node && position.offset > index) {
+                        position.node = newNode;
+                        position.offset -= index;
+                    }
+                    // Handle the case where the position is a node offset within node's parent
+                    else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
+                        ++position.offset;
+                    }
+                }
             }
-        });
-        return nodes;
-    }
-
-    function inspect(range) {
-        var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
-        return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
-                dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
-    }
+            return newNode;
+        }
 
-    /*----------------------------------------------------------------------------------------------------------------*/
+        function getDocument(node) {
+            if (node.nodeType == 9) {
+                return node;
+            } else if (typeof node.ownerDocument != UNDEF) {
+                return node.ownerDocument;
+            } else if (typeof node.document != UNDEF) {
+                return node.document;
+            } else if (node.parentNode) {
+                return getDocument(node.parentNode);
+            } else {
+                throw module.createError("getDocument: no document found for node");
+            }
+        }
 
-    // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
+        function getWindow(node) {
+            var doc = getDocument(node);
+            if (typeof doc.defaultView != UNDEF) {
+                return doc.defaultView;
+            } else if (typeof doc.parentWindow != UNDEF) {
+                return doc.parentWindow;
+            } else {
+                throw module.createError("Cannot get a window object for node");
+            }
+        }
 
-    /**
-     * @constructor
-     */
-    function RangeIterator(range, clonePartiallySelectedTextNodes) {
-        this.range = range;
-        this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
+        function getIframeDocument(iframeEl) {
+            if (typeof iframeEl.contentDocument != UNDEF) {
+                return iframeEl.contentDocument;
+            } else if (typeof iframeEl.contentWindow != UNDEF) {
+                return iframeEl.contentWindow.document;
+            } else {
+                throw module.createError("getIframeDocument: No Document object found for iframe element");
+            }
+        }
 
+        function getIframeWindow(iframeEl) {
+            if (typeof iframeEl.contentWindow != UNDEF) {
+                return iframeEl.contentWindow;
+            } else if (typeof iframeEl.contentDocument != UNDEF) {
+                return iframeEl.contentDocument.defaultView;
+            } else {
+                throw module.createError("getIframeWindow: No Window object found for iframe element");
+            }
+        }
 
+        // This looks bad. Is it worth it?
+        function isWindow(obj) {
+            return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
+        }
 
-        if (!range.collapsed) {
-            this.sc = range.startContainer;
-            this.so = range.startOffset;
-            this.ec = range.endContainer;
-            this.eo = range.endOffset;
-            var root = range.commonAncestorContainer;
+        function getContentDocument(obj, module, methodName) {
+            var doc;
 
-            if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
-                this.isSingleCharacterDataNode = true;
-                this._first = this._last = this._next = this.sc;
-            } else {
-                this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
-                    this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
-                this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
-                    this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
+            if (!obj) {
+                doc = document;
             }
 
-        }
-    }
+            // Test if a DOM node has been passed and obtain a document object for it if so
+            else if (util.isHostProperty(obj, "nodeType")) {
+                doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
+                    getIframeDocument(obj) : getDocument(obj);
+            }
 
-    RangeIterator.prototype = {
-        _current: null,
-        _next: null,
-        _first: null,
-        _last: null,
-        isSingleCharacterDataNode: false,
+            // Test if the doc parameter appears to be a Window object
+            else if (isWindow(obj)) {
+                doc = obj.document;
+            }
 
-        reset: function() {
-            this._current = null;
-            this._next = this._first;
-        },
+            if (!doc) {
+                throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
+            }
 
-        hasNext: function() {
-            return !!this._next;
-        },
+            return doc;
+        }
 
-        next: function() {
-            // Move to next node
-            var current = this._current = this._next;
-            if (current) {
-                this._next = (current !== this._last) ? current.nextSibling : null;
+        function getRootContainer(node) {
+            var parent;
+            while ( (parent = node.parentNode) ) {
+                node = parent;
+            }
+            return node;
+        }
 
-                // Check for partially selected text nodes
-                if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
-                    if (current === this.ec) {
+        function comparePoints(nodeA, offsetA, nodeB, offsetB) {
+            // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
+            var nodeC, root, childA, childB, n;
+            if (nodeA == nodeB) {
+                // Case 1: nodes are the same
+                return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
+            } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
+                // Case 2: node C (container B or an ancestor) is a child node of A
+                return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
+            } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
+                // Case 3: node C (container A or an ancestor) is a child node of B
+                return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
+            } else {
+                root = getCommonAncestor(nodeA, nodeB);
+                if (!root) {
+                    throw new Error("comparePoints error: nodes have no common ancestor");
+                }
 
-                        (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
-                    }
-                    if (this._current === this.sc) {
+                // Case 4: containers are siblings or descendants of siblings
+                childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
+                childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
 
-                        (current = current.cloneNode(true)).deleteData(0, this.so);
+                if (childA === childB) {
+                    // This shouldn't be possible
+                    throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
+                } else {
+                    n = root.firstChild;
+                    while (n) {
+                        if (n === childA) {
+                            return -1;
+                        } else if (n === childB) {
+                            return 1;
+                        }
+                        n = n.nextSibling;
                     }
                 }
             }
+        }
 
-            return current;
-        },
-
-        remove: function() {
-            var current = this._current, start, end;
+        /*----------------------------------------------------------------------------------------------------------------*/
 
-            if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
-                start = (current === this.sc) ? this.so : 0;
-                end = (current === this.ec) ? this.eo : current.length;
-                if (start != end) {
-                    current.deleteData(start, end - start);
-                }
-            } else {
-                if (current.parentNode) {
-                    current.parentNode.removeChild(current);
-                } else {
+        // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
+        var crashyTextNodes = false;
 
-                }
+        function isBrokenNode(node) {
+            var n;
+            try {
+                n = node.parentNode;
+                return false;
+            } catch (e) {
+                return true;
             }
-        },
+        }
 
-        // Checks if the current node is partially selected
-        isPartiallySelectedSubtree: function() {
-            var current = this._current;
-            return isNonTextPartiallySelected(current, this.range);
-        },
+        (function() {
+            var el = document.createElement("b");
+            el.innerHTML = "1";
+            var textNode = el.firstChild;
+            el.innerHTML = "<br />";
+            crashyTextNodes = isBrokenNode(textNode);
 
-        getSubtreeIterator: function() {
-            var subRange;
-            if (this.isSingleCharacterDataNode) {
-                subRange = this.range.cloneRange();
-                subRange.collapse();
-            } else {
-                subRange = new Range(getRangeDocument(this.range));
-                var current = this._current;
-                var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
+            api.features.crashyTextNodes = crashyTextNodes;
+        })();
 
-                if (dom.isAncestorOf(current, this.sc, true)) {
-                    startContainer = this.sc;
-                    startOffset = this.so;
-                }
-                if (dom.isAncestorOf(current, this.ec, true)) {
-                    endContainer = this.ec;
-                    endOffset = this.eo;
-                }
+        /*----------------------------------------------------------------------------------------------------------------*/
 
-                updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
+        function inspectNode(node) {
+            if (!node) {
+                return "[No node]";
+            }
+            if (crashyTextNodes && isBrokenNode(node)) {
+                return "[Broken node]";
             }
-            return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
-        },
+            if (isCharacterDataNode(node)) {
+                return '"' + node.data + '"';
+            }
+            if (node.nodeType == 1) {
+                var idAttr = node.id ? ' id="' + node.id + '"' : "";
+                return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
+            }
+            return node.nodeName;
+        }
 
-        detach: function(detachRange) {
-            if (detachRange) {
-                this.range.detach();
+        function fragmentFromNodeChildren(node) {
+            var fragment = getDocument(node).createDocumentFragment(), child;
+            while ( (child = node.firstChild) ) {
+                fragment.appendChild(child);
             }
-            this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
+            return fragment;
+        }
+
+        var getComputedStyleProperty;
+        if (typeof window.getComputedStyle != UNDEF) {
+            getComputedStyleProperty = function(el, propName) {
+                return getWindow(el).getComputedStyle(el, null)[propName];
+            };
+        } else if (typeof document.documentElement.currentStyle != UNDEF) {
+            getComputedStyleProperty = function(el, propName) {
+                return el.currentStyle ? el.currentStyle[propName] : "";
+            };
+        } else {
+            module.fail("No means of obtaining computed style properties found");
         }
-    };
 
-    /*----------------------------------------------------------------------------------------------------------------*/
+        function createTestElement(doc, html, contentEditable) {
+            var body = getBody(doc);
+            var el = doc.createElement("div");
+            el.contentEditable = "" + !!contentEditable;
+            if (html) {
+                el.innerHTML = html;
+            }
 
-    // Exceptions
+            // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
+            var bodyFirstChild = body.firstChild;
+            if (bodyFirstChild) {
+                body.insertBefore(el, bodyFirstChild);
+            } else {
+                body.appendChild(el);
+            }
 
-    /**
-     * @constructor
-     */
-    function RangeException(codeName) {
-        this.code = this[codeName];
-        this.codeName = codeName;
-        this.message = "RangeException: " + this.codeName;
-    }
+            return el;
+        }
 
-    RangeException.prototype = {
-        BAD_BOUNDARYPOINTS_ERR: 1,
-        INVALID_NODE_TYPE_ERR: 2
-    };
+        function removeNode(node) {
+            return node.parentNode.removeChild(node);
+        }
 
-    RangeException.prototype.toString = function() {
-        return this.message;
-    };
+        function NodeIterator(root) {
+            this.root = root;
+            this._next = root;
+        }
 
-    /*----------------------------------------------------------------------------------------------------------------*/
+        NodeIterator.prototype = {
+            _current: null,
 
-    /**
-     * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
-     * TODO: Look into making this a proper iterator, not requiring preloading everything first
-     * @constructor
-     */
-    function RangeNodeIterator(range, nodeTypes, filter) {
-        this.nodes = getNodesInRange(range, nodeTypes, filter);
-        this._next = this.nodes[0];
-        this._position = 0;
-    }
+            hasNext: function() {
+                return !!this._next;
+            },
 
-    RangeNodeIterator.prototype = {
-        _current: null,
+            next: function() {
+                var n = this._current = this._next;
+                var child, next;
+                if (this._current) {
+                    child = n.firstChild;
+                    if (child) {
+                        this._next = child;
+                    } else {
+                        next = null;
+                        while ((n !== this.root) && !(next = n.nextSibling)) {
+                            n = n.parentNode;
+                        }
+                        this._next = next;
+                    }
+                }
+                return this._current;
+            },
 
-        hasNext: function() {
-            return !!this._next;
-        },
+            detach: function() {
+                this._current = this._next = this.root = null;
+            }
+        };
 
-        next: function() {
-            this._current = this._next;
-            this._next = this.nodes[ ++this._position ];
-            return this._current;
-        },
+        function createIterator(root) {
+            return new NodeIterator(root);
+        }
 
-        detach: function() {
-            this._current = this._next = this.nodes = null;
+        function DomPosition(node, offset) {
+            this.node = node;
+            this.offset = offset;
         }
-    };
 
-    var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
-    var rootContainerNodeTypes = [2, 9, 11];
-    var readonlyNodeTypes = [5, 6, 10, 12];
-    var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
-    var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
+        DomPosition.prototype = {
+            equals: function(pos) {
+                return !!pos && this.node === pos.node && this.offset == pos.offset;
+            },
 
-    function createAncestorFinder(nodeTypes) {
-        return function(node, selfIsAncestor) {
-            var t, n = selfIsAncestor ? node : node.parentNode;
-            while (n) {
-                t = n.nodeType;
-                if (dom.arrayContains(nodeTypes, t)) {
-                    return n;
-                }
-                n = n.parentNode;
+            inspect: function() {
+                return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
+            },
+
+            toString: function() {
+                return this.inspect();
             }
-            return null;
         };
-    }
 
-    var getRootContainer = dom.getRootContainer;
-    var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
-    var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
-    var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
-
-    function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
-        if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
-            throw new RangeException("INVALID_NODE_TYPE_ERR");
+        function DOMException(codeName) {
+            this.code = this[codeName];
+            this.codeName = codeName;
+            this.message = "DOMException: " + this.codeName;
         }
-    }
 
-    function assertNotDetached(range) {
-        if (!range.startContainer) {
-            throw new DOMException("INVALID_STATE_ERR");
+        DOMException.prototype = {
+            INDEX_SIZE_ERR: 1,
+            HIERARCHY_REQUEST_ERR: 3,
+            WRONG_DOCUMENT_ERR: 4,
+            NO_MODIFICATION_ALLOWED_ERR: 7,
+            NOT_FOUND_ERR: 8,
+            NOT_SUPPORTED_ERR: 9,
+            INVALID_STATE_ERR: 11,
+            INVALID_NODE_TYPE_ERR: 24
+        };
+
+        DOMException.prototype.toString = function() {
+            return this.message;
+        };
+
+        api.dom = {
+            arrayContains: arrayContains,
+            isHtmlNamespace: isHtmlNamespace,
+            parentElement: parentElement,
+            getNodeIndex: getNodeIndex,
+            getNodeLength: getNodeLength,
+            getCommonAncestor: getCommonAncestor,
+            isAncestorOf: isAncestorOf,
+            isOrIsAncestorOf: isOrIsAncestorOf,
+            getClosestAncestorIn: getClosestAncestorIn,
+            isCharacterDataNode: isCharacterDataNode,
+            isTextOrCommentNode: isTextOrCommentNode,
+            insertAfter: insertAfter,
+            splitDataNode: splitDataNode,
+            getDocument: getDocument,
+            getWindow: getWindow,
+            getIframeWindow: getIframeWindow,
+            getIframeDocument: getIframeDocument,
+            getBody: getBody,
+            isWindow: isWindow,
+            getContentDocument: getContentDocument,
+            getRootContainer: getRootContainer,
+            comparePoints: comparePoints,
+            isBrokenNode: isBrokenNode,
+            inspectNode: inspectNode,
+            getComputedStyleProperty: getComputedStyleProperty,
+            createTestElement: createTestElement,
+            removeNode: removeNode,
+            fragmentFromNodeChildren: fragmentFromNodeChildren,
+            createIterator: createIterator,
+            DomPosition: DomPosition
+        };
+
+        api.DOMException = DOMException;
+    });\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    // Pure JavaScript implementation of DOM Range
+    api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
+        var dom = api.dom;
+        var util = api.util;
+        var DomPosition = dom.DomPosition;
+        var DOMException = api.DOMException;
+
+        var isCharacterDataNode = dom.isCharacterDataNode;
+        var getNodeIndex = dom.getNodeIndex;
+        var isOrIsAncestorOf = dom.isOrIsAncestorOf;
+        var getDocument = dom.getDocument;
+        var comparePoints = dom.comparePoints;
+        var splitDataNode = dom.splitDataNode;
+        var getClosestAncestorIn = dom.getClosestAncestorIn;
+        var getNodeLength = dom.getNodeLength;
+        var arrayContains = dom.arrayContains;
+        var getRootContainer = dom.getRootContainer;
+        var crashyTextNodes = api.features.crashyTextNodes;
+
+        var removeNode = dom.removeNode;
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Utility functions
+
+        function isNonTextPartiallySelected(node, range) {
+            return (node.nodeType != 3) &&
+                   (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
         }
-    }
 
-    function assertValidNodeType(node, invalidTypes) {
-        if (!dom.arrayContains(invalidTypes, node.nodeType)) {
-            throw new RangeException("INVALID_NODE_TYPE_ERR");
+        function getRangeDocument(range) {
+            return range.document || getDocument(range.startContainer);
         }
-    }
 
-    function assertValidOffset(node, offset) {
-        if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
-            throw new DOMException("INDEX_SIZE_ERR");
+        function getRangeRoot(range) {
+            return getRootContainer(range.startContainer);
         }
-    }
 
-    function assertSameDocumentOrFragment(node1, node2) {
-        if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
-            throw new DOMException("WRONG_DOCUMENT_ERR");
+        function getBoundaryBeforeNode(node) {
+            return new DomPosition(node.parentNode, getNodeIndex(node));
         }
-    }
 
-    function assertNodeNotReadOnly(node) {
-        if (getReadonlyAncestor(node, true)) {
-            throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
+        function getBoundaryAfterNode(node) {
+            return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
         }
-    }
 
-    function assertNode(node, codeName) {
-        if (!node) {
-            throw new DOMException(codeName);
+        function insertNodeAtPosition(node, n, o) {
+            var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
+            if (isCharacterDataNode(n)) {
+                if (o == n.length) {
+                    dom.insertAfter(node, n);
+                } else {
+                    n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
+                }
+            } else if (o >= n.childNodes.length) {
+                n.appendChild(node);
+            } else {
+                n.insertBefore(node, n.childNodes[o]);
+            }
+            return firstNodeInserted;
         }
-    }
 
-    function isOrphan(node) {
-        return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
-    }
+        function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
+            assertRangeValid(rangeA);
+            assertRangeValid(rangeB);
 
-    function isValidOffset(node, offset) {
-        return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
-    }
+            if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
 
-    function isRangeValid(range) {
-        return (!!range.startContainer && !!range.endContainer
-                && !isOrphan(range.startContainer)
-                && !isOrphan(range.endContainer)
-                && isValidOffset(range.startContainer, range.startOffset)
-                && isValidOffset(range.endContainer, range.endOffset));
-    }
+            var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
+                endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
 
-    function assertRangeValid(range) {
-        assertNotDetached(range);
-        if (!isRangeValid(range)) {
-            throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
+            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
         }
-    }
-
-    /*----------------------------------------------------------------------------------------------------------------*/
-
-    // Test the browser's innerHTML support to decide how to implement createContextualFragment
-    var styleEl = document.createElement("style");
-    var htmlParsingConforms = false;
-    try {
-        styleEl.innerHTML = "<b>x</b>";
-        htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
-    } catch (e) {
-        // IE 6 and 7 throw
-    }
 
-    api.features.htmlParsingConforms = htmlParsingConforms;
-
-    var createContextualFragment = htmlParsingConforms ?
+        function cloneSubtree(iterator) {
+            var partiallySelected;
+            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
+                partiallySelected = iterator.isPartiallySelectedSubtree();
+                node = node.cloneNode(!partiallySelected);
+                if (partiallySelected) {
+                    subIterator = iterator.getSubtreeIterator();
+                    node.appendChild(cloneSubtree(subIterator));
+                    subIterator.detach();
+                }
 
-        // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
-        // discussion and base code for this implementation at issue 67.
-        // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
-        // Thanks to Aleks Williams.
-        function(fragmentStr) {
-            // "Let node the context object's start's node."
-            var node = this.startContainer;
-            var doc = dom.getDocument(node);
+                if (node.nodeType == 10) { // DocumentType
+                    throw new DOMException("HIERARCHY_REQUEST_ERR");
+                }
+                frag.appendChild(node);
+            }
+            return frag;
+        }
 
-            // "If the context object's start's node is null, raise an INVALID_STATE_ERR
-            // exception and abort these steps."
-            if (!node) {
-                throw new DOMException("INVALID_STATE_ERR");
+        function iterateSubtree(rangeIterator, func, iteratorState) {
+            var it, n;
+            iteratorState = iteratorState || { stop: false };
+            for (var node, subRangeIterator; node = rangeIterator.next(); ) {
+                if (rangeIterator.isPartiallySelectedSubtree()) {
+                    if (func(node) === false) {
+                        iteratorState.stop = true;
+                        return;
+                    } else {
+                        // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
+                        // the node selected by the Range.
+                        subRangeIterator = rangeIterator.getSubtreeIterator();
+                        iterateSubtree(subRangeIterator, func, iteratorState);
+                        subRangeIterator.detach();
+                        if (iteratorState.stop) {
+                            return;
+                        }
+                    }
+                } else {
+                    // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
+                    // descendants
+                    it = dom.createIterator(node);
+                    while ( (n = it.next()) ) {
+                        if (func(n) === false) {
+                            iteratorState.stop = true;
+                            return;
+                        }
+                    }
+                }
             }
+        }
 
-            // "Let element be as follows, depending on node's interface:"
-            // Document, Document Fragment: null
-            var el = null;
+        function deleteSubtree(iterator) {
+            var subIterator;
+            while (iterator.next()) {
+                if (iterator.isPartiallySelectedSubtree()) {
+                    subIterator = iterator.getSubtreeIterator();
+                    deleteSubtree(subIterator);
+                    subIterator.detach();
+                } else {
+                    iterator.remove();
+                }
+            }
+        }
 
-            // "Element: node"
-            if (node.nodeType == 1) {
-                el = node;
+        function extractSubtree(iterator) {
+            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
 
-            // "Text, Comment: node's parentElement"
-            } else if (dom.isCharacterDataNode(node)) {
-                el = dom.parentElement(node);
+                if (iterator.isPartiallySelectedSubtree()) {
+                    node = node.cloneNode(false);
+                    subIterator = iterator.getSubtreeIterator();
+                    node.appendChild(extractSubtree(subIterator));
+                    subIterator.detach();
+                } else {
+                    iterator.remove();
+                }
+                if (node.nodeType == 10) { // DocumentType
+                    throw new DOMException("HIERARCHY_REQUEST_ERR");
+                }
+                frag.appendChild(node);
             }
+            return frag;
+        }
 
-            // "If either element is null or element's ownerDocument is an HTML document
-            // and element's local name is "html" and element's namespace is the HTML
-            // namespace"
-            if (el === null || (
-                el.nodeName == "HTML"
-                && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
-                && dom.isHtmlNamespace(el)
-            )) {
-
-            // "let element be a new Element with "body" as its local name and the HTML
-            // namespace as its namespace.""
-                el = doc.createElement("body");
-            } else {
-                el = el.cloneNode(false);
+        function getNodesInRange(range, nodeTypes, filter) {
+            var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
+            var filterExists = !!filter;
+            if (filterNodeTypes) {
+                regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
             }
 
-            // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
-            // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
-            // "In either case, the algorithm must be invoked with fragment as the input
-            // and element as the context element."
-            el.innerHTML = fragmentStr;
+            var nodes = [];
+            iterateSubtree(new RangeIterator(range, false), function(node) {
+                if (filterNodeTypes && !regex.test(node.nodeType)) {
+                    return;
+                }
+                if (filterExists && !filter(node)) {
+                    return;
+                }
+                // Don't include a boundary container if it is a character data node and the range does not contain any
+                // of its character data. See issue 190.
+                var sc = range.startContainer;
+                if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
+                    return;
+                }
 
-            // "If this raises an exception, then abort these steps. Otherwise, let new
-            // children be the nodes returned."
+                var ec = range.endContainer;
+                if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
+                    return;
+                }
 
-            // "Let fragment be a new DocumentFragment."
-            // "Append all new children to fragment."
-            // "Return fragment."
-            return dom.fragmentFromNodeChildren(el);
-        } :
+                nodes.push(node);
+            });
+            return nodes;
+        }
 
-        // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
-        // previous versions of Rangy used (with the exception of using a body element rather than a div)
-        function(fragmentStr) {
-            assertNotDetached(this);
-            var doc = getRangeDocument(this);
-            var el = doc.createElement("body");
-            el.innerHTML = fragmentStr;
+        function inspect(range) {
+            var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
+            return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
+                    dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
+        }
 
-            return dom.fragmentFromNodeChildren(el);
-        };
+        /*----------------------------------------------------------------------------------------------------------------*/
 
-    /*----------------------------------------------------------------------------------------------------------------*/
+        // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
 
-    var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
-        "commonAncestorContainer"];
+        function RangeIterator(range, clonePartiallySelectedTextNodes) {
+            this.range = range;
+            this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
 
-    var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
-    var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
 
-    function RangePrototype() {}
+            if (!range.collapsed) {
+                this.sc = range.startContainer;
+                this.so = range.startOffset;
+                this.ec = range.endContainer;
+                this.eo = range.endOffset;
+                var root = range.commonAncestorContainer;
 
-    RangePrototype.prototype = {
-        attachListener: function(type, listener) {
-            this._listeners[type].push(listener);
-        },
+                if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
+                    this.isSingleCharacterDataNode = true;
+                    this._first = this._last = this._next = this.sc;
+                } else {
+                    this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
+                        this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
+                    this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
+                        this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
+                }
+            }
+        }
 
-        compareBoundaryPoints: function(how, range) {
-            assertRangeValid(this);
-            assertSameDocumentOrFragment(this.startContainer, range.startContainer);
+        RangeIterator.prototype = {
+            _current: null,
+            _next: null,
+            _first: null,
+            _last: null,
+            isSingleCharacterDataNode: false,
 
-            var nodeA, offsetA, nodeB, offsetB;
-            var prefixA = (how == e2s || how == s2s) ? "start" : "end";
-            var prefixB = (how == s2e || how == s2s) ? "start" : "end";
-            nodeA = this[prefixA + "Container"];
-            offsetA = this[prefixA + "Offset"];
-            nodeB = range[prefixB + "Container"];
-            offsetB = range[prefixB + "Offset"];
-            return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
-        },
+            reset: function() {
+                this._current = null;
+                this._next = this._first;
+            },
 
-        insertNode: function(node) {
-            assertRangeValid(this);
-            assertValidNodeType(node, insertableNodeTypes);
-            assertNodeNotReadOnly(this.startContainer);
+            hasNext: function() {
+                return !!this._next;
+            },
 
-            if (dom.isAncestorOf(node, this.startContainer, true)) {
-                throw new DOMException("HIERARCHY_REQUEST_ERR");
-            }
+            next: function() {
+                // Move to next node
+                var current = this._current = this._next;
+                if (current) {
+                    this._next = (current !== this._last) ? current.nextSibling : null;
 
-            // No check for whether the container of the start of the Range is of a type that does not allow
-            // children of the type of node: the browser's DOM implementation should do this for us when we attempt
-            // to add the node
+                    // Check for partially selected text nodes
+                    if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
+                        if (current === this.ec) {
+                            (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
+                        }
+                        if (this._current === this.sc) {
+                            (current = current.cloneNode(true)).deleteData(0, this.so);
+                        }
+                    }
+                }
 
-            var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
-            this.setStartBefore(firstNodeInserted);
-        },
+                return current;
+            },
 
-        cloneContents: function() {
-            assertRangeValid(this);
+            remove: function() {
+                var current = this._current, start, end;
 
-            var clone, frag;
-            if (this.collapsed) {
-                return getRangeDocument(this).createDocumentFragment();
-            } else {
-                if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
-                    clone = this.startContainer.cloneNode(true);
-                    clone.data = clone.data.slice(this.startOffset, this.endOffset);
-                    frag = getRangeDocument(this).createDocumentFragment();
-                    frag.appendChild(clone);
-                    return frag;
+                if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
+                    start = (current === this.sc) ? this.so : 0;
+                    end = (current === this.ec) ? this.eo : current.length;
+                    if (start != end) {
+                        current.deleteData(start, end - start);
+                    }
                 } else {
-                    var iterator = new RangeIterator(this, true);
-                    clone = cloneSubtree(iterator);
-                    iterator.detach();
+                    if (current.parentNode) {
+                        removeNode(current);
+                    } else {
+                    }
                 }
-                return clone;
-            }
-        },
-
-        canSurroundContents: function() {
-            assertRangeValid(this);
-            assertNodeNotReadOnly(this.startContainer);
-            assertNodeNotReadOnly(this.endContainer);
-
-            // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
-            // no non-text nodes.
-            var iterator = new RangeIterator(this, true);
-            var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
-                    (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
-            iterator.detach();
-            return !boundariesInvalid;
-        },
+            },
 
-        surroundContents: function(node) {
-            assertValidNodeType(node, surroundNodeTypes);
+            // Checks if the current node is partially selected
+            isPartiallySelectedSubtree: function() {
+                var current = this._current;
+                return isNonTextPartiallySelected(current, this.range);
+            },
 
-            if (!this.canSurroundContents()) {
-                throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
-            }
+            getSubtreeIterator: function() {
+                var subRange;
+                if (this.isSingleCharacterDataNode) {
+                    subRange = this.range.cloneRange();
+                    subRange.collapse(false);
+                } else {
+                    subRange = new Range(getRangeDocument(this.range));
+                    var current = this._current;
+                    var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
 
-            // Extract the contents
-            var content = this.extractContents();
+                    if (isOrIsAncestorOf(current, this.sc)) {
+                        startContainer = this.sc;
+                        startOffset = this.so;
+                    }
+                    if (isOrIsAncestorOf(current, this.ec)) {
+                        endContainer = this.ec;
+                        endOffset = this.eo;
+                    }
 
-            // Clear the children of the node
-            if (node.hasChildNodes()) {
-                while (node.lastChild) {
-                    node.removeChild(node.lastChild);
+                    updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
                 }
-            }
-
-            // Insert the new node and add the extracted contents
-            insertNodeAtPosition(node, this.startContainer, this.startOffset);
-            node.appendChild(content);
-
-            this.selectNode(node);
-        },
+                return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
+            },
 
-        cloneRange: function() {
-            assertRangeValid(this);
-            var range = new Range(getRangeDocument(this));
-            var i = rangeProperties.length, prop;
-            while (i--) {
-                prop = rangeProperties[i];
-                range[prop] = this[prop];
+            detach: function() {
+                this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
             }
-            return range;
-        },
-
-        toString: function() {
-            assertRangeValid(this);
-            var sc = this.startContainer;
-            if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
-                return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
-            } else {
-                var textBits = [], iterator = new RangeIterator(this, true);
-
-                iterateSubtree(iterator, function(node) {
-                    // Accept only text or CDATA nodes, not comments
+        };
 
-                    if (node.nodeType == 3 || node.nodeType == 4) {
-                        textBits.push(node.data);
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
+        var rootContainerNodeTypes = [2, 9, 11];
+        var readonlyNodeTypes = [5, 6, 10, 12];
+        var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
+        var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
+
+        function createAncestorFinder(nodeTypes) {
+            return function(node, selfIsAncestor) {
+                var t, n = selfIsAncestor ? node : node.parentNode;
+                while (n) {
+                    t = n.nodeType;
+                    if (arrayContains(nodeTypes, t)) {
+                        return n;
                     }
-                });
-                iterator.detach();
-                return textBits.join("");
-            }
-        },
-
-        // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
-        // been removed from Mozilla.
+                    n = n.parentNode;
+                }
+                return null;
+            };
+        }
 
-        compareNode: function(node) {
-            assertRangeValid(this);
+        var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
+        var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
+        var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
 
-            var parent = node.parentNode;
-            var nodeIndex = dom.getNodeIndex(node);
+        function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
+            if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
+                throw new DOMException("INVALID_NODE_TYPE_ERR");
+            }
+        }
 
-            if (!parent) {
-                throw new DOMException("NOT_FOUND_ERR");
+        function assertValidNodeType(node, invalidTypes) {
+            if (!arrayContains(invalidTypes, node.nodeType)) {
+                throw new DOMException("INVALID_NODE_TYPE_ERR");
             }
+        }
 
-            var startComparison = this.comparePoint(parent, nodeIndex),
-                endComparison = this.comparePoint(parent, nodeIndex + 1);
+        function assertValidOffset(node, offset) {
+            if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
+                throw new DOMException("INDEX_SIZE_ERR");
+            }
+        }
 
-            if (startComparison < 0) { // Node starts before
-                return (endComparison > 0) ? n_b_a : n_b;
-            } else {
-                return (endComparison > 0) ? n_a : n_i;
+        function assertSameDocumentOrFragment(node1, node2) {
+            if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
             }
-        },
+        }
 
-        comparePoint: function(node, offset) {
-            assertRangeValid(this);
-            assertNode(node, "HIERARCHY_REQUEST_ERR");
-            assertSameDocumentOrFragment(node, this.startContainer);
+        function assertNodeNotReadOnly(node) {
+            if (getReadonlyAncestor(node, true)) {
+                throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
+            }
+        }
 
-            if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
-                return -1;
-            } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
-                return 1;
+        function assertNode(node, codeName) {
+            if (!node) {
+                throw new DOMException(codeName);
             }
-            return 0;
-        },
+        }
 
-        createContextualFragment: createContextualFragment,
+        function isValidOffset(node, offset) {
+            return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
+        }
 
-        toHtml: function() {
-            assertRangeValid(this);
-            var container = getRangeDocument(this).createElement("div");
-            container.appendChild(this.cloneContents());
-            return container.innerHTML;
-        },
-
-        // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
-        // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
-        intersectsNode: function(node, touchingIsIntersecting) {
-            assertRangeValid(this);
-            assertNode(node, "NOT_FOUND_ERR");
-            if (dom.getDocument(node) !== getRangeDocument(this)) {
-                return false;
+        function isRangeValid(range) {
+            return (!!range.startContainer && !!range.endContainer &&
+                    !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
+                    getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
+                    isValidOffset(range.startContainer, range.startOffset) &&
+                    isValidOffset(range.endContainer, range.endOffset));
+        }
+
+        function assertRangeValid(range) {
+            if (!isRangeValid(range)) {
+                throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
             }
+        }
 
-            var parent = node.parentNode, offset = dom.getNodeIndex(node);
-            assertNode(parent, "NOT_FOUND_ERR");
+        /*----------------------------------------------------------------------------------------------------------------*/
 
-            var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
-                endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
+        // Test the browser's innerHTML support to decide how to implement createContextualFragment
+        var styleEl = document.createElement("style");
+        var htmlParsingConforms = false;
+        try {
+            styleEl.innerHTML = "<b>x</b>";
+            htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
+        } catch (e) {
+            // IE 6 and 7 throw
+        }
 
-            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
-        },
+        api.features.htmlParsingConforms = htmlParsingConforms;
 
+        var createContextualFragment = htmlParsingConforms ?
 
-        isPointInRange: function(node, offset) {
-            assertRangeValid(this);
-            assertNode(node, "HIERARCHY_REQUEST_ERR");
-            assertSameDocumentOrFragment(node, this.startContainer);
+            // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
+            // discussion and base code for this implementation at issue 67.
+            // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
+            // Thanks to Aleks Williams.
+            function(fragmentStr) {
+                // "Let node the context object's start's node."
+                var node = this.startContainer;
+                var doc = getDocument(node);
 
-            return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
-                   (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
-        },
+                // "If the context object's start's node is null, raise an INVALID_STATE_ERR
+                // exception and abort these steps."
+                if (!node) {
+                    throw new DOMException("INVALID_STATE_ERR");
+                }
 
-        // The methods below are non-standard and invented by me.
+                // "Let element be as follows, depending on node's interface:"
+                // Document, Document Fragment: null
+                var el = null;
 
-        // Sharing a boundary start-to-end or end-to-start does not count as intersection.
-        intersectsRange: function(range, touchingIsIntersecting) {
-            assertRangeValid(this);
+                // "Element: node"
+                if (node.nodeType == 1) {
+                    el = node;
 
-            if (getRangeDocument(range) != getRangeDocument(this)) {
-                throw new DOMException("WRONG_DOCUMENT_ERR");
-            }
+                // "Text, Comment: node's parentElement"
+                } else if (isCharacterDataNode(node)) {
+                    el = dom.parentElement(node);
+                }
 
-            var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
-                endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
+                // "If either element is null or element's ownerDocument is an HTML document
+                // and element's local name is "html" and element's namespace is the HTML
+                // namespace"
+                if (el === null || (
+                    el.nodeName == "HTML" &&
+                    dom.isHtmlNamespace(getDocument(el).documentElement) &&
+                    dom.isHtmlNamespace(el)
+                )) {
+
+                // "let element be a new Element with "body" as its local name and the HTML
+                // namespace as its namespace.""
+                    el = doc.createElement("body");
+                } else {
+                    el = el.cloneNode(false);
+                }
 
-            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
-        },
+                // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
+                // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
+                // "In either case, the algorithm must be invoked with fragment as the input
+                // and element as the context element."
+                el.innerHTML = fragmentStr;
+
+                // "If this raises an exception, then abort these steps. Otherwise, let new
+                // children be the nodes returned."
+
+                // "Let fragment be a new DocumentFragment."
+                // "Append all new children to fragment."
+                // "Return fragment."
+                return dom.fragmentFromNodeChildren(el);
+            } :
+
+            // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
+            // previous versions of Rangy used (with the exception of using a body element rather than a div)
+            function(fragmentStr) {
+                var doc = getRangeDocument(this);
+                var el = doc.createElement("body");
+                el.innerHTML = fragmentStr;
+
+                return dom.fragmentFromNodeChildren(el);
+            };
 
-        intersection: function(range) {
-            if (this.intersectsRange(range)) {
-                var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
-                    endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
+        function splitRangeBoundaries(range, positionsToPreserve) {
+            assertRangeValid(range);
 
-                var intersectionRange = this.cloneRange();
+            var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
+            var startEndSame = (sc === ec);
 
-                if (startComparison == -1) {
-                    intersectionRange.setStart(range.startContainer, range.startOffset);
-                }
-                if (endComparison == 1) {
-                    intersectionRange.setEnd(range.endContainer, range.endOffset);
-                }
-                return intersectionRange;
+            if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
+                splitDataNode(ec, eo, positionsToPreserve);
             }
-            return null;
-        },
 
-        union: function(range) {
-            if (this.intersectsRange(range, true)) {
-                var unionRange = this.cloneRange();
-                if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
-                    unionRange.setStart(range.startContainer, range.startOffset);
+            if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
+                sc = splitDataNode(sc, so, positionsToPreserve);
+                if (startEndSame) {
+                    eo -= so;
+                    ec = sc;
+                } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
+                    eo++;
                 }
-                if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
-                    unionRange.setEnd(range.endContainer, range.endOffset);
-                }
-                return unionRange;
-            } else {
-                throw new RangeException("Ranges do not intersect");
-            }
-        },
-
-        containsNode: function(node, allowPartial) {
-            if (allowPartial) {
-                return this.intersectsNode(node, false);
-            } else {
-                return this.compareNode(node) == n_i;
-            }
-        },
-
-        containsNodeContents: function(node) {
-            return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
-        },
-
-        containsRange: function(range) {
-            return this.intersection(range).equals(range);
-        },
-
-        containsNodeText: function(node) {
-            var nodeRange = this.cloneRange();
-            nodeRange.selectNode(node);
-            var textNodes = nodeRange.getNodes([3]);
-            if (textNodes.length > 0) {
-                nodeRange.setStart(textNodes[0], 0);
-                var lastTextNode = textNodes.pop();
-                nodeRange.setEnd(lastTextNode, lastTextNode.length);
-                var contains = this.containsRange(nodeRange);
-                nodeRange.detach();
-                return contains;
-            } else {
-                return this.containsNodeContents(node);
+                so = 0;
             }
-        },
+            range.setStartAndEnd(sc, so, ec, eo);
+        }
 
-        createNodeIterator: function(nodeTypes, filter) {
-            assertRangeValid(this);
-            return new RangeNodeIterator(this, nodeTypes, filter);
-        },
+        function rangeToHtml(range) {
+            assertRangeValid(range);
+            var container = range.commonAncestorContainer.parentNode.cloneNode(false);
+            container.appendChild( range.cloneContents() );
+            return container.innerHTML;
+        }
 
-        getNodes: function(nodeTypes, filter) {
-            assertRangeValid(this);
-            return getNodesInRange(this, nodeTypes, filter);
-        },
+        /*----------------------------------------------------------------------------------------------------------------*/
 
-        getDocument: function() {
-            return getRangeDocument(this);
-        },
+        var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
+            "commonAncestorContainer"];
 
-        collapseBefore: function(node) {
-            assertNotDetached(this);
+        var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
+        var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
 
-            this.setEndBefore(node);
-            this.collapse(false);
-        },
+        util.extend(api.rangePrototype, {
+            compareBoundaryPoints: function(how, range) {
+                assertRangeValid(this);
+                assertSameDocumentOrFragment(this.startContainer, range.startContainer);
+
+                var nodeA, offsetA, nodeB, offsetB;
+                var prefixA = (how == e2s || how == s2s) ? "start" : "end";
+                var prefixB = (how == s2e || how == s2s) ? "start" : "end";
+                nodeA = this[prefixA + "Container"];
+                offsetA = this[prefixA + "Offset"];
+                nodeB = range[prefixB + "Container"];
+                offsetB = range[prefixB + "Offset"];
+                return comparePoints(nodeA, offsetA, nodeB, offsetB);
+            },
 
-        collapseAfter: function(node) {
-            assertNotDetached(this);
+            insertNode: function(node) {
+                assertRangeValid(this);
+                assertValidNodeType(node, insertableNodeTypes);
+                assertNodeNotReadOnly(this.startContainer);
 
-            this.setStartAfter(node);
-            this.collapse(true);
-        },
+                if (isOrIsAncestorOf(node, this.startContainer)) {
+                    throw new DOMException("HIERARCHY_REQUEST_ERR");
+                }
 
-        getName: function() {
-            return "DomRange";
-        },
+                // No check for whether the container of the start of the Range is of a type that does not allow
+                // children of the type of node: the browser's DOM implementation should do this for us when we attempt
+                // to add the node
 
-        equals: function(range) {
-            return Range.rangesEqual(this, range);
-        },
+                var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
+                this.setStartBefore(firstNodeInserted);
+            },
 
-        isValid: function() {
-            return isRangeValid(this);
-        },
+            cloneContents: function() {
+                assertRangeValid(this);
 
-        inspect: function() {
-            return inspect(this);
-        }
-    };
+                var clone, frag;
+                if (this.collapsed) {
+                    return getRangeDocument(this).createDocumentFragment();
+                } else {
+                    if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
+                        clone = this.startContainer.cloneNode(true);
+                        clone.data = clone.data.slice(this.startOffset, this.endOffset);
+                        frag = getRangeDocument(this).createDocumentFragment();
+                        frag.appendChild(clone);
+                        return frag;
+                    } else {
+                        var iterator = new RangeIterator(this, true);
+                        clone = cloneSubtree(iterator);
+                        iterator.detach();
+                    }
+                    return clone;
+                }
+            },
 
-    function copyComparisonConstantsToObject(obj) {
-        obj.START_TO_START = s2s;
-        obj.START_TO_END = s2e;
-        obj.END_TO_END = e2e;
-        obj.END_TO_START = e2s;
+            canSurroundContents: function() {
+                assertRangeValid(this);
+                assertNodeNotReadOnly(this.startContainer);
+                assertNodeNotReadOnly(this.endContainer);
 
-        obj.NODE_BEFORE = n_b;
-        obj.NODE_AFTER = n_a;
-        obj.NODE_BEFORE_AND_AFTER = n_b_a;
-        obj.NODE_INSIDE = n_i;
-    }
+                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+                // no non-text nodes.
+                var iterator = new RangeIterator(this, true);
+                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
+                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+                iterator.detach();
+                return !boundariesInvalid;
+            },
 
-    function copyComparisonConstants(constructor) {
-        copyComparisonConstantsToObject(constructor);
-        copyComparisonConstantsToObject(constructor.prototype);
-    }
+            surroundContents: function(node) {
+                assertValidNodeType(node, surroundNodeTypes);
 
-    function createRangeContentRemover(remover, boundaryUpdater) {
-        return function() {
-            assertRangeValid(this);
+                if (!this.canSurroundContents()) {
+                    throw new DOMException("INVALID_STATE_ERR");
+                }
 
-            var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
+                // Extract the contents
+                var content = this.extractContents();
 
-            var iterator = new RangeIterator(this, true);
+                // Clear the children of the node
+                if (node.hasChildNodes()) {
+                    while (node.lastChild) {
+                        node.removeChild(node.lastChild);
+                    }
+                }
 
-            // Work out where to position the range after content removal
-            var node, boundary;
-            if (sc !== root) {
-                node = dom.getClosestAncestorIn(sc, root, true);
-                boundary = getBoundaryAfterNode(node);
-                sc = boundary.node;
-                so = boundary.offset;
-            }
+                // Insert the new node and add the extracted contents
+                insertNodeAtPosition(node, this.startContainer, this.startOffset);
+                node.appendChild(content);
 
-            // Check none of the range is read-only
-            iterateSubtree(iterator, assertNodeNotReadOnly);
+                this.selectNode(node);
+            },
 
-            iterator.reset();
-
-            // Remove the content
-            var returnValue = remover(iterator);
-            iterator.detach();
+            cloneRange: function() {
+                assertRangeValid(this);
+                var range = new Range(getRangeDocument(this));
+                var i = rangeProperties.length, prop;
+                while (i--) {
+                    prop = rangeProperties[i];
+                    range[prop] = this[prop];
+                }
+                return range;
+            },
 
-            // Move to the new position
-            boundaryUpdater(this, sc, so, sc, so);
+            toString: function() {
+                assertRangeValid(this);
+                var sc = this.startContainer;
+                if (sc === this.endContainer && isCharacterDataNode(sc)) {
+                    return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
+                } else {
+                    var textParts = [], iterator = new RangeIterator(this, true);
+                    iterateSubtree(iterator, function(node) {
+                        // Accept only text or CDATA nodes, not comments
+                        if (node.nodeType == 3 || node.nodeType == 4) {
+                            textParts.push(node.data);
+                        }
+                    });
+                    iterator.detach();
+                    return textParts.join("");
+                }
+            },
 
-            return returnValue;
-        };
-    }
+            // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
+            // been removed from Mozilla.
 
-    function createPrototypeRange(constructor, boundaryUpdater, detacher) {
-        function createBeforeAfterNodeSetter(isBefore, isStart) {
-            return function(node) {
-                assertNotDetached(this);
-                assertValidNodeType(node, beforeAfterNodeTypes);
-                assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
+            compareNode: function(node) {
+                assertRangeValid(this);
 
-                var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
-                (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
-            };
-        }
+                var parent = node.parentNode;
+                var nodeIndex = getNodeIndex(node);
 
-        function setRangeStart(range, node, offset) {
-            var ec = range.endContainer, eo = range.endOffset;
-            if (node !== range.startContainer || offset !== range.startOffset) {
-                // Check the root containers of the range and the new boundary, and also check whether the new boundary
-                // is after the current end. In either case, collapse the range to the new position
-                if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
-                    ec = node;
-                    eo = offset;
+                if (!parent) {
+                    throw new DOMException("NOT_FOUND_ERR");
                 }
-                boundaryUpdater(range, node, offset, ec, eo);
-            }
-        }
 
-        function setRangeEnd(range, node, offset) {
-            var sc = range.startContainer, so = range.startOffset;
-            if (node !== range.endContainer || offset !== range.endOffset) {
-                // Check the root containers of the range and the new boundary, and also check whether the new boundary
-                // is after the current end. In either case, collapse the range to the new position
-                if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
-                    sc = node;
-                    so = offset;
+                var startComparison = this.comparePoint(parent, nodeIndex),
+                    endComparison = this.comparePoint(parent, nodeIndex + 1);
+
+                if (startComparison < 0) { // Node starts before
+                    return (endComparison > 0) ? n_b_a : n_b;
+                } else {
+                    return (endComparison > 0) ? n_a : n_i;
                 }
-                boundaryUpdater(range, sc, so, node, offset);
-            }
-        }
+            },
 
-        function setRangeStartAndEnd(range, node, offset) {
-            if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
-                boundaryUpdater(range, node, offset, node, offset);
-            }
-        }
+            comparePoint: function(node, offset) {
+                assertRangeValid(this);
+                assertNode(node, "HIERARCHY_REQUEST_ERR");
+                assertSameDocumentOrFragment(node, this.startContainer);
 
-        constructor.prototype = new RangePrototype();
+                if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
+                    return -1;
+                } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
+                    return 1;
+                }
+                return 0;
+            },
 
-        api.util.extend(constructor.prototype, {
-            setStart: function(node, offset) {
-                assertNotDetached(this);
-                assertNoDocTypeNotationEntityAncestor(node, true);
-                assertValidOffset(node, offset);
+            createContextualFragment: createContextualFragment,
 
-                setRangeStart(this, node, offset);
+            toHtml: function() {
+                return rangeToHtml(this);
             },
 
-            setEnd: function(node, offset) {
-                assertNotDetached(this);
-                assertNoDocTypeNotationEntityAncestor(node, true);
-                assertValidOffset(node, offset);
+            // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
+            // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
+            intersectsNode: function(node, touchingIsIntersecting) {
+                assertRangeValid(this);
+                if (getRootContainer(node) != getRangeRoot(this)) {
+                    return false;
+                }
+
+                var parent = node.parentNode, offset = getNodeIndex(node);
+                if (!parent) {
+                    return true;
+                }
+
+                var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
+                    endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
 
-                setRangeEnd(this, node, offset);
+                return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
             },
 
-            setStartBefore: createBeforeAfterNodeSetter(true, true),
-            setStartAfter: createBeforeAfterNodeSetter(false, true),
-            setEndBefore: createBeforeAfterNodeSetter(true, false),
-            setEndAfter: createBeforeAfterNodeSetter(false, false),
-
-            collapse: function(isStart) {
+            isPointInRange: function(node, offset) {
                 assertRangeValid(this);
-                if (isStart) {
-                    boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
-                } else {
-                    boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
-                }
+                assertNode(node, "HIERARCHY_REQUEST_ERR");
+                assertSameDocumentOrFragment(node, this.startContainer);
+
+                return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
+                       (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
             },
 
-            selectNodeContents: function(node) {
-                // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
-                // could be taken to mean only its children. However, browsers implement this the same as selectNode for
-                // text nodes, so I shall do likewise
-                assertNotDetached(this);
-                assertNoDocTypeNotationEntityAncestor(node, true);
+            // The methods below are non-standard and invented by me.
+
+            // Sharing a boundary start-to-end or end-to-start does not count as intersection.
+            intersectsRange: function(range) {
+                return rangesIntersect(this, range, false);
+            },
 
-                boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
+            // Sharing a boundary start-to-end or end-to-start does count as intersection.
+            intersectsOrTouchesRange: function(range) {
+                return rangesIntersect(this, range, true);
             },
 
-            selectNode: function(node) {
-                assertNotDetached(this);
-                assertNoDocTypeNotationEntityAncestor(node, false);
-                assertValidNodeType(node, beforeAfterNodeTypes);
+            intersection: function(range) {
+                if (this.intersectsRange(range)) {
+                    var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
+                        endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
 
-                var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
-                boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
+                    var intersectionRange = this.cloneRange();
+                    if (startComparison == -1) {
+                        intersectionRange.setStart(range.startContainer, range.startOffset);
+                    }
+                    if (endComparison == 1) {
+                        intersectionRange.setEnd(range.endContainer, range.endOffset);
+                    }
+                    return intersectionRange;
+                }
+                return null;
             },
 
-            extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
+            union: function(range) {
+                if (this.intersectsOrTouchesRange(range)) {
+                    var unionRange = this.cloneRange();
+                    if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
+                        unionRange.setStart(range.startContainer, range.startOffset);
+                    }
+                    if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
+                        unionRange.setEnd(range.endContainer, range.endOffset);
+                    }
+                    return unionRange;
+                } else {
+                    throw new DOMException("Ranges do not intersect");
+                }
+            },
 
-            deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
+            containsNode: function(node, allowPartial) {
+                if (allowPartial) {
+                    return this.intersectsNode(node, false);
+                } else {
+                    return this.compareNode(node) == n_i;
+                }
+            },
 
-            canSurroundContents: function() {
-                assertRangeValid(this);
-                assertNodeNotReadOnly(this.startContainer);
-                assertNodeNotReadOnly(this.endContainer);
+            containsNodeContents: function(node) {
+                return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
+            },
 
-                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
-                // no non-text nodes.
-                var iterator = new RangeIterator(this, true);
-                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
-                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
-                iterator.detach();
-                return !boundariesInvalid;
+            containsRange: function(range) {
+                var intersection = this.intersection(range);
+                return intersection !== null && range.equals(intersection);
             },
 
-            detach: function() {
-                detacher(this);
+            containsNodeText: function(node) {
+                var nodeRange = this.cloneRange();
+                nodeRange.selectNode(node);
+                var textNodes = nodeRange.getNodes([3]);
+                if (textNodes.length > 0) {
+                    nodeRange.setStart(textNodes[0], 0);
+                    var lastTextNode = textNodes.pop();
+                    nodeRange.setEnd(lastTextNode, lastTextNode.length);
+                    return this.containsRange(nodeRange);
+                } else {
+                    return this.containsNodeContents(node);
+                }
             },
 
-            splitBoundaries: function() {
+            getNodes: function(nodeTypes, filter) {
                 assertRangeValid(this);
+                return getNodesInRange(this, nodeTypes, filter);
+            },
 
+            getDocument: function() {
+                return getRangeDocument(this);
+            },
 
-                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
-                var startEndSame = (sc === ec);
+            collapseBefore: function(node) {
+                this.setEndBefore(node);
+                this.collapse(false);
+            },
 
-                if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
-                    dom.splitDataNode(ec, eo);
+            collapseAfter: function(node) {
+                this.setStartAfter(node);
+                this.collapse(true);
+            },
 
+            getBookmark: function(containerNode) {
+                var doc = getRangeDocument(this);
+                var preSelectionRange = api.createRange(doc);
+                containerNode = containerNode || dom.getBody(doc);
+                preSelectionRange.selectNodeContents(containerNode);
+                var range = this.intersection(preSelectionRange);
+                var start = 0, end = 0;
+                if (range) {
+                    preSelectionRange.setEnd(range.startContainer, range.startOffset);
+                    start = preSelectionRange.toString().length;
+                    end = start + range.toString().length;
                 }
 
-                if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
+                return {
+                    start: start,
+                    end: end,
+                    containerNode: containerNode
+                };
+            },
 
-                    sc = dom.splitDataNode(sc, so);
-                    if (startEndSame) {
-                        eo -= so;
-                        ec = sc;
-                    } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
-                        eo++;
+            moveToBookmark: function(bookmark) {
+                var containerNode = bookmark.containerNode;
+                var charIndex = 0;
+                this.setStart(containerNode, 0);
+                this.collapse(true);
+                var nodeStack = [containerNode], node, foundStart = false, stop = false;
+                var nextCharIndex, i, childNodes;
+
+                while (!stop && (node = nodeStack.pop())) {
+                    if (node.nodeType == 3) {
+                        nextCharIndex = charIndex + node.length;
+                        if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
+                            this.setStart(node, bookmark.start - charIndex);
+                            foundStart = true;
+                        }
+                        if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
+                            this.setEnd(node, bookmark.end - charIndex);
+                            stop = true;
+                        }
+                        charIndex = nextCharIndex;
+                    } else {
+                        childNodes = node.childNodes;
+                        i = childNodes.length;
+                        while (i--) {
+                            nodeStack.push(childNodes[i]);
+                        }
                     }
-                    so = 0;
-
                 }
-                boundaryUpdater(this, sc, so, ec, eo);
             },
 
-            normalizeBoundaries: function() {
+            getName: function() {
+                return "DomRange";
+            },
+
+            equals: function(range) {
+                return Range.rangesEqual(this, range);
+            },
+
+            isValid: function() {
+                return isRangeValid(this);
+            },
+
+            inspect: function() {
+                return inspect(this);
+            },
+
+            detach: function() {
+                // In DOM4, detach() is now a no-op.
+            }
+        });
+
+        function copyComparisonConstantsToObject(obj) {
+            obj.START_TO_START = s2s;
+            obj.START_TO_END = s2e;
+            obj.END_TO_END = e2e;
+            obj.END_TO_START = e2s;
+
+            obj.NODE_BEFORE = n_b;
+            obj.NODE_AFTER = n_a;
+            obj.NODE_BEFORE_AND_AFTER = n_b_a;
+            obj.NODE_INSIDE = n_i;
+        }
+
+        function copyComparisonConstants(constructor) {
+            copyComparisonConstantsToObject(constructor);
+            copyComparisonConstantsToObject(constructor.prototype);
+        }
+
+        function createRangeContentRemover(remover, boundaryUpdater) {
+            return function() {
                 assertRangeValid(this);
 
-                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+                var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
+
+                var iterator = new RangeIterator(this, true);
+
+                // Work out where to position the range after content removal
+                var node, boundary;
+                if (sc !== root) {
+                    node = getClosestAncestorIn(sc, root, true);
+                    boundary = getBoundaryAfterNode(node);
+                    sc = boundary.node;
+                    so = boundary.offset;
+                }
+
+                // Check none of the range is read-only
+                iterateSubtree(iterator, assertNodeNotReadOnly);
 
-                var mergeForward = function(node) {
-                    var sibling = node.nextSibling;
-                    if (sibling && sibling.nodeType == node.nodeType) {
+                iterator.reset();
+
+                // Remove the content
+                var returnValue = remover(iterator);
+                iterator.detach();
+
+                // Move to the new position
+                boundaryUpdater(this, sc, so, sc, so);
+
+                return returnValue;
+            };
+        }
+
+        function createPrototypeRange(constructor, boundaryUpdater) {
+            function createBeforeAfterNodeSetter(isBefore, isStart) {
+                return function(node) {
+                    assertValidNodeType(node, beforeAfterNodeTypes);
+                    assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
+
+                    var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
+                    (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
+                };
+            }
+
+            function setRangeStart(range, node, offset) {
+                var ec = range.endContainer, eo = range.endOffset;
+                if (node !== range.startContainer || offset !== range.startOffset) {
+                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
+                    // is after the current end. In either case, collapse the range to the new position
+                    if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
                         ec = node;
-                        eo = node.length;
-                        node.appendData(sibling.data);
-                        sibling.parentNode.removeChild(sibling);
+                        eo = offset;
                     }
-                };
+                    boundaryUpdater(range, node, offset, ec, eo);
+                }
+            }
 
-                var mergeBackward = function(node) {
-                    var sibling = node.previousSibling;
-                    if (sibling && sibling.nodeType == node.nodeType) {
+            function setRangeEnd(range, node, offset) {
+                var sc = range.startContainer, so = range.startOffset;
+                if (node !== range.endContainer || offset !== range.endOffset) {
+                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
+                    // is after the current end. In either case, collapse the range to the new position
+                    if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
                         sc = node;
-                        var nodeLength = node.length;
-                        so = sibling.length;
-                        node.insertData(0, sibling.data);
-                        sibling.parentNode.removeChild(sibling);
-                        if (sc == ec) {
-                            eo += so;
-                            ec = sc;
-                        } else if (ec == node.parentNode) {
-                            var nodeIndex = dom.getNodeIndex(node);
-                            if (eo == nodeIndex) {
-                                ec = node;
-                                eo = nodeLength;
-                            } else if (eo > nodeIndex) {
-                                eo--;
+                        so = offset;
+                    }
+                    boundaryUpdater(range, sc, so, node, offset);
+                }
+            }
+
+            // Set up inheritance
+            var F = function() {};
+            F.prototype = api.rangePrototype;
+            constructor.prototype = new F();
+
+            util.extend(constructor.prototype, {
+                setStart: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
+
+                    setRangeStart(this, node, offset);
+                },
+
+                setEnd: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
+
+                    setRangeEnd(this, node, offset);
+                },
+
+                /**
+                 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
+                 * - Two parameters (node, offset) creates a collapsed range at that position
+                 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
+                 *   startOffset and ending at endOffset
+                 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
+                 *   startNode and ending at endOffset in endNode
+                 */
+                setStartAndEnd: function() {
+                    var args = arguments;
+                    var sc = args[0], so = args[1], ec = sc, eo = so;
+
+                    switch (args.length) {
+                        case 3:
+                            eo = args[2];
+                            break;
+                        case 4:
+                            ec = args[2];
+                            eo = args[3];
+                            break;
+                    }
+
+                    boundaryUpdater(this, sc, so, ec, eo);
+                },
+
+                setBoundary: function(node, offset, isStart) {
+                    this["set" + (isStart ? "Start" : "End")](node, offset);
+                },
+
+                setStartBefore: createBeforeAfterNodeSetter(true, true),
+                setStartAfter: createBeforeAfterNodeSetter(false, true),
+                setEndBefore: createBeforeAfterNodeSetter(true, false),
+                setEndAfter: createBeforeAfterNodeSetter(false, false),
+
+                collapse: function(isStart) {
+                    assertRangeValid(this);
+                    if (isStart) {
+                        boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
+                    } else {
+                        boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
+                    }
+                },
+
+                selectNodeContents: function(node) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+
+                    boundaryUpdater(this, node, 0, node, getNodeLength(node));
+                },
+
+                selectNode: function(node) {
+                    assertNoDocTypeNotationEntityAncestor(node, false);
+                    assertValidNodeType(node, beforeAfterNodeTypes);
+
+                    var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
+                    boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
+                },
+
+                extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
+
+                deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
+
+                canSurroundContents: function() {
+                    assertRangeValid(this);
+                    assertNodeNotReadOnly(this.startContainer);
+                    assertNodeNotReadOnly(this.endContainer);
+
+                    // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
+                    // no non-text nodes.
+                    var iterator = new RangeIterator(this, true);
+                    var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
+                            (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
+                    iterator.detach();
+                    return !boundariesInvalid;
+                },
+
+                splitBoundaries: function() {
+                    splitRangeBoundaries(this);
+                },
+
+                splitBoundariesPreservingPositions: function(positionsToPreserve) {
+                    splitRangeBoundaries(this, positionsToPreserve);
+                },
+
+                normalizeBoundaries: function() {
+                    assertRangeValid(this);
+
+                    var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
+
+                    var mergeForward = function(node) {
+                        var sibling = node.nextSibling;
+                        if (sibling && sibling.nodeType == node.nodeType) {
+                            ec = node;
+                            eo = node.length;
+                            node.appendData(sibling.data);
+                            removeNode(sibling);
+                        }
+                    };
+
+                    var mergeBackward = function(node) {
+                        var sibling = node.previousSibling;
+                        if (sibling && sibling.nodeType == node.nodeType) {
+                            sc = node;
+                            var nodeLength = node.length;
+                            so = sibling.length;
+                            node.insertData(0, sibling.data);
+                            removeNode(sibling);
+                            if (sc == ec) {
+                                eo += so;
+                                ec = sc;
+                            } else if (ec == node.parentNode) {
+                                var nodeIndex = getNodeIndex(node);
+                                if (eo == nodeIndex) {
+                                    ec = node;
+                                    eo = nodeLength;
+                                } else if (eo > nodeIndex) {
+                                    eo--;
+                                }
+                            }
+                        }
+                    };
+
+                    var normalizeStart = true;
+                    var sibling;
+
+                    if (isCharacterDataNode(ec)) {
+                        if (eo == ec.length) {
+                            mergeForward(ec);
+                        } else if (eo == 0) {
+                            sibling = ec.previousSibling;
+                            if (sibling && sibling.nodeType == ec.nodeType) {
+                                eo = sibling.length;
+                                if (sc == ec) {
+                                    normalizeStart = false;
+                                }
+                                sibling.appendData(ec.data);
+                                removeNode(ec);
+                                ec = sibling;
+                            }
+                        }
+                    } else {
+                        if (eo > 0) {
+                            var endNode = ec.childNodes[eo - 1];
+                            if (endNode && isCharacterDataNode(endNode)) {
+                                mergeForward(endNode);
                             }
                         }
+                        normalizeStart = !this.collapsed;
                     }
-                };
 
-                var normalizeStart = true;
+                    if (normalizeStart) {
+                        if (isCharacterDataNode(sc)) {
+                            if (so == 0) {
+                                mergeBackward(sc);
+                            } else if (so == sc.length) {
+                                sibling = sc.nextSibling;
+                                if (sibling && sibling.nodeType == sc.nodeType) {
+                                    if (ec == sibling) {
+                                        ec = sc;
+                                        eo += sc.length;
+                                    }
+                                    sc.appendData(sibling.data);
+                                    removeNode(sibling);
+                                }
+                            }
+                        } else {
+                            if (so < sc.childNodes.length) {
+                                var startNode = sc.childNodes[so];
+                                if (startNode && isCharacterDataNode(startNode)) {
+                                    mergeBackward(startNode);
+                                }
+                            }
+                        }
+                    } else {
+                        sc = ec;
+                        so = eo;
+                    }
+
+                    boundaryUpdater(this, sc, so, ec, eo);
+                },
+
+                collapseToPoint: function(node, offset) {
+                    assertNoDocTypeNotationEntityAncestor(node, true);
+                    assertValidOffset(node, offset);
+                    this.setStartAndEnd(node, offset);
+                }
+            });
+
+            copyComparisonConstants(constructor);
+        }
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        // Updates commonAncestorContainer and collapsed after boundary change
+        function updateCollapsedAndCommonAncestor(range) {
+            range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
+            range.commonAncestorContainer = range.collapsed ?
+                range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
+        }
+
+        function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
+            range.startContainer = startContainer;
+            range.startOffset = startOffset;
+            range.endContainer = endContainer;
+            range.endOffset = endOffset;
+            range.document = dom.getDocument(startContainer);
+
+            updateCollapsedAndCommonAncestor(range);
+        }
+
+        function Range(doc) {
+            this.startContainer = doc;
+            this.startOffset = 0;
+            this.endContainer = doc;
+            this.endOffset = 0;
+            this.document = doc;
+            updateCollapsedAndCommonAncestor(this);
+        }
+
+        createPrototypeRange(Range, updateBoundaries);
+
+        util.extend(Range, {
+            rangeProperties: rangeProperties,
+            RangeIterator: RangeIterator,
+            copyComparisonConstants: copyComparisonConstants,
+            createPrototypeRange: createPrototypeRange,
+            inspect: inspect,
+            toHtml: rangeToHtml,
+            getRangeDocument: getRangeDocument,
+            rangesEqual: function(r1, r2) {
+                return r1.startContainer === r2.startContainer &&
+                    r1.startOffset === r2.startOffset &&
+                    r1.endContainer === r2.endContainer &&
+                    r1.endOffset === r2.endOffset;
+            }
+        });
 
-                if (dom.isCharacterDataNode(ec)) {
-                    if (ec.length == eo) {
-                        mergeForward(ec);
+        api.DomRange = Range;
+    });\r
+\r
+    /*----------------------------------------------------------------------------------------------------------------*/\r
+\r
+    // Wrappers for the browser's native DOM Range and/or TextRange implementation
+    api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
+        var WrappedRange, WrappedTextRange;
+        var dom = api.dom;
+        var util = api.util;
+        var DomPosition = dom.DomPosition;
+        var DomRange = api.DomRange;
+        var getBody = dom.getBody;
+        var getContentDocument = dom.getContentDocument;
+        var isCharacterDataNode = dom.isCharacterDataNode;
+
+
+        /*----------------------------------------------------------------------------------------------------------------*/
+
+        if (api.features.implementsDomRange) {
+            // This is a wrapper around the browser's native DOM Range. It has two aims:
+            // - Provide workarounds for specific browser bugs
+            // - provide convenient extensions, which are inherited from Rangy's DomRange
+
+            (function() {
+                var rangeProto;
+                var rangeProperties = DomRange.rangeProperties;
+
+                function updateRangeProperties(range) {
+                    var i = rangeProperties.length, prop;
+                    while (i--) {
+                        prop = rangeProperties[i];
+                        range[prop] = range.nativeRange[prop];
                     }
-                } else {
-                    if (eo > 0) {
-                        var endNode = ec.childNodes[eo - 1];
-                        if (endNode && dom.isCharacterDataNode(endNode)) {
-                            mergeForward(endNode);
-                        }
+                    // Fix for broken collapsed property in IE 9.
+                    range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
+                }
+
+                function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
+                    var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
+                    var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
+                    var nativeRangeDifferent = !range.equals(range.nativeRange);
+
+                    // Always set both boundaries for the benefit of IE9 (see issue 35)
+                    if (startMoved || endMoved || nativeRangeDifferent) {
+                        range.setEnd(endContainer, endOffset);
+                        range.setStart(startContainer, startOffset);
                     }
-                    normalizeStart = !this.collapsed;
                 }
 
-                if (normalizeStart) {
-                    if (dom.isCharacterDataNode(sc)) {
-                        if (so == 0) {
-                            mergeBackward(sc);
+                var createBeforeAfterNodeSetter;
+
+                WrappedRange = function(range) {
+                    if (!range) {
+                        throw module.createError("WrappedRange: Range must be specified");
+                    }
+                    this.nativeRange = range;
+                    updateRangeProperties(this);
+                };
+
+                DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
+
+                rangeProto = WrappedRange.prototype;
+
+                rangeProto.selectNode = function(node) {
+                    this.nativeRange.selectNode(node);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.cloneContents = function() {
+                    return this.nativeRange.cloneContents();
+                };
+
+                // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
+                // insertNode() is never delegated to the native range.
+
+                rangeProto.surroundContents = function(node) {
+                    this.nativeRange.surroundContents(node);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.collapse = function(isStart) {
+                    this.nativeRange.collapse(isStart);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.cloneRange = function() {
+                    return new WrappedRange(this.nativeRange.cloneRange());
+                };
+
+                rangeProto.refresh = function() {
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.toString = function() {
+                    return this.nativeRange.toString();
+                };
+
+                // Create test range and node for feature detection
+
+                var testTextNode = document.createTextNode("test");
+                getBody(document).appendChild(testTextNode);
+                var range = document.createRange();
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
+                // correct for it
+
+                range.setStart(testTextNode, 0);
+                range.setEnd(testTextNode, 0);
+
+                try {
+                    range.setStart(testTextNode, 1);
+
+                    rangeProto.setStart = function(node, offset) {
+                        this.nativeRange.setStart(node, offset);
+                        updateRangeProperties(this);
+                    };
+
+                    rangeProto.setEnd = function(node, offset) {
+                        this.nativeRange.setEnd(node, offset);
+                        updateRangeProperties(this);
+                    };
+
+                    createBeforeAfterNodeSetter = function(name) {
+                        return function(node) {
+                            this.nativeRange[name](node);
+                            updateRangeProperties(this);
+                        };
+                    };
+
+                } catch(ex) {
+
+                    rangeProto.setStart = function(node, offset) {
+                        try {
+                            this.nativeRange.setStart(node, offset);
+                        } catch (ex) {
+                            this.nativeRange.setEnd(node, offset);
+                            this.nativeRange.setStart(node, offset);
                         }
-                    } else {
-                        if (so < sc.childNodes.length) {
-                            var startNode = sc.childNodes[so];
-                            if (startNode && dom.isCharacterDataNode(startNode)) {
-                                mergeBackward(startNode);
+                        updateRangeProperties(this);
+                    };
+
+                    rangeProto.setEnd = function(node, offset) {
+                        try {
+                            this.nativeRange.setEnd(node, offset);
+                        } catch (ex) {
+                            this.nativeRange.setStart(node, offset);
+                            this.nativeRange.setEnd(node, offset);
+                        }
+                        updateRangeProperties(this);
+                    };
+
+                    createBeforeAfterNodeSetter = function(name, oppositeName) {
+                        return function(node) {
+                            try {
+                                this.nativeRange[name](node);
+                            } catch (ex) {
+                                this.nativeRange[oppositeName](node);
+                                this.nativeRange[name](node);
                             }
+                            updateRangeProperties(this);
+                        };
+                    };
+                }
+
+                rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
+                rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
+                rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
+                rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
+                // whether the native implementation can be trusted
+                rangeProto.selectNodeContents = function(node) {
+                    this.setStartAndEnd(node, 0, dom.getNodeLength(node));
+                };
+
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
+                // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
+
+                range.selectNodeContents(testTextNode);
+                range.setEnd(testTextNode, 3);
+
+                var range2 = document.createRange();
+                range2.selectNodeContents(testTextNode);
+                range2.setEnd(testTextNode, 4);
+                range2.setStart(testTextNode, 2);
+
+                if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
+                        range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
+                    // This is the wrong way round, so correct for it
+
+                    rangeProto.compareBoundaryPoints = function(type, range) {
+                        range = range.nativeRange || range;
+                        if (type == range.START_TO_END) {
+                            type = range.END_TO_START;
+                        } else if (type == range.END_TO_START) {
+                            type = range.START_TO_END;
                         }
-                    }
+                        return this.nativeRange.compareBoundaryPoints(type, range);
+                    };
                 } else {
-                    sc = ec;
-                    so = eo;
+                    rangeProto.compareBoundaryPoints = function(type, range) {
+                        return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
+                    };
                 }
 
-                boundaryUpdater(this, sc, so, ec, eo);
-            },
+                /*--------------------------------------------------------------------------------------------------------*/
+
+                // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
+
+                var el = document.createElement("div");
+                el.innerHTML = "123";
+                var textNode = el.firstChild;
+                var body = getBody(document);
+                body.appendChild(el);
+
+                range.setStart(textNode, 1);
+                range.setEnd(textNode, 2);
+                range.deleteContents();
+
+                if (textNode.data == "13") {
+                    // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
+                    // extractContents()
+                    rangeProto.deleteContents = function() {
+                        this.nativeRange.deleteContents();
+                        updateRangeProperties(this);
+                    };
+
+                    rangeProto.extractContents = function() {
+                        var frag = this.nativeRange.extractContents();
+                        updateRangeProperties(this);
+                        return frag;
+                    };
+                } else {
+                }
 
-            collapseToPoint: function(node, offset) {
-                assertNotDetached(this);
+                body.removeChild(el);
+                body = null;
 
-                assertNoDocTypeNotationEntityAncestor(node, true);
-                assertValidOffset(node, offset);
+                /*--------------------------------------------------------------------------------------------------------*/
 
-                setRangeStartAndEnd(this, node, offset);
-            }
-        });
+                // Test for existence of createContextualFragment and delegate to it if it exists
+                if (util.isHostMethod(range, "createContextualFragment")) {
+                    rangeProto.createContextualFragment = function(fragmentStr) {
+                        return this.nativeRange.createContextualFragment(fragmentStr);
+                    };
+                }
 
-        copyComparisonConstants(constructor);
-    }
-
-    /*----------------------------------------------------------------------------------------------------------------*/
-
-    // Updates commonAncestorContainer and collapsed after boundary change
-    function updateCollapsedAndCommonAncestor(range) {
-        range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
-        range.commonAncestorContainer = range.collapsed ?
-            range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
-    }
-
-    function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
-        var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
-        var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
-
-        range.startContainer = startContainer;
-        range.startOffset = startOffset;
-        range.endContainer = endContainer;
-        range.endOffset = endOffset;
-
-        updateCollapsedAndCommonAncestor(range);
-        dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
-    }
-
-    function detach(range) {
-        assertNotDetached(range);
-        range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
-        range.collapsed = range.commonAncestorContainer = null;
-        dispatchEvent(range, "detach", null);
-        range._listeners = null;
-    }
-
-    /**
-     * @constructor
-     */
-    function Range(doc) {
-        this.startContainer = doc;
-        this.startOffset = 0;
-        this.endContainer = doc;
-        this.endOffset = 0;
-        this._listeners = {
-            boundarychange: [],
-            detach: []
-        };
-        updateCollapsedAndCommonAncestor(this);
-    }
+                /*--------------------------------------------------------------------------------------------------------*/
 
-    createPrototypeRange(Range, updateBoundaries, detach);
+                // Clean up
+                getBody(document).removeChild(testTextNode);
 
-    api.rangePrototype = RangePrototype.prototype;
+                rangeProto.getName = function() {
+                    return "WrappedRange";
+                };
 
-    Range.rangeProperties = rangeProperties;
-    Range.RangeIterator = RangeIterator;
-    Range.copyComparisonConstants = copyComparisonConstants;
-    Range.createPrototypeRange = createPrototypeRange;
-    Range.inspect = inspect;
-    Range.getRangeDocument = getRangeDocument;
-    Range.rangesEqual = function(r1, r2) {
-        return r1.startContainer === r2.startContainer &&
-               r1.startOffset === r2.startOffset &&
-               r1.endContainer === r2.endContainer &&
-               r1.endOffset === r2.endOffset;
-    };
+                api.WrappedRange = WrappedRange;
 
-    api.DomRange = Range;
-    api.RangeException = RangeException;
-});rangy.createModule("WrappedRange", function(api, module) {\r
-    api.requireModules( ["DomUtil", "DomRange"] );\r
-\r
-    /**\r
-     * @constructor\r
-     */\r
-    var WrappedRange;\r
-    var dom = api.dom;\r
-    var DomPosition = dom.DomPosition;\r
-    var DomRange = api.DomRange;\r
-\r
-\r
-\r
-    /*----------------------------------------------------------------------------------------------------------------*/\r
-\r
-    /*\r
-    This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()\r
-    method. For example, in the following (where pipes denote the selection boundaries):\r
-\r
-    <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>\r
-\r
-    var range = document.selection.createRange();\r
-    alert(range.parentElement().id); // Should alert "ul" but alerts "b"\r
-\r
-    This method returns the common ancestor node of the following:\r
-    - the parentElement() of the textRange\r
-    - the parentElement() of the textRange after calling collapse(true)\r
-    - the parentElement() of the textRange after calling collapse(false)\r
-     */\r
-    function getTextRangeContainerElement(textRange) {\r
-        var parentEl = textRange.parentElement();\r
-\r
-        var range = textRange.duplicate();\r
-        range.collapse(true);\r
-        var startEl = range.parentElement();\r
-        range = textRange.duplicate();\r
-        range.collapse(false);\r
-        var endEl = range.parentElement();\r
-        var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);\r
-\r
-        return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);\r
-    }\r
-\r
-    function textRangeIsCollapsed(textRange) {\r
-        return textRange.compareEndPoints("StartToEnd", textRange) == 0;\r
-    }\r
-\r
-    // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as\r
-    // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has\r
-    // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling\r
-    // for inputs and images, plus optimizations.\r
-    function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {\r
-        var workingRange = textRange.duplicate();\r
-\r
-        workingRange.collapse(isStart);\r
-        var containerElement = workingRange.parentElement();\r
-\r
-        // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so\r
-        // check for that\r
-        // TODO: Find out when. Workaround for wholeRangeContainerElement may break this\r
-        if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {\r
-            containerElement = wholeRangeContainerElement;\r
-\r
-        }\r
-\r
-\r
-\r
-        // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and\r
-        // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx\r
-        if (!containerElement.canHaveHTML) {\r
-            return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));\r
-        }\r
-\r
-        var workingNode = dom.getDocument(containerElement).createElement("span");\r
-        // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
-        // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
-        if (workingNode.parentNode) {
-            workingNode.parentNode.removeChild(workingNode);
-        }
-        var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";\r
-        var previousNode, nextNode, boundaryPosition, boundaryNode;\r
-\r
-        // Move the working range through the container's children, starting at the end and working backwards, until the\r
-        // working range reaches or goes past the boundary we're interested in\r
-        do {\r
-            containerElement.insertBefore(workingNode, workingNode.previousSibling);\r
-            workingRange.moveToElementText(workingNode);\r
-        } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&\r
-                workingNode.previousSibling);\r
-\r
-        // We've now reached or gone past the boundary of the text range we're interested in\r
-        // so have identified the node we want\r
-        boundaryNode = workingNode.nextSibling;\r
-\r
-        if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {\r
-            // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the\r
-            // node containing the text range's boundary, so we move the end of the working range to the boundary point\r
-            // and measure the length of its text to get the boundary's offset within the node.\r
-            workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);\r
-\r
-\r
-            var offset;\r
-\r
-            if (/[\r\n]/.test(boundaryNode.data)) {\r
-                /*\r
-                For the particular case of a boundary within a text node containing line breaks (within a <pre> element,\r
-                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:\r
-\r
-                - Each line break is represented as \r in the text node's data/nodeValue properties\r
-                - Each line break is represented as \r\n in the TextRange's 'text' property\r
-                - The 'text' property of the TextRange does not contain trailing line breaks\r
-\r
-                To get round the problem presented by the final fact above, we can use the fact that TextRange's\r
-                moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily\r
-                the same as the number of characters it was instructed to move. The simplest approach is to use this to\r
-                store the characters moved when moving both the start and end of the range to the start of the document\r
-                body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).\r
-                However, this is extremely slow when the document is large and the range is near the end of it. Clearly\r
-                doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same\r
-                problem.\r
-\r
-                Another approach that works is to use moveStart() to move the start boundary of the range up to the end\r
-                boundary one character at a time and incrementing a counter with the value returned by the moveStart()\r
-                call. However, the check for whether the start boundary has reached the end boundary is expensive, so\r
-                this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of\r
-                the range within the document).\r
-\r
-                The method below is a hybrid of the two methods above. It uses the fact that a string containing the\r
-                TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the\r
-                text of the TextRange, so the start of the range is moved that length initially and then a character at\r
-                a time to make up for any trailing line breaks not contained in the 'text' property. This has good\r
-                performance in most situations compared to the previous two methods.\r
-                */\r
-                var tempRange = workingRange.duplicate();\r
-                var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;\r
-\r
-                offset = tempRange.moveStart("character", rangeLength);\r
-                while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {\r
-                    offset++;\r
-                    tempRange.moveStart("character", 1);\r
-                }\r
-            } else {\r
-                offset = workingRange.text.length;\r
-            }\r
-            boundaryPosition = new DomPosition(boundaryNode, offset);\r
-        } else {\r
-\r
-\r
-            // If the boundary immediately follows a character data node and this is the end boundary, we should favour\r
-            // a position within that, and likewise for a start boundary preceding a character data node\r
-            previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;\r
-            nextNode = (isCollapsed || isStart) && workingNode.nextSibling;\r
-\r
-\r
-\r
-            if (nextNode && dom.isCharacterDataNode(nextNode)) {\r
-                boundaryPosition = new DomPosition(nextNode, 0);\r
-            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {\r
-                boundaryPosition = new DomPosition(previousNode, previousNode.length);\r
-            } else {\r
-                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));\r
-            }\r
-        }\r
-\r
-        // Clean up\r
-        workingNode.parentNode.removeChild(workingNode);\r
-\r
-        return boundaryPosition;\r
-    }\r
-\r
-    // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.\r
-    // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange\r
-    // (http://code.google.com/p/ierange/)\r
-    function createBoundaryTextRange(boundaryPosition, isStart) {\r
-        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;\r
-        var doc = dom.getDocument(boundaryPosition.node);\r
-        var workingNode, childNodes, workingRange = doc.body.createTextRange();\r
-        var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);\r
-\r
-        if (nodeIsDataNode) {\r
-            boundaryNode = boundaryPosition.node;\r
-            boundaryParent = boundaryNode.parentNode;\r
-        } else {\r
-            childNodes = boundaryPosition.node.childNodes;\r
-            boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;\r
-            boundaryParent = boundaryPosition.node;\r
-        }\r
-\r
-        // Position the range immediately before the node containing the boundary\r
-        workingNode = doc.createElement("span");\r
-\r
-        // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the\r
-        // element rather than immediately before or after it, which is what we want\r
-        workingNode.innerHTML = "&#feff;";\r
-\r
-        // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report\r
-        // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12\r
-        if (boundaryNode) {\r
-            boundaryParent.insertBefore(workingNode, boundaryNode);\r
-        } else {\r
-            boundaryParent.appendChild(workingNode);\r
-        }\r
-\r
-        workingRange.moveToElementText(workingNode);\r
-        workingRange.collapse(!isStart);\r
-\r
-        // Clean up\r
-        boundaryParent.removeChild(workingNode);\r
-\r
-        // Move the working range to the text offset, if required\r
-        if (nodeIsDataNode) {\r
-            workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);\r
-        }\r
-\r
-        return workingRange;\r
-    }\r
-\r
-    /*----------------------------------------------------------------------------------------------------------------*/\r
-\r
-    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {\r
-        // This is a wrapper around the browser's native DOM Range. It has two aims:\r
-        // - Provide workarounds for specific browser bugs\r
-        // - provide convenient extensions, which are inherited from Rangy's DomRange\r
-\r
-        (function() {\r
-            var rangeProto;\r
-            var rangeProperties = DomRange.rangeProperties;\r
-            var canSetRangeStartAfterEnd;\r
-\r
-            function updateRangeProperties(range) {\r
-                var i = rangeProperties.length, prop;\r
-                while (i--) {\r
-                    prop = rangeProperties[i];\r
-                    range[prop] = range.nativeRange[prop];\r
-                }\r
-            }\r
-\r
-            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {\r
-                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);\r
-                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);\r
-\r
-                // Always set both boundaries for the benefit of IE9 (see issue 35)\r
-                if (startMoved || endMoved) {\r
-                    range.setEnd(endContainer, endOffset);\r
-                    range.setStart(startContainer, startOffset);\r
-                }\r
-            }\r
-\r
-            function detach(range) {\r
-                range.nativeRange.detach();\r
-                range.detached = true;\r
-                var i = rangeProperties.length, prop;\r
-                while (i--) {\r
-                    prop = rangeProperties[i];\r
-                    range[prop] = null;\r
-                }\r
-            }\r
-\r
-            var createBeforeAfterNodeSetter;\r
-\r
-            WrappedRange = function(range) {\r
-                if (!range) {\r
-                    throw new Error("Range must be specified");\r
-                }\r
-                this.nativeRange = range;\r
-                updateRangeProperties(this);\r
-            };\r
-\r
-            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);\r
-\r
-            rangeProto = WrappedRange.prototype;\r
-\r
-            rangeProto.selectNode = function(node) {\r
-                this.nativeRange.selectNode(node);\r
-                updateRangeProperties(this);\r
-            };\r
-\r
-            rangeProto.deleteContents = function() {\r
-                this.nativeRange.deleteContents();\r
-                updateRangeProperties(this);\r
-            };\r
-\r
-            rangeProto.extractContents = function() {\r
-                var frag = this.nativeRange.extractContents();\r
-                updateRangeProperties(this);\r
-                return frag;\r
-            };\r
-\r
-            rangeProto.cloneContents = function() {\r
-                return this.nativeRange.cloneContents();\r
-            };\r
-\r
-            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still\r
-            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for\r
-            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of\r
-            // insertNode, which works but is almost certainly slower than the native implementation.\r
-/*\r
-            rangeProto.insertNode = function(node) {\r
-                this.nativeRange.insertNode(node);\r
-                updateRangeProperties(this);\r
-            };\r
-*/\r
-\r
-            rangeProto.surroundContents = function(node) {\r
-                this.nativeRange.surroundContents(node);\r
-                updateRangeProperties(this);\r
-            };\r
-\r
-            rangeProto.collapse = function(isStart) {\r
-                this.nativeRange.collapse(isStart);\r
-                updateRangeProperties(this);\r
-            };\r
-\r
-            rangeProto.cloneRange = function() {\r
-                return new WrappedRange(this.nativeRange.cloneRange());\r
-            };\r
-\r
-            rangeProto.refresh = function() {\r
-                updateRangeProperties(this);\r
-            };\r
-\r
-            rangeProto.toString = function() {\r
-                return this.nativeRange.toString();\r
-            };\r
-\r
-            // Create test range and node for feature detection\r
-\r
-            var testTextNode = document.createTextNode("test");\r
-            dom.getBody(document).appendChild(testTextNode);\r
-            var range = document.createRange();\r
-\r
-            /*--------------------------------------------------------------------------------------------------------*/\r
-\r
-            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and\r
-            // correct for it\r
-\r
-            range.setStart(testTextNode, 0);\r
-            range.setEnd(testTextNode, 0);\r
-\r
-            try {\r
-                range.setStart(testTextNode, 1);\r
-                canSetRangeStartAfterEnd = true;\r
-\r
-                rangeProto.setStart = function(node, offset) {\r
-                    this.nativeRange.setStart(node, offset);\r
-                    updateRangeProperties(this);\r
-                };\r
-\r
-                rangeProto.setEnd = function(node, offset) {\r
-                    this.nativeRange.setEnd(node, offset);\r
-                    updateRangeProperties(this);\r
-                };\r
-\r
-                createBeforeAfterNodeSetter = function(name) {\r
-                    return function(node) {\r
-                        this.nativeRange[name](node);\r
-                        updateRangeProperties(this);\r
-                    };\r
-                };\r
-\r
-            } catch(ex) {\r
-\r
-\r
-                canSetRangeStartAfterEnd = false;\r
-\r
-                rangeProto.setStart = function(node, offset) {\r
-                    try {\r
-                        this.nativeRange.setStart(node, offset);\r
-                    } catch (ex) {\r
-                        this.nativeRange.setEnd(node, offset);\r
-                        this.nativeRange.setStart(node, offset);\r
-                    }\r
-                    updateRangeProperties(this);\r
-                };\r
-\r
-                rangeProto.setEnd = function(node, offset) {\r
-                    try {\r
-                        this.nativeRange.setEnd(node, offset);\r
-                    } catch (ex) {\r
-                        this.nativeRange.setStart(node, offset);\r
-                        this.nativeRange.setEnd(node, offset);\r
-                    }\r
-                    updateRangeProperties(this);\r
-                };\r
-\r
-                createBeforeAfterNodeSetter = function(name, oppositeName) {\r
-                    return function(node) {\r
-                        try {\r
-                            this.nativeRange[name](node);\r
-                        } catch (ex) {\r
-                            this.nativeRange[oppositeName](node);\r
-                            this.nativeRange[name](node);\r
-                        }\r
-                        updateRangeProperties(this);\r
-                    };\r
-                };\r
-            }\r
-\r
-            rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");\r
-            rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");\r
-            rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");\r
-            rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");\r
-\r
-            /*--------------------------------------------------------------------------------------------------------*/\r
-\r
-            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to\r
-            // the 0th character of the text node\r
-            range.selectNodeContents(testTextNode);\r
-            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&\r
-                    range.startOffset == 0 && range.endOffset == testTextNode.length) {\r
-                rangeProto.selectNodeContents = function(node) {\r
-                    this.nativeRange.selectNodeContents(node);\r
-                    updateRangeProperties(this);\r
-                };\r
-            } else {\r
-                rangeProto.selectNodeContents = function(node) {\r
-                    this.setStart(node, 0);\r
-                    this.setEnd(node, DomRange.getEndOffset(node));\r
-                };\r
-            }\r
-\r
-            /*--------------------------------------------------------------------------------------------------------*/\r
-\r
-            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants\r
-            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738\r
-\r
-            range.selectNodeContents(testTextNode);\r
-            range.setEnd(testTextNode, 3);\r
-\r
-            var range2 = document.createRange();\r
-            range2.selectNodeContents(testTextNode);\r
-            range2.setEnd(testTextNode, 4);\r
-            range2.setStart(testTextNode, 2);\r
-\r
-            if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &\r
-                    range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {\r
-                // This is the wrong way round, so correct for it\r
-\r
-\r
-                rangeProto.compareBoundaryPoints = function(type, range) {\r
-                    range = range.nativeRange || range;\r
-                    if (type == range.START_TO_END) {\r
-                        type = range.END_TO_START;\r
-                    } else if (type == range.END_TO_START) {\r
-                        type = range.START_TO_END;\r
-                    }\r
-                    return this.nativeRange.compareBoundaryPoints(type, range);\r
-                };\r
-            } else {\r
-                rangeProto.compareBoundaryPoints = function(type, range) {\r
-                    return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);\r
-                };\r
-            }\r
-\r
-            /*--------------------------------------------------------------------------------------------------------*/\r
-\r
-            // Test for existence of createContextualFragment and delegate to it if it exists\r
-            if (api.util.isHostMethod(range, "createContextualFragment")) {\r
-                rangeProto.createContextualFragment = function(fragmentStr) {\r
-                    return this.nativeRange.createContextualFragment(fragmentStr);\r
-                };\r
-            }\r
-\r
-            /*--------------------------------------------------------------------------------------------------------*/\r
-\r
-            // Clean up\r
-            dom.getBody(document).removeChild(testTextNode);\r
-            range.detach();\r
-            range2.detach();\r
-        })();\r
-\r
-        api.createNativeRange = function(doc) {\r
-            doc = doc || document;\r
-            return doc.createRange();\r
-        };\r
-    } else if (api.features.implementsTextRange) {\r
-        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a\r
-        // prototype\r
-\r
-        WrappedRange = function(textRange) {\r
-            this.textRange = textRange;\r
-            this.refresh();\r
-        };\r
-\r
-        WrappedRange.prototype = new DomRange(document);\r
-\r
-        WrappedRange.prototype.refresh = function() {\r
-            var start, end;\r
-\r
-            // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.\r
-            var rangeContainerElement = getTextRangeContainerElement(this.textRange);\r
-\r
-            if (textRangeIsCollapsed(this.textRange)) {\r
-                end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);\r
-            } else {\r
-\r
-                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);\r
-                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);\r
-            }\r
-\r
-            this.setStart(start.node, start.offset);\r
-            this.setEnd(end.node, end.offset);\r
-        };\r
-\r
-        DomRange.copyComparisonConstants(WrappedRange);\r
-\r
-        // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work\r
-        var globalObj = (function() { return this; })();\r
-        if (typeof globalObj.Range == "undefined") {\r
-            globalObj.Range = WrappedRange;\r
-        }\r
-\r
-        api.createNativeRange = function(doc) {\r
-            doc = doc || document;\r
-            return doc.body.createTextRange();\r
-        };\r
-    }\r
-\r
-    if (api.features.implementsTextRange) {\r
-        WrappedRange.rangeToTextRange = function(range) {\r
-            if (range.collapsed) {\r
-                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
-\r
-\r
-\r
-                return tr;\r
-\r
-                //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
-            } else {\r
-                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);\r
-                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);\r
-                var textRange = dom.getDocument(range.startContainer).body.createTextRange();\r
-                textRange.setEndPoint("StartToStart", startRange);\r
-                textRange.setEndPoint("EndToEnd", endRange);\r
-                return textRange;\r
-            }\r
-        };\r
-    }\r
-\r
-    WrappedRange.prototype.getName = function() {\r
-        return "WrappedRange";\r
-    };\r
-\r
-    api.WrappedRange = WrappedRange;\r
-\r
-    api.createRange = function(doc) {\r
-        doc = doc || document;\r
-        return new WrappedRange(api.createNativeRange(doc));\r
-    };\r
-\r
-    api.createRangyRange = function(doc) {\r
-        doc = doc || document;\r
-        return new DomRange(doc);\r
-    };\r
-\r
-    api.createIframeRange = function(iframeEl) {\r
-        return api.createRange(dom.getIframeDocument(iframeEl));\r
-    };\r
-\r
-    api.createIframeRangyRange = function(iframeEl) {\r
-        return api.createRangyRange(dom.getIframeDocument(iframeEl));\r
-    };\r
-\r
-    api.addCreateMissingNativeApiListener(function(win) {\r
-        var doc = win.document;\r
-        if (typeof doc.createRange == "undefined") {\r
-            doc.createRange = function() {\r
-                return api.createRange(this);\r
-            };\r
-        }\r
-        doc = win = null;\r
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return doc.createRange();
+                };
+            })();
+        }
+
+        if (api.features.implementsTextRange) {
+            /*
+            This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
+            method. For example, in the following (where pipes denote the selection boundaries):
+
+            <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
+
+            var range = document.selection.createRange();
+            alert(range.parentElement().id); // Should alert "ul" but alerts "b"
+
+            This method returns the common ancestor node of the following:
+            - the parentElement() of the textRange
+            - the parentElement() of the textRange after calling collapse(true)
+            - the parentElement() of the textRange after calling collapse(false)
+            */
+            var getTextRangeContainerElement = function(textRange) {
+                var parentEl = textRange.parentElement();
+                var range = textRange.duplicate();
+                range.collapse(true);
+                var startEl = range.parentElement();
+                range = textRange.duplicate();
+                range.collapse(false);
+                var endEl = range.parentElement();
+                var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
+
+                return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
+            };
+
+            var textRangeIsCollapsed = function(textRange) {
+                return textRange.compareEndPoints("StartToEnd", textRange) == 0;
+            };
+
+            // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
+            // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
+            // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
+            // bugs, handling for inputs and images, plus optimizations.
+            var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
+                var workingRange = textRange.duplicate();
+                workingRange.collapse(isStart);
+                var containerElement = workingRange.parentElement();
+
+                // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
+                // check for that
+                if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
+                    containerElement = wholeRangeContainerElement;
+                }
+
+
+                // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
+                // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
+                if (!containerElement.canHaveHTML) {
+                    var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
+                    return {
+                        boundaryPosition: pos,
+                        nodeInfo: {
+                            nodeIndex: pos.offset,
+                            containerElement: pos.node
+                        }
+                    };
+                }
+
+                var workingNode = dom.getDocument(containerElement).createElement("span");
+
+                // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
+                // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
+                if (workingNode.parentNode) {
+                    dom.removeNode(workingNode);
+                }
+
+                var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
+                var previousNode, nextNode, boundaryPosition, boundaryNode;
+                var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
+                var childNodeCount = containerElement.childNodes.length;
+                var end = childNodeCount;
+
+                // Check end first. Code within the loop assumes that the endth child node of the container is definitely
+                // after the range boundary.
+                var nodeIndex = end;
+
+                while (true) {
+                    if (nodeIndex == childNodeCount) {
+                        containerElement.appendChild(workingNode);
+                    } else {
+                        containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
+                    }
+                    workingRange.moveToElementText(workingNode);
+                    comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
+                    if (comparison == 0 || start == end) {
+                        break;
+                    } else if (comparison == -1) {
+                        if (end == start + 1) {
+                            // We know the endth child node is after the range boundary, so we must be done.
+                            break;
+                        } else {
+                            start = nodeIndex;
+                        }
+                    } else {
+                        end = (end == start + 1) ? start : nodeIndex;
+                    }
+                    nodeIndex = Math.floor((start + end) / 2);
+                    containerElement.removeChild(workingNode);
+                }
+
+
+                // We've now reached or gone past the boundary of the text range we're interested in
+                // so have identified the node we want
+                boundaryNode = workingNode.nextSibling;
+
+                if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
+                    // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
+                    // the node containing the text range's boundary, so we move the end of the working range to the
+                    // boundary point and measure the length of its text to get the boundary's offset within the node.
+                    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
+
+                    var offset;
+
+                    if (/[\r\n]/.test(boundaryNode.data)) {
+                        /*
+                        For the particular case of a boundary within a text node containing rendered line breaks (within a
+                        <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
+                        IE. The facts:
+
+                        - Each line break is represented as \r in the text node's data/nodeValue properties
+                        - Each line break is represented as \r\n in the TextRange's 'text' property
+                        - The 'text' property of the TextRange does not contain trailing line breaks
+
+                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
+                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
+                        to use this to store the characters moved when moving both the start and end of the range to the
+                        start of the document body and subtracting the start offset from the end offset (the
+                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
+                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
+                        the end of the document) has the same problem.
+
+                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
+                        end boundary one character at a time and incrementing a counter with the value returned by the
+                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
+                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
+                        by the location of the range within the document).
+
+                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
+                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
+                        be longer than the text of the TextRange, so the start of the range is moved that length initially
+                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
+                        property. This has good performance in most situations compared to the previous two methods.
+                        */
+                        var tempRange = workingRange.duplicate();
+                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                        offset = tempRange.moveStart("character", rangeLength);
+                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                            offset++;
+                            tempRange.moveStart("character", 1);
+                        }
+                    } else {
+                        offset = workingRange.text.length;
+                    }
+                    boundaryPosition = new DomPosition(boundaryNode, offset);
+                } else {
+
+                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+                    // a position within that, and likewise for a start boundary preceding a character data node
+                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+                    if (nextNode && isCharacterDataNode(nextNode)) {
+                        boundaryPosition = new DomPosition(nextNode, 0);
+                    } else if (previousNode && isCharacterDataNode(previousNode)) {
+                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
+                    } else {
+                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+                    }
+                }
+
+                // Clean up
+                dom.removeNode(workingNode);
+
+                return {
+                    boundaryPosition: boundaryPosition,
+                    nodeInfo: {
+                        nodeIndex: nodeIndex,
+                        containerElement: containerElement
+                    }
+                };
+            };
+
+            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
+            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+            // (http://code.google.com/p/ierange/)
+            var createBoundaryTextRange = function(boundaryPosition, isStart) {
+                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+                var doc = dom.getDocument(boundaryPosition.node);
+                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
+                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
+
+                if (nodeIsDataNode) {
+                    boundaryNode = boundaryPosition.node;
+                    boundaryParent = boundaryNode.parentNode;
+                } else {
+                    childNodes = boundaryPosition.node.childNodes;
+                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+                    boundaryParent = boundaryPosition.node;
+                }
+
+                // Position the range immediately before the node containing the boundary
+                workingNode = doc.createElement("span");
+
+                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
+                // the element rather than immediately before or after it
+                workingNode.innerHTML = "&#feff;";
+
+                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+                if (boundaryNode) {
+                    boundaryParent.insertBefore(workingNode, boundaryNode);
+                } else {
+                    boundaryParent.appendChild(workingNode);
+                }
+
+                workingRange.moveToElementText(workingNode);
+                workingRange.collapse(!isStart);
+
+                // Clean up
+                boundaryParent.removeChild(workingNode);
+
+                // Move the working range to the text offset, if required
+                if (nodeIsDataNode) {
+                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+                }
+
+                return workingRange;
+            };
+
+            /*------------------------------------------------------------------------------------------------------------*/
+
+            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+            // prototype
+
+            WrappedTextRange = function(textRange) {
+                this.textRange = textRange;
+                this.refresh();
+            };
+
+            WrappedTextRange.prototype = new DomRange(document);
+
+            WrappedTextRange.prototype.refresh = function() {
+                var start, end, startBoundary;
+
+                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+                if (textRangeIsCollapsed(this.textRange)) {
+                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
+                        true).boundaryPosition;
+                } else {
+                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                    start = startBoundary.boundaryPosition;
+
+                    // An optimization used here is that if the start and end boundaries have the same parent element, the
+                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
+                    // the start boundary
+                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
+                        startBoundary.nodeInfo).boundaryPosition;
+                }
+
+                this.setStart(start.node, start.offset);
+                this.setEnd(end.node, end.offset);
+            };
+
+            WrappedTextRange.prototype.getName = function() {
+                return "WrappedTextRange";
+            };
+
+            DomRange.copyComparisonConstants(WrappedTextRange);
+
+            var rangeToTextRange = function(range) {
+                if (range.collapsed) {
+                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                } else {
+                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
+                    textRange.setEndPoint("StartToStart", startRange);
+                    textRange.setEndPoint("EndToEnd", endRange);
+                    return textRange;
+                }
+            };
+
+            WrappedTextRange.rangeToTextRange = rangeToTextRange;
+
+            WrappedTextRange.prototype.toTextRange = function() {
+                return rangeToTextRange(this);
+            };
+
+            api.WrappedTextRange = WrappedTextRange;
+
+            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
+            // implementation to use by default.
+            if (!api.features.implementsDomRange || api.config.preferTextRange) {
+                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+                var globalObj = (function(f) { return f("return this;")(); })(Function);
+                if (typeof globalObj.Range == "undefined") {
+                    globalObj.Range = WrappedTextRange;
+                }
+
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return getBody(doc).createTextRange();
+                };
+
+                api.WrappedRange = WrappedTextRange;
+            }
+        }
+
+        api.createRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRange");
+            return new api.WrappedRange(api.createNativeRange(doc));
+        };
+
+        api.createRangyRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRangyRange");
+            return new DomRange(doc);
+        };
+
+        util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
+        util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
+
+        api.addShimListener(function(win) {
+            var doc = win.document;
+            if (typeof doc.createRange == "undefined") {
+                doc.createRange = function() {
+                    return api.createRange(doc);
+                };
+            }
+            doc = win = null;
+        });
     });\r
-});rangy.createModule("WrappedSelection", function(api, module) {\r
-    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range\r
-    // spec (http://html5.org/specs/dom-range.html)\r
-\r
-    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );\r
-\r
-    api.config.checkSelectionRanges = true;\r
-\r
-    var BOOLEAN = "boolean",\r
-        windowPropertyName = "_rangySelection",\r
-        dom = api.dom,\r
-        util = api.util,\r
-        DomRange = api.DomRange,\r
-        WrappedRange = api.WrappedRange,\r
-        DOMException = api.DOMException,\r
-        DomPosition = dom.DomPosition,\r
-        getSelection,\r
-        selectionIsCollapsed,\r
-        CONTROL = "Control";\r
-\r
-\r
-\r
-    function getWinSelection(winParam) {\r
-        return (winParam || window).getSelection();\r
-    }\r
-\r
-    function getDocSelection(winParam) {\r
-        return (winParam || window).document.selection;\r
-    }\r
-\r
-    // Test for the Range/TextRange and Selection features required\r
-    // Test for ability to retrieve selection\r
-    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),\r
-        implementsDocSelection = api.util.isHostObject(document, "selection");\r
-\r
-    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);\r
 \r
-    if (useDocumentSelection) {\r
-        getSelection = getDocSelection;\r
-        api.isSelectionValid = function(winParam) {\r
-            var doc = (winParam || window).document, nativeSel = doc.selection;\r
-\r
-            // Check whether the selection TextRange is actually contained within the correct document\r
-            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);\r
-        };\r
-    } else if (implementsWinGetSelection) {\r
-        getSelection = getWinSelection;\r
-        api.isSelectionValid = function() {\r
-            return true;\r
-        };\r
-    } else {\r
-        module.fail("Neither document.selection or window.getSelection() detected.");\r
-    }\r
-\r
-    api.getNativeSelection = getSelection;\r
-\r
-    var testSelection = getSelection();\r
-    var testRange = api.createNativeRange(document);\r
-    var body = dom.getBody(document);\r
-\r
-    // Obtaining a range from a selection\r
-    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&\r
-                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));\r
-    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;\r
-\r
-    // Test for existence of native selection extend() method\r
-    var selectionHasExtend = util.isHostMethod(testSelection, "extend");\r
-    api.features.selectionHasExtend = selectionHasExtend;\r
-\r
-    // Test if rangeCount exists\r
-    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");\r
-    api.features.selectionHasRangeCount = selectionHasRangeCount;\r
-\r
-    var selectionSupportsMultipleRanges = false;\r
-    var collapsedNonEditableSelectionsSupported = true;\r
-<