[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/build/moodle-editor_atto-rangy/ -> moodle-editor_atto-rangy.js (source)

   1  /**

   2   * Rangy, a cross-browser JavaScript range and selection library

   3   * https://github.com/timdown/rangy

   4   *

   5   * Copyright 2015, Tim Down

   6   * Licensed under the MIT license.

   7   * Version: 1.3.0

   8   * Build date: 10 May 2015

   9   */
  10  
  11  (function(factory, root) {
  12      // No AMD or CommonJS support so we place Rangy in (probably) the global variable

  13      root.rangy = factory();
  14  })(function() {
  15  
  16      var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
  17  
  18      // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START

  19      // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.

  20      var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
  21          "commonAncestorContainer"];
  22  
  23      // Minimal set of methods required for DOM Level 2 Range compliance

  24      var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
  25          "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
  26          "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
  27  
  28      var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
  29  
  30      // Subset of TextRange's full set of methods that we're interested in

  31      var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
  32          "setEndPoint", "getBoundingClientRect"];
  33  
  34      /*----------------------------------------------------------------------------------------------------------------*/

  35  
  36      // Trio of functions taken from Peter Michaux's article:

  37      // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting

  38      function isHostMethod(o, p) {
  39          var t = typeof o[p];
  40          return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
  41      }
  42  
  43      function isHostObject(o, p) {
  44          return !!(typeof o[p] == OBJECT && o[p]);
  45      }
  46  
  47      function isHostProperty(o, p) {
  48          return typeof o[p] != UNDEFINED;
  49      }
  50  
  51      // Creates a convenience function to save verbose repeated calls to tests functions

  52      function createMultiplePropertyTest(testFunc) {
  53          return function(o, props) {
  54              var i = props.length;
  55              while (i--) {
  56                  if (!testFunc(o, props[i])) {
  57                      return false;
  58                  }
  59              }
  60              return true;
  61          };
  62      }
  63  
  64      // Next trio of functions are a convenience to save verbose repeated calls to previous two functions

  65      var areHostMethods = createMultiplePropertyTest(isHostMethod);
  66      var areHostObjects = createMultiplePropertyTest(isHostObject);
  67      var areHostProperties = createMultiplePropertyTest(isHostProperty);
  68  
  69      function isTextRange(range) {
  70          return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
  71      }
  72  
  73      function getBody(doc) {
  74          return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
  75      }
  76  
  77      var forEach = [].forEach ?
  78          function(arr, func) {
  79              arr.forEach(func);
  80          } :
  81          function(arr, func) {
  82              for (var i = 0, len = arr.length; i < len; ++i) {
  83                  func(arr[i], i);
  84              }
  85          };
  86  
  87      var modules = {};
  88  
  89      var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
  90  
  91      var util = {
  92          isHostMethod: isHostMethod,
  93          isHostObject: isHostObject,
  94          isHostProperty: isHostProperty,
  95          areHostMethods: areHostMethods,
  96          areHostObjects: areHostObjects,
  97          areHostProperties: areHostProperties,
  98          isTextRange: isTextRange,
  99          getBody: getBody,
 100          forEach: forEach
 101      };
 102  
 103      var api = {
 104          version: "1.3.0",
 105          initialized: false,
 106          isBrowser: isBrowser,
 107          supported: true,
 108          util: util,
 109          features: {},
 110          modules: modules,
 111          config: {
 112              alertOnFail: false,
 113              alertOnWarn: false,
 114              preferTextRange: false,
 115              autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
 116          }
 117      };
 118  
 119      function consoleLog(msg) {
 120          if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
 121              console.log(msg);
 122          }
 123      }
 124  
 125      function alertOrLog(msg, shouldAlert) {
 126          if (isBrowser && shouldAlert) {
 127              alert(msg);
 128          } else  {
 129              consoleLog(msg);
 130          }
 131      }
 132  
 133      function fail(reason) {
 134          api.initialized = true;
 135          api.supported = false;
 136          alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
 137      }
 138  
 139      api.fail = fail;
 140  
 141      function warn(msg) {
 142          alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
 143      }
 144  
 145      api.warn = warn;
 146  
 147      // Add utility extend() method

 148      var extend;
 149      if ({}.hasOwnProperty) {
 150          util.extend = extend = function(obj, props, deep) {
 151              var o, p;
 152              for (var i in props) {
 153                  if (props.hasOwnProperty(i)) {
 154                      o = obj[i];
 155                      p = props[i];
 156                      if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
 157                          extend(o, p, true);
 158                      }
 159                      obj[i] = p;
 160                  }
 161              }
 162              // Special case for toString, which does not show up in for...in loops in IE <= 8

 163              if (props.hasOwnProperty("toString")) {
 164                  obj.toString = props.toString;
 165              }
 166              return obj;
 167          };
 168  
 169          util.createOptions = function(optionsParam, defaults) {
 170              var options = {};
 171              extend(options, defaults);
 172              if (optionsParam) {
 173                  extend(options, optionsParam);
 174              }
 175              return options;
 176          };
 177      } else {
 178          fail("hasOwnProperty not supported");
 179      }
 180  
 181      // Test whether we're in a browser and bail out if not

 182      if (!isBrowser) {
 183          fail("Rangy can only run in a browser");
 184      }
 185  
 186      // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not

 187      (function() {
 188          var toArray;
 189  
 190          if (isBrowser) {
 191              var el = document.createElement("div");
 192              el.appendChild(document.createElement("span"));
 193              var slice = [].slice;
 194              try {
 195                  if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
 196                      toArray = function(arrayLike) {
 197                          return slice.call(arrayLike, 0);
 198                      };
 199                  }
 200              } catch (e) {}
 201          }
 202  
 203          if (!toArray) {
 204              toArray = function(arrayLike) {
 205                  var arr = [];
 206                  for (var i = 0, len = arrayLike.length; i < len; ++i) {
 207                      arr[i] = arrayLike[i];
 208                  }
 209                  return arr;
 210              };
 211          }
 212  
 213          util.toArray = toArray;
 214      })();
 215  
 216      // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or

 217      // normalization of event properties

 218      var addListener;
 219      if (isBrowser) {
 220          if (isHostMethod(document, "addEventListener")) {
 221              addListener = function(obj, eventType, listener) {
 222                  obj.addEventListener(eventType, listener, false);
 223              };
 224          } else if (isHostMethod(document, "attachEvent")) {
 225              addListener = function(obj, eventType, listener) {
 226                  obj.attachEvent("on" + eventType, listener);
 227              };
 228          } else {
 229              fail("Document does not have required addEventListener or attachEvent method");
 230          }
 231  
 232          util.addListener = addListener;
 233      }
 234  
 235      var initListeners = [];
 236  
 237      function getErrorDesc(ex) {
 238          return ex.message || ex.description || String(ex);
 239      }
 240  
 241      // Initialization

 242      function init() {
 243          if (!isBrowser || api.initialized) {
 244              return;
 245          }
 246          var testRange;
 247          var implementsDomRange = false, implementsTextRange = false;
 248  
 249          // First, perform basic feature tests

 250  
 251          if (isHostMethod(document, "createRange")) {
 252              testRange = document.createRange();
 253              if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
 254                  implementsDomRange = true;
 255              }
 256          }
 257  
 258          var body = getBody(document);
 259          if (!body || body.nodeName.toLowerCase() != "body") {
 260              fail("No body element found");
 261              return;
 262          }
 263  
 264          if (body && isHostMethod(body, "createTextRange")) {
 265              testRange = body.createTextRange();
 266              if (isTextRange(testRange)) {
 267                  implementsTextRange = true;
 268              }
 269          }
 270  
 271          if (!implementsDomRange && !implementsTextRange) {
 272              fail("Neither Range nor TextRange are available");
 273              return;
 274          }
 275  
 276          api.initialized = true;
 277          api.features = {
 278              implementsDomRange: implementsDomRange,
 279              implementsTextRange: implementsTextRange
 280          };
 281  
 282          // Initialize modules

 283          var module, errorMessage;
 284          for (var moduleName in modules) {
 285              if ( (module = modules[moduleName]) instanceof Module ) {
 286                  module.init(module, api);
 287              }
 288          }
 289  
 290          // Call init listeners

 291          for (var i = 0, len = initListeners.length; i < len; ++i) {
 292              try {
 293                  initListeners[i](api);
 294              } catch (ex) {
 295                  errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
 296                  consoleLog(errorMessage);
 297              }
 298          }
 299      }
 300  
 301      function deprecationNotice(deprecated, replacement, module) {
 302          if (module) {
 303              deprecated += " in module " + module.name;
 304          }
 305          api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
 306          replacement + " instead.");
 307      }
 308  
 309      function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
 310          owner[deprecated] = function() {
 311              deprecationNotice(deprecated, replacement, module);
 312              return owner[replacement].apply(owner, util.toArray(arguments));
 313          };
 314      }
 315  
 316      util.deprecationNotice = deprecationNotice;
 317      util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
 318  
 319      // Allow external scripts to initialize this library in case it's loaded after the document has loaded

 320      api.init = init;
 321  
 322      // Execute listener immediately if already initialized

 323      api.addInitListener = function(listener) {
 324          if (api.initialized) {
 325              listener(api);
 326          } else {
 327              initListeners.push(listener);
 328          }
 329      };
 330  
 331      var shimListeners = [];
 332  
 333      api.addShimListener = function(listener) {
 334          shimListeners.push(listener);
 335      };
 336  
 337      function shim(win) {
 338          win = win || window;
 339          init();
 340  
 341          // Notify listeners

 342          for (var i = 0, len = shimListeners.length; i < len; ++i) {
 343              shimListeners[i](win);
 344          }
 345      }
 346  
 347      if (isBrowser) {
 348          api.shim = api.createMissingNativeApi = shim;
 349          createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
 350      }
 351  
 352      function Module(name, dependencies, initializer) {
 353          this.name = name;
 354          this.dependencies = dependencies;
 355          this.initialized = false;
 356          this.supported = false;
 357          this.initializer = initializer;
 358      }
 359  
 360      Module.prototype = {
 361          init: function() {
 362              var requiredModuleNames = this.dependencies || [];
 363              for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
 364                  moduleName = requiredModuleNames[i];
 365  
 366                  requiredModule = modules[moduleName];
 367                  if (!requiredModule || !(requiredModule instanceof Module)) {
 368                      throw new Error("required module '" + moduleName + "' not found");
 369                  }
 370  
 371                  requiredModule.init();
 372  
 373                  if (!requiredModule.supported) {
 374                      throw new Error("required module '" + moduleName + "' not supported");
 375                  }
 376              }
 377  
 378              // Now run initializer

 379              this.initializer(this);
 380          },
 381  
 382          fail: function(reason) {
 383              this.initialized = true;
 384              this.supported = false;
 385              throw new Error(reason);
 386          },
 387  
 388          warn: function(msg) {
 389              api.warn("Module " + this.name + ": " + msg);
 390          },
 391  
 392          deprecationNotice: function(deprecated, replacement) {
 393              api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
 394                  replacement + " instead");
 395          },
 396  
 397          createError: function(msg) {
 398              return new Error("Error in Rangy " + this.name + " module: " + msg);
 399          }
 400      };
 401  
 402      function createModule(name, dependencies, initFunc) {
 403          var newModule = new Module(name, dependencies, function(module) {
 404              if (!module.initialized) {
 405                  module.initialized = true;
 406                  try {
 407                      initFunc(api, module);
 408                      module.supported = true;
 409                  } catch (ex) {
 410                      var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
 411                      consoleLog(errorMessage);
 412                      if (ex.stack) {
 413                          consoleLog(ex.stack);
 414                      }
 415                  }
 416              }
 417          });
 418          modules[name] = newModule;
 419          return newModule;
 420      }
 421  
 422      api.createModule = function(name) {
 423          // Allow 2 or 3 arguments (second argument is an optional array of dependencies)

 424          var initFunc, dependencies;
 425          if (arguments.length == 2) {
 426              initFunc = arguments[1];
 427              dependencies = [];
 428          } else {
 429              initFunc = arguments[2];
 430              dependencies = arguments[1];
 431          }
 432  
 433          var module = createModule(name, dependencies, initFunc);
 434  
 435          // Initialize the module immediately if the core is already initialized

 436          if (api.initialized && api.supported) {
 437              module.init();
 438          }
 439      };
 440  
 441      api.createCoreModule = function(name, dependencies, initFunc) {
 442          createModule(name, dependencies, initFunc);
 443      };
 444  
 445      /*----------------------------------------------------------------------------------------------------------------*/

 446  
 447      // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately

 448  
 449      function RangePrototype() {}
 450      api.RangePrototype = RangePrototype;
 451      api.rangePrototype = new RangePrototype();
 452  
 453      function SelectionPrototype() {}
 454      api.selectionPrototype = new SelectionPrototype();
 455  
 456      /*----------------------------------------------------------------------------------------------------------------*/

 457  
 458      // DOM utility methods used by Rangy
 459      api.createCoreModule("DomUtil", [], function(api, module) {
 460          var UNDEF = "undefined";
 461          var util = api.util;
 462          var getBody = util.getBody;
 463  
 464          // Perform feature tests
 465          if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
 466              module.fail("document missing a Node creation method");
 467          }
 468  
 469          if (!util.isHostMethod(document, "getElementsByTagName")) {
 470              module.fail("document missing getElementsByTagName method");
 471          }
 472  
 473          var el = document.createElement("div");
 474          if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
 475                  !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
 476              module.fail("Incomplete Element implementation");
 477          }
 478  
 479          // innerHTML is required for Range's createContextualFragment method
 480          if (!util.isHostProperty(el, "innerHTML")) {
 481              module.fail("Element is missing innerHTML property");
 482          }
 483  
 484          var textNode = document.createTextNode("test");
 485          if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
 486                  !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
 487                  !util.areHostProperties(textNode, ["data"]))) {
 488              module.fail("Incomplete Text Node implementation");
 489          }
 490  
 491          /*----------------------------------------------------------------------------------------------------------------*/
 492  
 493          // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
 494          // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
 495          // contains just the document as a single element and the value searched for is the document.
 496          var arrayContains = /*Array.prototype.indexOf ?
 497              function(arr, val) {
 498                  return arr.indexOf(val) > -1;
 499              }:*/
 500  
 501              function(arr, val) {
 502                  var i = arr.length;
 503                  while (i--) {
 504                      if (arr[i] === val) {
 505                          return true;
 506                      }
 507                  }
 508                  return false;
 509              };
 510  
 511          // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
 512          function isHtmlNamespace(node) {
 513              var ns;
 514              return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
 515          }
 516  
 517          function parentElement(node) {
 518              var parent = node.parentNode;
 519              return (parent.nodeType == 1) ? parent : null;
 520          }
 521  
 522          function getNodeIndex(node) {
 523              var i = 0;
 524              while( (node = node.previousSibling) ) {
 525                  ++i;
 526              }
 527              return i;
 528          }
 529  
 530          function getNodeLength(node) {
 531              switch (node.nodeType) {
 532                  case 7:
 533                  case 10:
 534                      return 0;
 535                  case 3:
 536                  case 8:
 537                      return node.length;
 538                  default:
 539                      return node.childNodes.length;
 540              }
 541          }
 542  
 543          function getCommonAncestor(node1, node2) {
 544              var ancestors = [], n;
 545              for (n = node1; n; n = n.parentNode) {
 546                  ancestors.push(n);
 547              }
 548  
 549              for (n = node2; n; n = n.parentNode) {
 550                  if (arrayContains(ancestors, n)) {
 551                      return n;
 552                  }
 553              }
 554  
 555              return null;
 556          }
 557  
 558          function isAncestorOf(ancestor, descendant, selfIsAncestor) {
 559              var n = selfIsAncestor ? descendant : descendant.parentNode;
 560              while (n) {
 561                  if (n === ancestor) {
 562                      return true;
 563                  } else {
 564                      n = n.parentNode;
 565                  }
 566              }
 567              return false;
 568          }
 569  
 570          function isOrIsAncestorOf(ancestor, descendant) {
 571              return isAncestorOf(ancestor, descendant, true);
 572          }
 573  
 574          function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
 575              var p, n = selfIsAncestor ? node : node.parentNode;
 576              while (n) {
 577                  p = n.parentNode;
 578                  if (p === ancestor) {
 579                      return n;
 580                  }
 581                  n = p;
 582              }
 583              return null;
 584          }
 585  
 586          function isCharacterDataNode(node) {
 587              var t = node.nodeType;
 588              return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
 589          }
 590  
 591          function isTextOrCommentNode(node) {
 592              if (!node) {
 593                  return false;
 594              }
 595              var t = node.nodeType;
 596              return t == 3 || t == 8 ; // Text or Comment
 597          }
 598  
 599          function insertAfter(node, precedingNode) {
 600              var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
 601              if (nextNode) {
 602                  parent.insertBefore(node, nextNode);
 603              } else {
 604                  parent.appendChild(node);
 605              }
 606              return node;
 607          }
 608  
 609          // Note that we cannot use splitText() because it is bugridden in IE 9.
 610          function splitDataNode(node, index, positionsToPreserve) {
 611              var newNode = node.cloneNode(false);
 612              newNode.deleteData(0, index);
 613              node.deleteData(index, node.length - index);
 614              insertAfter(newNode, node);
 615  
 616              // Preserve positions
 617              if (positionsToPreserve) {
 618                  for (var i = 0, position; position = positionsToPreserve[i++]; ) {
 619                      // Handle case where position was inside the portion of node after the split point
 620                      if (position.node == node && position.offset > index) {
 621                          position.node = newNode;
 622                          position.offset -= index;
 623                      }
 624                      // Handle the case where the position is a node offset within node's parent
 625                      else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
 626                          ++position.offset;
 627                      }
 628                  }
 629              }
 630              return newNode;
 631          }
 632  
 633          function getDocument(node) {
 634              if (node.nodeType == 9) {
 635                  return node;
 636              } else if (typeof node.ownerDocument != UNDEF) {
 637                  return node.ownerDocument;
 638              } else if (typeof node.document != UNDEF) {
 639                  return node.document;
 640              } else if (node.parentNode) {
 641                  return getDocument(node.parentNode);
 642              } else {
 643                  throw module.createError("getDocument: no document found for node");
 644              }
 645          }
 646  
 647          function getWindow(node) {
 648              var doc = getDocument(node);
 649              if (typeof doc.defaultView != UNDEF) {
 650                  return doc.defaultView;
 651              } else if (typeof doc.parentWindow != UNDEF) {
 652                  return doc.parentWindow;
 653              } else {
 654                  throw module.createError("Cannot get a window object for node");
 655              }
 656          }
 657  
 658          function getIframeDocument(iframeEl) {
 659              if (typeof iframeEl.contentDocument != UNDEF) {
 660                  return iframeEl.contentDocument;
 661              } else if (typeof iframeEl.contentWindow != UNDEF) {
 662                  return iframeEl.contentWindow.document;
 663              } else {
 664                  throw module.createError("getIframeDocument: No Document object found for iframe element");
 665              }
 666          }
 667  
 668          function getIframeWindow(iframeEl) {
 669              if (typeof iframeEl.contentWindow != UNDEF) {
 670                  return iframeEl.contentWindow;
 671              } else if (typeof iframeEl.contentDocument != UNDEF) {
 672                  return iframeEl.contentDocument.defaultView;
 673              } else {
 674                  throw module.createError("getIframeWindow: No Window object found for iframe element");
 675              }
 676          }
 677  
 678          // This looks bad. Is it worth it?
 679          function isWindow(obj) {
 680              return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
 681          }
 682  
 683          function getContentDocument(obj, module, methodName) {
 684              var doc;
 685  
 686              if (!obj) {
 687                  doc = document;
 688              }
 689  
 690              // Test if a DOM node has been passed and obtain a document object for it if so
 691              else if (util.isHostProperty(obj, "nodeType")) {
 692                  doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
 693                      getIframeDocument(obj) : getDocument(obj);
 694              }
 695  
 696              // Test if the doc parameter appears to be a Window object
 697              else if (isWindow(obj)) {
 698                  doc = obj.document;
 699              }
 700  
 701              if (!doc) {
 702                  throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
 703              }
 704  
 705              return doc;
 706          }
 707  
 708          function getRootContainer(node) {
 709              var parent;
 710              while ( (parent = node.parentNode) ) {
 711                  node = parent;
 712              }
 713              return node;
 714          }
 715  
 716          function comparePoints(nodeA, offsetA, nodeB, offsetB) {
 717              // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
 718              var nodeC, root, childA, childB, n;
 719              if (nodeA == nodeB) {
 720                  // Case 1: nodes are the same
 721                  return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
 722              } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
 723                  // Case 2: node C (container B or an ancestor) is a child node of A
 724                  return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
 725              } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
 726                  // Case 3: node C (container A or an ancestor) is a child node of B
 727                  return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
 728              } else {
 729                  root = getCommonAncestor(nodeA, nodeB);
 730                  if (!root) {
 731                      throw new Error("comparePoints error: nodes have no common ancestor");
 732                  }
 733  
 734                  // Case 4: containers are siblings or descendants of siblings
 735                  childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
 736                  childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
 737  
 738                  if (childA === childB) {
 739                      // This shouldn't be possible
 740                      throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
 741                  } else {
 742                      n = root.firstChild;
 743                      while (n) {
 744                          if (n === childA) {
 745                              return -1;
 746                          } else if (n === childB) {
 747                              return 1;
 748                          }
 749                          n = n.nextSibling;
 750                      }
 751                  }
 752              }
 753          }
 754  
 755          /*----------------------------------------------------------------------------------------------------------------*/
 756  
 757          // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
 758          var crashyTextNodes = false;
 759  
 760          function isBrokenNode(node) {
 761              var n;
 762              try {
 763                  n = node.parentNode;
 764                  return false;
 765              } catch (e) {
 766                  return true;
 767              }
 768          }
 769  
 770          (function() {
 771              var el = document.createElement("b");
 772              el.innerHTML = "1";
 773              var textNode = el.firstChild;
 774              el.innerHTML = "<br />";
 775              crashyTextNodes = isBrokenNode(textNode);
 776  
 777              api.features.crashyTextNodes = crashyTextNodes;
 778          })();
 779  
 780          /*----------------------------------------------------------------------------------------------------------------*/
 781  
 782          function inspectNode(node) {
 783              if (!node) {
 784                  return "[No node]";
 785              }
 786              if (crashyTextNodes && isBrokenNode(node)) {
 787                  return "[Broken node]";
 788              }
 789              if (isCharacterDataNode(node)) {
 790                  return '"' + node.data + '"';
 791              }
 792              if (node.nodeType == 1) {
 793                  var idAttr = node.id ? ' id="' + node.id + '"' : "";
 794                  return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
 795              }
 796              return node.nodeName;
 797          }
 798  
 799          function fragmentFromNodeChildren(node) {
 800              var fragment = getDocument(node).createDocumentFragment(), child;
 801              while ( (child = node.firstChild) ) {
 802                  fragment.appendChild(child);
 803              }
 804              return fragment;
 805          }
 806  
 807          var getComputedStyleProperty;
 808          if (typeof window.getComputedStyle != UNDEF) {
 809              getComputedStyleProperty = function(el, propName) {
 810                  return getWindow(el).getComputedStyle(el, null)[propName];
 811              };
 812          } else if (typeof document.documentElement.currentStyle != UNDEF) {
 813              getComputedStyleProperty = function(el, propName) {
 814                  return el.currentStyle ? el.currentStyle[propName] : "";
 815              };
 816          } else {
 817              module.fail("No means of obtaining computed style properties found");
 818          }
 819  
 820          function createTestElement(doc, html, contentEditable) {
 821              var body = getBody(doc);
 822              var el = doc.createElement("div");
 823              el.contentEditable = "" + !!contentEditable;
 824              if (html) {
 825                  el.innerHTML = html;
 826              }
 827  
 828              // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
 829              var bodyFirstChild = body.firstChild;
 830              if (bodyFirstChild) {
 831                  body.insertBefore(el, bodyFirstChild);
 832              } else {
 833                  body.appendChild(el);
 834              }
 835  
 836              return el;
 837          }
 838  
 839          function removeNode(node) {
 840              return node.parentNode.removeChild(node);
 841          }
 842  
 843          function NodeIterator(root) {
 844              this.root = root;
 845              this._next = root;
 846          }
 847  
 848          NodeIterator.prototype = {
 849              _current: null,
 850  
 851              hasNext: function() {
 852                  return !!this._next;
 853              },
 854  
 855              next: function() {
 856                  var n = this._current = this._next;
 857                  var child, next;
 858                  if (this._current) {
 859                      child = n.firstChild;
 860                      if (child) {
 861                          this._next = child;
 862                      } else {
 863                          next = null;
 864                          while ((n !== this.root) && !(next = n.nextSibling)) {
 865                              n = n.parentNode;
 866                          }
 867                          this._next = next;
 868                      }
 869                  }
 870                  return this._current;
 871              },
 872  
 873              detach: function() {
 874                  this._current = this._next = this.root = null;
 875              }
 876          };
 877  
 878          function createIterator(root) {
 879              return new NodeIterator(root);
 880          }
 881  
 882          function DomPosition(node, offset) {
 883              this.node = node;
 884              this.offset = offset;
 885          }
 886  
 887          DomPosition.prototype = {
 888              equals: function(pos) {
 889                  return !!pos && this.node === pos.node && this.offset == pos.offset;
 890              },
 891  
 892              inspect: function() {
 893                  return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
 894              },
 895  
 896              toString: function() {
 897                  return this.inspect();
 898              }
 899          };
 900  
 901          function DOMException(codeName) {
 902              this.code = this[codeName];
 903              this.codeName = codeName;
 904              this.message = "DOMException: " + this.codeName;
 905          }
 906  
 907          DOMException.prototype = {
 908              INDEX_SIZE_ERR: 1,
 909              HIERARCHY_REQUEST_ERR: 3,
 910              WRONG_DOCUMENT_ERR: 4,
 911              NO_MODIFICATION_ALLOWED_ERR: 7,
 912              NOT_FOUND_ERR: 8,
 913              NOT_SUPPORTED_ERR: 9,
 914              INVALID_STATE_ERR: 11,
 915              INVALID_NODE_TYPE_ERR: 24
 916          };
 917  
 918          DOMException.prototype.toString = function() {
 919              return this.message;
 920          };
 921  
 922          api.dom = {
 923              arrayContains: arrayContains,
 924              isHtmlNamespace: isHtmlNamespace,
 925              parentElement: parentElement,
 926              getNodeIndex: getNodeIndex,
 927              getNodeLength: getNodeLength,
 928              getCommonAncestor: getCommonAncestor,
 929              isAncestorOf: isAncestorOf,
 930              isOrIsAncestorOf: isOrIsAncestorOf,
 931              getClosestAncestorIn: getClosestAncestorIn,
 932              isCharacterDataNode: isCharacterDataNode,
 933              isTextOrCommentNode: isTextOrCommentNode,
 934              insertAfter: insertAfter,
 935              splitDataNode: splitDataNode,
 936              getDocument: getDocument,
 937              getWindow: getWindow,
 938              getIframeWindow: getIframeWindow,
 939              getIframeDocument: getIframeDocument,
 940              getBody: getBody,
 941              isWindow: isWindow,
 942              getContentDocument: getContentDocument,
 943              getRootContainer: getRootContainer,
 944              comparePoints: comparePoints,
 945              isBrokenNode: isBrokenNode,
 946              inspectNode: inspectNode,
 947              getComputedStyleProperty: getComputedStyleProperty,
 948              createTestElement: createTestElement,
 949              removeNode: removeNode,
 950              fragmentFromNodeChildren: fragmentFromNodeChildren,
 951              createIterator: createIterator,
 952              DomPosition: DomPosition
 953          };
 954  
 955          api.DOMException = DOMException;
 956      });
 957  
 958      /*----------------------------------------------------------------------------------------------------------------*/

 959  
 960      // Pure JavaScript implementation of DOM Range
 961      api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
 962          var dom = api.dom;
 963          var util = api.util;
 964          var DomPosition = dom.DomPosition;
 965          var DOMException = api.DOMException;
 966  
 967          var isCharacterDataNode = dom.isCharacterDataNode;
 968          var getNodeIndex = dom.getNodeIndex;
 969          var isOrIsAncestorOf = dom.isOrIsAncestorOf;
 970          var getDocument = dom.getDocument;
 971          var comparePoints = dom.comparePoints;
 972          var splitDataNode = dom.splitDataNode;
 973          var getClosestAncestorIn = dom.getClosestAncestorIn;
 974          var getNodeLength = dom.getNodeLength;
 975          var arrayContains = dom.arrayContains;
 976          var getRootContainer = dom.getRootContainer;
 977          var crashyTextNodes = api.features.crashyTextNodes;
 978  
 979          var removeNode = dom.removeNode;
 980  
 981          /*----------------------------------------------------------------------------------------------------------------*/
 982  
 983          // Utility functions
 984  
 985          function isNonTextPartiallySelected(node, range) {
 986              return (node.nodeType != 3) &&
 987                     (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
 988          }
 989  
 990          function getRangeDocument(range) {
 991              return range.document || getDocument(range.startContainer);
 992          }
 993  
 994          function getRangeRoot(range) {
 995              return getRootContainer(range.startContainer);
 996          }
 997  
 998          function getBoundaryBeforeNode(node) {
 999              return new DomPosition(node.parentNode, getNodeIndex(node));
1000          }
1001  
1002          function getBoundaryAfterNode(node) {
1003              return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1004          }
1005  
1006          function insertNodeAtPosition(node, n, o) {
1007              var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1008              if (isCharacterDataNode(n)) {
1009                  if (o == n.length) {
1010                      dom.insertAfter(node, n);
1011                  } else {
1012                      n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1013                  }
1014              } else if (o >= n.childNodes.length) {
1015                  n.appendChild(node);
1016              } else {
1017                  n.insertBefore(node, n.childNodes[o]);
1018              }
1019              return firstNodeInserted;
1020          }
1021  
1022          function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1023              assertRangeValid(rangeA);
1024              assertRangeValid(rangeB);
1025  
1026              if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1027                  throw new DOMException("WRONG_DOCUMENT_ERR");
1028              }
1029  
1030              var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1031                  endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1032  
1033              return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1034          }
1035  
1036          function cloneSubtree(iterator) {
1037              var partiallySelected;
1038              for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1039                  partiallySelected = iterator.isPartiallySelectedSubtree();
1040                  node = node.cloneNode(!partiallySelected);
1041                  if (partiallySelected) {
1042                      subIterator = iterator.getSubtreeIterator();
1043                      node.appendChild(cloneSubtree(subIterator));
1044                      subIterator.detach();
1045                  }
1046  
1047                  if (node.nodeType == 10) { // DocumentType
1048                      throw new DOMException("HIERARCHY_REQUEST_ERR");
1049                  }
1050                  frag.appendChild(node);
1051              }
1052              return frag;
1053          }
1054  
1055          function iterateSubtree(rangeIterator, func, iteratorState) {
1056              var it, n;
1057              iteratorState = iteratorState || { stop: false };
1058              for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1059                  if (rangeIterator.isPartiallySelectedSubtree()) {
1060                      if (func(node) === false) {
1061                          iteratorState.stop = true;
1062                          return;
1063                      } else {
1064                          // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1065                          // the node selected by the Range.
1066                          subRangeIterator = rangeIterator.getSubtreeIterator();
1067                          iterateSubtree(subRangeIterator, func, iteratorState);
1068                          subRangeIterator.detach();
1069                          if (iteratorState.stop) {
1070                              return;
1071                          }
1072                      }
1073                  } else {
1074                      // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1075                      // descendants
1076                      it = dom.createIterator(node);
1077                      while ( (n = it.next()) ) {
1078                          if (func(n) === false) {
1079                              iteratorState.stop = true;
1080                              return;
1081                          }
1082                      }
1083                  }
1084              }
1085          }
1086  
1087          function deleteSubtree(iterator) {
1088              var subIterator;
1089              while (iterator.next()) {
1090                  if (iterator.isPartiallySelectedSubtree()) {
1091                      subIterator = iterator.getSubtreeIterator();
1092                      deleteSubtree(subIterator);
1093                      subIterator.detach();
1094                  } else {
1095                      iterator.remove();
1096                  }
1097              }
1098          }
1099  
1100          function extractSubtree(iterator) {
1101              for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1102  
1103                  if (iterator.isPartiallySelectedSubtree()) {
1104                      node = node.cloneNode(false);
1105                      subIterator = iterator.getSubtreeIterator();
1106                      node.appendChild(extractSubtree(subIterator));
1107                      subIterator.detach();
1108                  } else {
1109                      iterator.remove();
1110                  }
1111                  if (node.nodeType == 10) { // DocumentType
1112                      throw new DOMException("HIERARCHY_REQUEST_ERR");
1113                  }
1114                  frag.appendChild(node);
1115              }
1116              return frag;
1117          }
1118  
1119          function getNodesInRange(range, nodeTypes, filter) {
1120              var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1121              var filterExists = !!filter;
1122              if (filterNodeTypes) {
1123                  regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1124              }
1125  
1126              var nodes = [];
1127              iterateSubtree(new RangeIterator(range, false), function(node) {
1128                  if (filterNodeTypes && !regex.test(node.nodeType)) {
1129                      return;
1130                  }
1131                  if (filterExists && !filter(node)) {
1132                      return;
1133                  }
1134                  // Don't include a boundary container if it is a character data node and the range does not contain any
1135                  // of its character data. See issue 190.
1136                  var sc = range.startContainer;
1137                  if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1138                      return;
1139                  }
1140  
1141                  var ec = range.endContainer;
1142                  if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1143                      return;
1144                  }
1145  
1146                  nodes.push(node);
1147              });
1148              return nodes;
1149          }
1150  
1151          function inspect(range) {
1152              var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1153              return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1154                      dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1155          }
1156  
1157          /*----------------------------------------------------------------------------------------------------------------*/
1158  
1159          // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1160  
1161          function RangeIterator(range, clonePartiallySelectedTextNodes) {
1162              this.range = range;
1163              this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1164  
1165  
1166              if (!range.collapsed) {
1167                  this.sc = range.startContainer;
1168                  this.so = range.startOffset;
1169                  this.ec = range.endContainer;
1170                  this.eo = range.endOffset;
1171                  var root = range.commonAncestorContainer;
1172  
1173                  if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1174                      this.isSingleCharacterDataNode = true;
1175                      this._first = this._last = this._next = this.sc;
1176                  } else {
1177                      this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1178                          this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1179                      this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1180                          this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1181                  }
1182              }
1183          }
1184  
1185          RangeIterator.prototype = {
1186              _current: null,
1187              _next: null,
1188              _first: null,
1189              _last: null,
1190              isSingleCharacterDataNode: false,
1191  
1192              reset: function() {
1193                  this._current = null;
1194                  this._next = this._first;
1195              },
1196  
1197              hasNext: function() {
1198                  return !!this._next;
1199              },
1200  
1201              next: function() {
1202                  // Move to next node
1203                  var current = this._current = this._next;
1204                  if (current) {
1205                      this._next = (current !== this._last) ? current.nextSibling : null;
1206  
1207                      // Check for partially selected text nodes
1208                      if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1209                          if (current === this.ec) {
1210                              (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1211                          }
1212                          if (this._current === this.sc) {
1213                              (current = current.cloneNode(true)).deleteData(0, this.so);
1214                          }
1215                      }
1216                  }
1217  
1218                  return current;
1219              },
1220  
1221              remove: function() {
1222                  var current = this._current, start, end;
1223  
1224                  if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1225                      start = (current === this.sc) ? this.so : 0;
1226                      end = (current === this.ec) ? this.eo : current.length;
1227                      if (start != end) {
1228                          current.deleteData(start, end - start);
1229                      }
1230                  } else {
1231                      if (current.parentNode) {
1232                          removeNode(current);
1233                      } else {
1234                      }
1235                  }
1236              },
1237  
1238              // Checks if the current node is partially selected
1239              isPartiallySelectedSubtree: function() {
1240                  var current = this._current;
1241                  return isNonTextPartiallySelected(current, this.range);
1242              },
1243  
1244              getSubtreeIterator: function() {
1245                  var subRange;
1246                  if (this.isSingleCharacterDataNode) {
1247                      subRange = this.range.cloneRange();
1248                      subRange.collapse(false);
1249                  } else {
1250                      subRange = new Range(getRangeDocument(this.range));
1251                      var current = this._current;
1252                      var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1253  
1254                      if (isOrIsAncestorOf(current, this.sc)) {
1255                          startContainer = this.sc;
1256                          startOffset = this.so;
1257                      }
1258                      if (isOrIsAncestorOf(current, this.ec)) {
1259                          endContainer = this.ec;
1260                          endOffset = this.eo;
1261                      }
1262  
1263                      updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1264                  }
1265                  return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1266              },
1267  
1268              detach: function() {
1269                  this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1270              }
1271          };
1272  
1273          /*----------------------------------------------------------------------------------------------------------------*/
1274  
1275          var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1276          var rootContainerNodeTypes = [2, 9, 11];
1277          var readonlyNodeTypes = [5, 6, 10, 12];
1278          var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1279          var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1280  
1281          function createAncestorFinder(nodeTypes) {
1282              return function(node, selfIsAncestor) {
1283                  var t, n = selfIsAncestor ? node : node.parentNode;
1284                  while (n) {
1285                      t = n.nodeType;
1286                      if (arrayContains(nodeTypes, t)) {
1287                          return n;
1288                      }
1289                      n = n.parentNode;
1290                  }
1291                  return null;
1292              };
1293          }
1294  
1295          var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1296          var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1297          var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1298  
1299          function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1300              if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1301                  throw new DOMException("INVALID_NODE_TYPE_ERR");
1302              }
1303          }
1304  
1305          function assertValidNodeType(node, invalidTypes) {
1306              if (!arrayContains(invalidTypes, node.nodeType)) {
1307                  throw new DOMException("INVALID_NODE_TYPE_ERR");
1308              }
1309          }
1310  
1311          function assertValidOffset(node, offset) {
1312              if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1313                  throw new DOMException("INDEX_SIZE_ERR");
1314              }
1315          }
1316  
1317          function assertSameDocumentOrFragment(node1, node2) {
1318              if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1319                  throw new DOMException("WRONG_DOCUMENT_ERR");
1320              }
1321          }
1322  
1323          function assertNodeNotReadOnly(node) {
1324              if (getReadonlyAncestor(node, true)) {
1325                  throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1326              }
1327          }
1328  
1329          function assertNode(node, codeName) {
1330              if (!node) {
1331                  throw new DOMException(codeName);
1332              }
1333          }
1334  
1335          function isValidOffset(node, offset) {
1336              return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1337          }
1338  
1339          function isRangeValid(range) {
1340              return (!!range.startContainer && !!range.endContainer &&
1341                      !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1342                      getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1343                      isValidOffset(range.startContainer, range.startOffset) &&
1344                      isValidOffset(range.endContainer, range.endOffset));
1345          }
1346  
1347          function assertRangeValid(range) {
1348              if (!isRangeValid(range)) {
1349                  throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1350              }
1351          }
1352  
1353          /*----------------------------------------------------------------------------------------------------------------*/
1354  
1355          // Test the browser's innerHTML support to decide how to implement createContextualFragment
1356          var styleEl = document.createElement("style");
1357          var htmlParsingConforms = false;
1358          try {
1359              styleEl.innerHTML = "<b>x</b>";
1360              htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1361          } catch (e) {
1362              // IE 6 and 7 throw
1363          }
1364  
1365          api.features.htmlParsingConforms = htmlParsingConforms;
1366  
1367          var createContextualFragment = htmlParsingConforms ?
1368  
1369              // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1370              // discussion and base code for this implementation at issue 67.
1371              // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1372              // Thanks to Aleks Williams.
1373              function(fragmentStr) {
1374                  // "Let node the context object's start's node."
1375                  var node = this.startContainer;
1376                  var doc = getDocument(node);
1377  
1378                  // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1379                  // exception and abort these steps."
1380                  if (!node) {
1381                      throw new DOMException("INVALID_STATE_ERR");
1382                  }
1383  
1384                  // "Let element be as follows, depending on node's interface:"
1385                  // Document, Document Fragment: null
1386                  var el = null;
1387  
1388                  // "Element: node"
1389                  if (node.nodeType == 1) {
1390                      el = node;
1391  
1392                  // "Text, Comment: node's parentElement"
1393                  } else if (isCharacterDataNode(node)) {
1394                      el = dom.parentElement(node);
1395                  }
1396  
1397                  // "If either element is null or element's ownerDocument is an HTML document
1398                  // and element's local name is "html" and element's namespace is the HTML
1399                  // namespace"
1400                  if (el === null || (
1401                      el.nodeName == "HTML" &&
1402                      dom.isHtmlNamespace(getDocument(el).documentElement) &&
1403                      dom.isHtmlNamespace(el)
1404                  )) {
1405  
1406                  // "let element be a new Element with "body" as its local name and the HTML
1407                  // namespace as its namespace.""
1408                      el = doc.createElement("body");
1409                  } else {
1410                      el = el.cloneNode(false);
1411                  }
1412  
1413                  // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1414                  // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1415                  // "In either case, the algorithm must be invoked with fragment as the input
1416                  // and element as the context element."
1417                  el.innerHTML = fragmentStr;
1418  
1419                  // "If this raises an exception, then abort these steps. Otherwise, let new
1420                  // children be the nodes returned."
1421  
1422                  // "Let fragment be a new DocumentFragment."
1423                  // "Append all new children to fragment."
1424                  // "Return fragment."
1425                  return dom.fragmentFromNodeChildren(el);
1426              } :
1427  
1428              // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1429              // previous versions of Rangy used (with the exception of using a body element rather than a div)
1430              function(fragmentStr) {
1431                  var doc = getRangeDocument(this);
1432                  var el = doc.createElement("body");
1433                  el.innerHTML = fragmentStr;
1434  
1435                  return dom.fragmentFromNodeChildren(el);
1436              };
1437  
1438          function splitRangeBoundaries(range, positionsToPreserve) {
1439              assertRangeValid(range);
1440  
1441              var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1442              var startEndSame = (sc === ec);
1443  
1444              if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1445                  splitDataNode(ec, eo, positionsToPreserve);
1446              }
1447  
1448              if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1449                  sc = splitDataNode(sc, so, positionsToPreserve);
1450                  if (startEndSame) {
1451                      eo -= so;
1452                      ec = sc;
1453                  } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1454                      eo++;
1455                  }
1456                  so = 0;
1457              }
1458              range.setStartAndEnd(sc, so, ec, eo);
1459          }
1460  
1461          function rangeToHtml(range) {
1462              assertRangeValid(range);
1463              var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1464              container.appendChild( range.cloneContents() );
1465              return container.innerHTML;
1466          }
1467  
1468          /*----------------------------------------------------------------------------------------------------------------*/
1469  
1470          var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1471              "commonAncestorContainer"];
1472  
1473          var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1474          var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1475  
1476          util.extend(api.rangePrototype, {
1477              compareBoundaryPoints: function(how, range) {
1478                  assertRangeValid(this);
1479                  assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1480  
1481                  var nodeA, offsetA, nodeB, offsetB;
1482                  var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1483                  var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1484                  nodeA = this[prefixA + "Container"];
1485                  offsetA = this[prefixA + "Offset"];
1486                  nodeB = range[prefixB + "Container"];
1487                  offsetB = range[prefixB + "Offset"];
1488                  return comparePoints(nodeA, offsetA, nodeB, offsetB);
1489              },
1490  
1491              insertNode: function(node) {
1492                  assertRangeValid(this);
1493                  assertValidNodeType(node, insertableNodeTypes);
1494                  assertNodeNotReadOnly(this.startContainer);
1495  
1496                  if (isOrIsAncestorOf(node, this.startContainer)) {
1497                      throw new DOMException("HIERARCHY_REQUEST_ERR");
1498                  }
1499  
1500                  // No check for whether the container of the start of the Range is of a type that does not allow
1501                  // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1502                  // to add the node
1503  
1504                  var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1505                  this.setStartBefore(firstNodeInserted);
1506              },
1507  
1508              cloneContents: function() {
1509                  assertRangeValid(this);
1510  
1511                  var clone, frag;
1512                  if (this.collapsed) {
1513                      return getRangeDocument(this).createDocumentFragment();
1514                  } else {
1515                      if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1516                          clone = this.startContainer.cloneNode(true);
1517                          clone.data = clone.data.slice(this.startOffset, this.endOffset);
1518                          frag = getRangeDocument(this).createDocumentFragment();
1519                          frag.appendChild(clone);
1520                          return frag;
1521                      } else {
1522                          var iterator = new RangeIterator(this, true);
1523                          clone = cloneSubtree(iterator);
1524                          iterator.detach();
1525                      }
1526                      return clone;
1527                  }
1528              },
1529  
1530              canSurroundContents: function() {
1531                  assertRangeValid(this);
1532                  assertNodeNotReadOnly(this.startContainer);
1533                  assertNodeNotReadOnly(this.endContainer);
1534  
1535                  // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1536                  // no non-text nodes.
1537                  var iterator = new RangeIterator(this, true);
1538                  var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1539                          (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1540                  iterator.detach();
1541                  return !boundariesInvalid;
1542              },
1543  
1544              surroundContents: function(node) {
1545                  assertValidNodeType(node, surroundNodeTypes);
1546  
1547                  if (!this.canSurroundContents()) {
1548                      throw new DOMException("INVALID_STATE_ERR");
1549                  }
1550  
1551                  // Extract the contents
1552                  var content = this.extractContents();
1553  
1554                  // Clear the children of the node
1555                  if (node.hasChildNodes()) {
1556                      while (node.lastChild) {
1557                          node.removeChild(node.lastChild);
1558                      }
1559                  }
1560  
1561                  // Insert the new node and add the extracted contents
1562                  insertNodeAtPosition(node, this.startContainer, this.startOffset);
1563                  node.appendChild(content);
1564  
1565                  this.selectNode(node);
1566              },
1567  
1568              cloneRange: function() {
1569                  assertRangeValid(this);
1570                  var range = new Range(getRangeDocument(this));
1571                  var i = rangeProperties.length, prop;
1572                  while (i--) {
1573                      prop = rangeProperties[i];
1574                      range[prop] = this[prop];
1575                  }
1576                  return range;
1577              },
1578  
1579              toString: function() {
1580                  assertRangeValid(this);
1581                  var sc = this.startContainer;
1582                  if (sc === this.endContainer && isCharacterDataNode(sc)) {
1583                      return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1584                  } else {
1585                      var textParts = [], iterator = new RangeIterator(this, true);
1586                      iterateSubtree(iterator, function(node) {
1587                          // Accept only text or CDATA nodes, not comments
1588                          if (node.nodeType == 3 || node.nodeType == 4) {
1589                              textParts.push(node.data);
1590                          }
1591                      });
1592                      iterator.detach();
1593                      return textParts.join("");
1594                  }
1595              },
1596  
1597              // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1598              // been removed from Mozilla.
1599  
1600              compareNode: function(node) {
1601                  assertRangeValid(this);
1602  
1603                  var parent = node.parentNode;
1604                  var nodeIndex = getNodeIndex(node);
1605  
1606                  if (!parent) {
1607                      throw new DOMException("NOT_FOUND_ERR");
1608                  }
1609  
1610                  var startComparison = this.comparePoint(parent, nodeIndex),
1611                      endComparison = this.comparePoint(parent, nodeIndex + 1);
1612  
1613                  if (startComparison < 0) { // Node starts before
1614                      return (endComparison > 0) ? n_b_a : n_b;
1615                  } else {
1616                      return (endComparison > 0) ? n_a : n_i;
1617                  }
1618              },
1619  
1620              comparePoint: function(node, offset) {
1621                  assertRangeValid(this);
1622                  assertNode(node, "HIERARCHY_REQUEST_ERR");
1623                  assertSameDocumentOrFragment(node, this.startContainer);
1624  
1625                  if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1626                      return -1;
1627                  } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1628                      return 1;
1629                  }
1630                  return 0;
1631              },
1632  
1633              createContextualFragment: createContextualFragment,
1634  
1635              toHtml: function() {
1636                  return rangeToHtml(this);
1637              },
1638  
1639              // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1640              // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1641              intersectsNode: function(node, touchingIsIntersecting) {
1642                  assertRangeValid(this);
1643                  if (getRootContainer(node) != getRangeRoot(this)) {
1644                      return false;
1645                  }
1646  
1647                  var parent = node.parentNode, offset = getNodeIndex(node);
1648                  if (!parent) {
1649                      return true;
1650                  }
1651  
1652                  var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1653                      endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1654  
1655                  return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1656              },
1657  
1658              isPointInRange: function(node, offset) {
1659                  assertRangeValid(this);
1660                  assertNode(node, "HIERARCHY_REQUEST_ERR");
1661                  assertSameDocumentOrFragment(node, this.startContainer);
1662  
1663                  return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1664                         (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1665              },
1666  
1667              // The methods below are non-standard and invented by me.
1668  
1669              // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1670              intersectsRange: function(range) {
1671                  return rangesIntersect(this, range, false);
1672              },
1673  
1674              // Sharing a boundary start-to-end or end-to-start does count as intersection.
1675              intersectsOrTouchesRange: function(range) {
1676                  return rangesIntersect(this, range, true);
1677              },
1678  
1679              intersection: function(range) {
1680                  if (this.intersectsRange(range)) {
1681                      var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1682                          endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1683  
1684                      var intersectionRange = this.cloneRange();
1685                      if (startComparison == -1) {
1686                          intersectionRange.setStart(range.startContainer, range.startOffset);
1687                      }
1688                      if (endComparison == 1) {
1689                          intersectionRange.setEnd(range.endContainer, range.endOffset);
1690                      }
1691                      return intersectionRange;
1692                  }
1693                  return null;
1694              },
1695  
1696              union: function(range) {
1697                  if (this.intersectsOrTouchesRange(range)) {
1698                      var unionRange = this.cloneRange();
1699                      if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1700                          unionRange.setStart(range.startContainer, range.startOffset);
1701                      }
1702                      if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1703                          unionRange.setEnd(range.endContainer, range.endOffset);
1704                      }
1705                      return unionRange;
1706                  } else {
1707                      throw new DOMException("Ranges do not intersect");
1708                  }
1709              },
1710  
1711              containsNode: function(node, allowPartial) {
1712                  if (allowPartial) {
1713                      return this.intersectsNode(node, false);
1714                  } else {
1715                      return this.compareNode(node) == n_i;
1716                  }
1717              },
1718  
1719              containsNodeContents: function(node) {
1720                  return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1721              },
1722  
1723              containsRange: function(range) {
1724                  var intersection = this.intersection(range);
1725                  return intersection !== null && range.equals(intersection);
1726              },
1727  
1728              containsNodeText: function(node) {
1729                  var nodeRange = this.cloneRange();
1730                  nodeRange.selectNode(node);
1731                  var textNodes = nodeRange.getNodes([3]);
1732                  if (textNodes.length > 0) {
1733                      nodeRange.setStart(textNodes[0], 0);
1734                      var lastTextNode = textNodes.pop();
1735                      nodeRange.setEnd(lastTextNode, lastTextNode.length);
1736                      return this.containsRange(nodeRange);
1737                  } else {
1738                      return this.containsNodeContents(node);
1739                  }
1740              },
1741  
1742              getNodes: function(nodeTypes, filter) {
1743                  assertRangeValid(this);
1744                  return getNodesInRange(this, nodeTypes, filter);
1745              },
1746  
1747              getDocument: function() {
1748                  return getRangeDocument(this);
1749              },
1750  
1751              collapseBefore: function(node) {
1752                  this.setEndBefore(node);
1753                  this.collapse(false);
1754              },
1755  
1756              collapseAfter: function(node) {
1757                  this.setStartAfter(node);
1758                  this.collapse(true);
1759              },
1760  
1761              getBookmark: function(containerNode) {
1762                  var doc = getRangeDocument(this);
1763                  var preSelectionRange = api.createRange(doc);
1764                  containerNode = containerNode || dom.getBody(doc);
1765                  preSelectionRange.selectNodeContents(containerNode);
1766                  var range = this.intersection(preSelectionRange);
1767                  var start = 0, end = 0;
1768                  if (range) {
1769                      preSelectionRange.setEnd(range.startContainer, range.startOffset);
1770                      start = preSelectionRange.toString().length;
1771                      end = start + range.toString().length;
1772                  }
1773  
1774                  return {
1775                      start: start,
1776                      end: end,
1777                      containerNode: containerNode
1778                  };
1779              },
1780  
1781              moveToBookmark: function(bookmark) {
1782                  var containerNode = bookmark.containerNode;
1783                  var charIndex = 0;
1784                  this.setStart(containerNode, 0);
1785                  this.collapse(true);
1786                  var nodeStack = [containerNode], node, foundStart = false, stop = false;
1787                  var nextCharIndex, i, childNodes;
1788  
1789                  while (!stop && (node = nodeStack.pop())) {
1790                      if (node.nodeType == 3) {
1791                          nextCharIndex = charIndex + node.length;
1792                          if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1793                              this.setStart(node, bookmark.start - charIndex);
1794                              foundStart = true;
1795                          }
1796                          if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1797                              this.setEnd(node, bookmark.end - charIndex);
1798                              stop = true;
1799                          }
1800                          charIndex = nextCharIndex;
1801                      } else {
1802                          childNodes = node.childNodes;
1803                          i = childNodes.length;
1804                          while (i--) {
1805                              nodeStack.push(childNodes[i]);
1806                          }
1807                      }
1808                  }
1809              },
1810  
1811              getName: function() {
1812                  return "DomRange";
1813              },
1814  
1815              equals: function(range) {
1816                  return Range.rangesEqual(this, range);
1817              },
1818  
1819              isValid: function() {
1820                  return isRangeValid(this);
1821              },
1822  
1823              inspect: function() {
1824                  return inspect(this);
1825              },
1826  
1827              detach: function() {
1828                  // In DOM4, detach() is now a no-op.
1829              }
1830          });
1831  
1832          function copyComparisonConstantsToObject(obj) {
1833              obj.START_TO_START = s2s;
1834              obj.START_TO_END = s2e;
1835              obj.END_TO_END = e2e;
1836              obj.END_TO_START = e2s;
1837  
1838              obj.NODE_BEFORE = n_b;
1839              obj.NODE_AFTER = n_a;
1840              obj.NODE_BEFORE_AND_AFTER = n_b_a;
1841              obj.NODE_INSIDE = n_i;
1842          }
1843  
1844          function copyComparisonConstants(constructor) {
1845              copyComparisonConstantsToObject(constructor);
1846              copyComparisonConstantsToObject(constructor.prototype);
1847          }
1848  
1849          function createRangeContentRemover(remover, boundaryUpdater) {
1850              return function() {
1851                  assertRangeValid(this);
1852  
1853                  var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1854  
1855                  var iterator = new RangeIterator(this, true);
1856  
1857                  // Work out where to position the range after content removal
1858                  var node, boundary;
1859                  if (sc !== root) {
1860                      node = getClosestAncestorIn(sc, root, true);
1861                      boundary = getBoundaryAfterNode(node);
1862                      sc = boundary.node;
1863                      so = boundary.offset;
1864                  }
1865  
1866                  // Check none of the range is read-only
1867                  iterateSubtree(iterator, assertNodeNotReadOnly);
1868  
1869                  iterator.reset();
1870  
1871                  // Remove the content
1872                  var returnValue = remover(iterator);
1873                  iterator.detach();
1874  
1875                  // Move to the new position
1876                  boundaryUpdater(this, sc, so, sc, so);
1877  
1878                  return returnValue;
1879              };
1880          }
1881  
1882          function createPrototypeRange(constructor, boundaryUpdater) {
1883              function createBeforeAfterNodeSetter(isBefore, isStart) {
1884                  return function(node) {
1885                      assertValidNodeType(node, beforeAfterNodeTypes);
1886                      assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1887  
1888                      var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1889                      (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1890                  };
1891              }
1892  
1893              function setRangeStart(range, node, offset) {
1894                  var ec = range.endContainer, eo = range.endOffset;
1895                  if (node !== range.startContainer || offset !== range.startOffset) {
1896                      // Check the root containers of the range and the new boundary, and also check whether the new boundary
1897                      // is after the current end. In either case, collapse the range to the new position
1898                      if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1899                          ec = node;
1900                          eo = offset;
1901                      }
1902                      boundaryUpdater(range, node, offset, ec, eo);
1903                  }
1904              }
1905  
1906              function setRangeEnd(range, node, offset) {
1907                  var sc = range.startContainer, so = range.startOffset;
1908                  if (node !== range.endContainer || offset !== range.endOffset) {
1909                      // Check the root containers of the range and the new boundary, and also check whether the new boundary
1910                      // is after the current end. In either case, collapse the range to the new position
1911                      if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1912                          sc = node;
1913                          so = offset;
1914                      }
1915                      boundaryUpdater(range, sc, so, node, offset);
1916                  }
1917              }
1918  
1919              // Set up inheritance
1920              var F = function() {};
1921              F.prototype = api.rangePrototype;
1922              constructor.prototype = new F();
1923  
1924              util.extend(constructor.prototype, {
1925                  setStart: function(node, offset) {
1926                      assertNoDocTypeNotationEntityAncestor(node, true);
1927                      assertValidOffset(node, offset);
1928  
1929                      setRangeStart(this, node, offset);
1930                  },
1931  
1932                  setEnd: function(node, offset) {
1933                      assertNoDocTypeNotationEntityAncestor(node, true);
1934                      assertValidOffset(node, offset);
1935  
1936                      setRangeEnd(this, node, offset);
1937                  },
1938  
1939                  /**
1940                   * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1941                   * - Two parameters (node, offset) creates a collapsed range at that position
1942                   * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1943                   *   startOffset and ending at endOffset
1944                   * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1945                   *   startNode and ending at endOffset in endNode
1946                   */
1947                  setStartAndEnd: function() {
1948                      var args = arguments;
1949                      var sc = args[0], so = args[1], ec = sc, eo = so;
1950  
1951                      switch (args.length) {
1952                          case 3:
1953                              eo = args[2];
1954                              break;
1955                          case 4:
1956                              ec = args[2];
1957                              eo = args[3];
1958                              break;
1959                      }
1960  
1961                      boundaryUpdater(this, sc, so, ec, eo);
1962                  },
1963  
1964                  setBoundary: function(node, offset, isStart) {
1965                      this["set" + (isStart ? "Start" : "End")](node, offset);
1966                  },
1967  
1968                  setStartBefore: createBeforeAfterNodeSetter(true, true),
1969                  setStartAfter: createBeforeAfterNodeSetter(false, true),
1970                  setEndBefore: createBeforeAfterNodeSetter(true, false),
1971                  setEndAfter: createBeforeAfterNodeSetter(false, false),
1972  
1973                  collapse: function(isStart) {
1974                      assertRangeValid(this);
1975                      if (isStart) {
1976                          boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1977                      } else {
1978                          boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1979                      }
1980                  },
1981  
1982                  selectNodeContents: function(node) {
1983                      assertNoDocTypeNotationEntityAncestor(node, true);
1984  
1985                      boundaryUpdater(this, node, 0, node, getNodeLength(node));
1986                  },
1987  
1988                  selectNode: function(node) {
1989                      assertNoDocTypeNotationEntityAncestor(node, false);
1990                      assertValidNodeType(node, beforeAfterNodeTypes);
1991  
1992                      var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1993                      boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1994                  },
1995  
1996                  extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1997  
1998                  deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
1999  
2000                  canSurroundContents: function() {
2001                      assertRangeValid(this);
2002                      assertNodeNotReadOnly(this.startContainer);
2003                      assertNodeNotReadOnly(this.endContainer);
2004  
2005                      // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2006                      // no non-text nodes.
2007                      var iterator = new RangeIterator(this, true);
2008                      var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2009                              (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2010                      iterator.detach();
2011                      return !boundariesInvalid;
2012                  },
2013  
2014                  splitBoundaries: function() {
2015                      splitRangeBoundaries(this);
2016                  },
2017  
2018                  splitBoundariesPreservingPositions: function(positionsToPreserve) {
2019                      splitRangeBoundaries(this, positionsToPreserve);
2020                  },
2021  
2022                  normalizeBoundaries: function() {
2023                      assertRangeValid(this);
2024  
2025                      var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2026  
2027                      var mergeForward = function(node) {
2028                          var sibling = node.nextSibling;
2029                          if (sibling && sibling.nodeType == node.nodeType) {
2030                              ec = node;
2031                              eo = node.length;
2032                              node.appendData(sibling.data);
2033                              removeNode(sibling);
2034                          }
2035                      };
2036  
2037                      var mergeBackward = function(node) {
2038                          var sibling = node.previousSibling;
2039                          if (sibling && sibling.nodeType == node.nodeType) {
2040                              sc = node;
2041                              var nodeLength = node.length;
2042                              so = sibling.length;
2043                              node.insertData(0, sibling.data);
2044                              removeNode(sibling);
2045                              if (sc == ec) {
2046                                  eo += so;
2047                                  ec = sc;
2048                              } else if (ec == node.parentNode) {
2049                                  var nodeIndex = getNodeIndex(node);
2050                                  if (eo == nodeIndex) {
2051                                      ec = node;
2052                                      eo = nodeLength;
2053                                  } else if (eo > nodeIndex) {
2054                                      eo--;
2055                                  }
2056                              }
2057                          }
2058                      };
2059  
2060                      var normalizeStart = true;
2061                      var sibling;
2062  
2063                      if (isCharacterDataNode(ec)) {
2064                          if (eo == ec.length) {
2065                              mergeForward(ec);
2066                          } else if (eo == 0) {
2067                              sibling = ec.previousSibling;
2068                              if (sibling && sibling.nodeType == ec.nodeType) {
2069                                  eo = sibling.length;
2070                                  if (sc == ec) {
2071                                      normalizeStart = false;
2072                                  }
2073                                  sibling.appendData(ec.data);
2074                                  removeNode(ec);
2075                                  ec = sibling;
2076                              }
2077                          }
2078                      } else {
2079                          if (eo > 0) {
2080                              var endNode = ec.childNodes[eo - 1];
2081                              if (endNode && isCharacterDataNode(endNode)) {
2082                                  mergeForward(endNode);
2083                              }
2084                          }
2085                          normalizeStart = !this.collapsed;
2086                      }
2087  
2088                      if (normalizeStart) {
2089                          if (isCharacterDataNode(sc)) {
2090                              if (so == 0) {
2091                                  mergeBackward(sc);
2092                              } else if (so == sc.length) {
2093                                  sibling = sc.nextSibling;
2094                                  if (sibling && sibling.nodeType == sc.nodeType) {
2095                                      if (ec == sibling) {
2096                                          ec = sc;
2097                                          eo += sc.length;
2098                                      }
2099                                      sc.appendData(sibling.data);
2100                                      removeNode(sibling);
2101                                  }
2102                              }
2103                          } else {
2104                              if (so < sc.childNodes.length) {
2105                                  var startNode = sc.childNodes[so];
2106                                  if (startNode && isCharacterDataNode(startNode)) {
2107                                      mergeBackward(startNode);
2108                                  }
2109                              }
2110                          }
2111                      } else {
2112                          sc = ec;
2113                          so = eo;
2114                      }
2115  
2116                      boundaryUpdater(this, sc, so, ec, eo);
2117                  },
2118  
2119                  collapseToPoint: function(node, offset) {
2120                      assertNoDocTypeNotationEntityAncestor(node, true);
2121                      assertValidOffset(node, offset);
2122                      this.setStartAndEnd(node, offset);
2123                  }
2124              });
2125  
2126              copyComparisonConstants(constructor);
2127          }
2128  
2129          /*----------------------------------------------------------------------------------------------------------------*/
2130  
2131          // Updates commonAncestorContainer and collapsed after boundary change
2132          function updateCollapsedAndCommonAncestor(range) {
2133              range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2134              range.commonAncestorContainer = range.collapsed ?
2135                  range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2136          }
2137  
2138          function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2139              range.startContainer = startContainer;
2140              range.startOffset = startOffset;
2141              range.endContainer = endContainer;
2142              range.endOffset = endOffset;
2143              range.document = dom.getDocument(startContainer);
2144  
2145              updateCollapsedAndCommonAncestor(range);
2146          }
2147  
2148          function Range(doc) {
2149              this.startContainer = doc;
2150              this.startOffset = 0;
2151              this.endContainer = doc;
2152              this.endOffset = 0;
2153              this.document = doc;
2154              updateCollapsedAndCommonAncestor(this);
2155          }
2156  
2157          createPrototypeRange(Range, updateBoundaries);
2158  
2159          util.extend(Range, {
2160              rangeProperties: rangeProperties,
2161              RangeIterator: RangeIterator,
2162              copyComparisonConstants: copyComparisonConstants,
2163              createPrototypeRange: createPrototypeRange,
2164              inspect: inspect,
2165              toHtml: rangeToHtml,
2166              getRangeDocument: getRangeDocument,
2167              rangesEqual: function(r1, r2) {
2168                  return r1.startContainer === r2.startContainer &&
2169                      r1.startOffset === r2.startOffset &&
2170                      r1.endContainer === r2.endContainer &&
2171                      r1.endOffset === r2.endOffset;
2172              }
2173          });
2174  
2175          api.DomRange = Range;
2176      });
2177  
2178      /*----------------------------------------------------------------------------------------------------------------*/

2179  
2180      // Wrappers for the browser's native DOM Range and/or TextRange implementation
2181      api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2182          var WrappedRange, WrappedTextRange;
2183          var dom = api.dom;
2184          var util = api.util;
2185          var DomPosition = dom.DomPosition;
2186          var DomRange = api.DomRange;
2187          var getBody = dom.getBody;
2188          var getContentDocument = dom.getContentDocument;
2189          var isCharacterDataNode = dom.isCharacterDataNode;
2190  
2191  
2192          /*----------------------------------------------------------------------------------------------------------------*/
2193  
2194          if (api.features.implementsDomRange) {
2195              // This is a wrapper around the browser's native DOM Range. It has two aims:
2196              // - Provide workarounds for specific browser bugs
2197              // - provide convenient extensions, which are inherited from Rangy's DomRange
2198  
2199              (function() {
2200                  var rangeProto;
2201                  var rangeProperties = DomRange.rangeProperties;
2202  
2203                  function updateRangeProperties(range) {
2204                      var i = rangeProperties.length, prop;
2205                      while (i--) {
2206                          prop = rangeProperties[i];
2207                          range[prop] = range.nativeRange[prop];
2208                      }
2209                      // Fix for broken collapsed property in IE 9.
2210                      range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2211                  }
2212  
2213                  function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2214                      var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2215                      var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2216                      var nativeRangeDifferent = !range.equals(range.nativeRange);
2217  
2218                      // Always set both boundaries for the benefit of IE9 (see issue 35)
2219                      if (startMoved || endMoved || nativeRangeDifferent) {
2220                          range.setEnd(endContainer, endOffset);
2221                          range.setStart(startContainer, startOffset);
2222                      }
2223                  }
2224  
2225                  var createBeforeAfterNodeSetter;
2226  
2227                  WrappedRange = function(range) {
2228                      if (!range) {
2229                          throw module.createError("WrappedRange: Range must be specified");
2230                      }
2231                      this.nativeRange = range;
2232                      updateRangeProperties(this);
2233                  };
2234  
2235                  DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2236  
2237                  rangeProto = WrappedRange.prototype;
2238  
2239                  rangeProto.selectNode = function(node) {
2240                      this.nativeRange.selectNode(node);
2241                      updateRangeProperties(this);
2242                  };
2243  
2244                  rangeProto.cloneContents = function() {
2245                      return this.nativeRange.cloneContents();
2246                  };
2247  
2248                  // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2249                  // insertNode() is never delegated to the native range.
2250  
2251                  rangeProto.surroundContents = function(node) {
2252                      this.nativeRange.surroundContents(node);
2253                      updateRangeProperties(this);
2254                  };
2255  
2256                  rangeProto.collapse = function(isStart) {
2257                      this.nativeRange.collapse(isStart);
2258                      updateRangeProperties(this);
2259                  };
2260  
2261                  rangeProto.cloneRange = function() {
2262                      return new WrappedRange(this.nativeRange.cloneRange());
2263                  };
2264  
2265                  rangeProto.refresh = function() {
2266                      updateRangeProperties(this);
2267                  };
2268  
2269                  rangeProto.toString = function() {
2270                      return this.nativeRange.toString();
2271                  };
2272  
2273                  // Create test range and node for feature detection
2274  
2275                  var testTextNode = document.createTextNode("test");
2276                  getBody(document).appendChild(testTextNode);
2277                  var range = document.createRange();
2278  
2279                  /*--------------------------------------------------------------------------------------------------------*/
2280  
2281                  // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2282                  // correct for it
2283  
2284                  range.setStart(testTextNode, 0);
2285                  range.setEnd(testTextNode, 0);
2286  
2287                  try {
2288                      range.setStart(testTextNode, 1);
2289  
2290                      rangeProto.setStart = function(node, offset) {
2291                          this.nativeRange.setStart(node, offset);
2292                          updateRangeProperties(this);
2293                      };
2294  
2295                      rangeProto.setEnd = function(node, offset) {
2296                          this.nativeRange.setEnd(node, offset);
2297                          updateRangeProperties(this);
2298                      };
2299  
2300                      createBeforeAfterNodeSetter = function(name) {
2301                          return function(node) {
2302                              this.nativeRange[name](node);
2303                              updateRangeProperties(this);
2304                          };
2305                      };
2306  
2307                  } catch(ex) {
2308  
2309                      rangeProto.setStart = function(node, offset) {
2310                          try {
2311                              this.nativeRange.setStart(node, offset);
2312                          } catch (ex) {
2313                              this.nativeRange.setEnd(node, offset);
2314                              this.nativeRange.setStart(node, offset);
2315                          }
2316                          updateRangeProperties(this);
2317                      };
2318  
2319                      rangeProto.setEnd = function(node, offset) {
2320                          try {
2321                              this.nativeRange.setEnd(node, offset);
2322                          } catch (ex) {
2323                              this.nativeRange.setStart(node, offset);
2324                              this.nativeRange.setEnd(node, offset);
2325                          }
2326                          updateRangeProperties(this);
2327                      };
2328  
2329                      createBeforeAfterNodeSetter = function(name, oppositeName) {
2330                          return function(node) {
2331                              try {
2332                                  this.nativeRange[name](node);
2333                              } catch (ex) {
2334                                  this.nativeRange[oppositeName](node);
2335                                  this.nativeRange[name](node);
2336                              }
2337                              updateRangeProperties(this);
2338                          };
2339                      };
2340                  }
2341  
2342                  rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2343                  rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2344                  rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2345                  rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2346  
2347                  /*--------------------------------------------------------------------------------------------------------*/
2348  
2349                  // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2350                  // whether the native implementation can be trusted
2351                  rangeProto.selectNodeContents = function(node) {
2352                      this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2353                  };
2354  
2355                  /*--------------------------------------------------------------------------------------------------------*/
2356  
2357                  // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2358                  // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2359  
2360                  range.selectNodeContents(testTextNode);
2361                  range.setEnd(testTextNode, 3);
2362  
2363                  var range2 = document.createRange();
2364                  range2.selectNodeContents(testTextNode);
2365                  range2.setEnd(testTextNode, 4);
2366                  range2.setStart(testTextNode, 2);
2367  
2368                  if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2369                          range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2370                      // This is the wrong way round, so correct for it
2371  
2372                      rangeProto.compareBoundaryPoints = function(type, range) {
2373                          range = range.nativeRange || range;
2374                          if (type == range.START_TO_END) {
2375                              type = range.END_TO_START;
2376                          } else if (type == range.END_TO_START) {
2377                              type = range.START_TO_END;
2378                          }
2379                          return this.nativeRange.compareBoundaryPoints(type, range);
2380                      };
2381                  } else {
2382                      rangeProto.compareBoundaryPoints = function(type, range) {
2383                          return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2384                      };
2385                  }
2386  
2387                  /*--------------------------------------------------------------------------------------------------------*/
2388  
2389                  // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2390  
2391                  var el = document.createElement("div");
2392                  el.innerHTML = "123";
2393                  var textNode = el.firstChild;
2394                  var body = getBody(document);
2395                  body.appendChild(el);
2396  
2397                  range.setStart(textNode, 1);
2398                  range.setEnd(textNode, 2);
2399                  range.deleteContents();
2400  
2401                  if (textNode.data == "13") {
2402                      // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2403                      // extractContents()
2404                      rangeProto.deleteContents = function() {
2405                          this.nativeRange.deleteContents();
2406                          updateRangeProperties(this);
2407                      };
2408  
2409                      rangeProto.extractContents = function() {
2410                          var frag = this.nativeRange.extractContents();
2411                          updateRangeProperties(this);
2412                          return frag;
2413                      };
2414                  } else {
2415                  }
2416  
2417                  body.removeChild(el);
2418                  body = null;
2419  
2420                  /*--------------------------------------------------------------------------------------------------------*/
2421  
2422                  // Test for existence of createContextualFragment and delegate to it if it exists
2423                  if (util.isHostMethod(range, "createContextualFragment")) {
2424                      rangeProto.createContextualFragment = function(fragmentStr) {
2425                          return this.nativeRange.createContextualFragment(fragmentStr);
2426                      };
2427                  }
2428  
2429                  /*--------------------------------------------------------------------------------------------------------*/
2430  
2431                  // Clean up
2432                  getBody(document).removeChild(testTextNode);
2433  
2434                  rangeProto.getName = function() {
2435                      return "WrappedRange";
2436                  };
2437  
2438                  api.WrappedRange = WrappedRange;
2439  
2440                  api.createNativeRange = function(doc) {
2441                      doc = getContentDocument(doc, module, "createNativeRange");
2442                      return doc.createRange();
2443                  };
2444              })();
2445          }
2446  
2447          if (api.features.implementsTextRange) {
2448              /*
2449              This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2450              method. For example, in the following (where pipes denote the selection boundaries):
2451  
2452              <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2453  
2454              var range = document.selection.createRange();
2455              alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2456  
2457              This method returns the common ancestor node of the following:
2458              - the parentElement() of the textRange
2459              - the parentElement() of the textRange after calling collapse(true)
2460              - the parentElement() of the textRange after calling collapse(false)
2461              */
2462              var getTextRangeContainerElement = function(textRange) {
2463                  var parentEl = textRange.parentElement();
2464                  var range = textRange.duplicate();
2465                  range.collapse(true);
2466                  var startEl = range.parentElement();
2467                  range = textRange.duplicate();
2468                  range.collapse(false);
2469                  var endEl = range.parentElement();
2470                  var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2471  
2472                  return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2473              };
2474  
2475              var textRangeIsCollapsed = function(textRange) {
2476                  return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2477              };
2478  
2479              // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2480              // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2481              // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2482              // bugs, handling for inputs and images, plus optimizations.
2483              var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2484                  var workingRange = textRange.duplicate();
2485                  workingRange.collapse(isStart);
2486                  var containerElement = workingRange.parentElement();
2487  
2488                  // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2489                  // check for that
2490                  if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2491                      containerElement = wholeRangeContainerElement;
2492                  }
2493  
2494  
2495                  // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2496                  // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2497                  if (!containerElement.canHaveHTML) {
2498                      var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2499                      return {
2500                          boundaryPosition: pos,
2501                          nodeInfo: {
2502                              nodeIndex: pos.offset,
2503                              containerElement: pos.node
2504                          }
2505                      };
2506                  }
2507  
2508                  var workingNode = dom.getDocument(containerElement).createElement("span");
2509  
2510                  // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2511                  // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2512                  if (workingNode.parentNode) {
2513                      dom.removeNode(workingNode);
2514                  }
2515  
2516                  var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2517                  var previousNode, nextNode, boundaryPosition, boundaryNode;
2518                  var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2519                  var childNodeCount = containerElement.childNodes.length;
2520                  var end = childNodeCount;
2521  
2522                  // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2523                  // after the range boundary.
2524                  var nodeIndex = end;
2525  
2526                  while (true) {
2527                      if (nodeIndex == childNodeCount) {
2528                          containerElement.appendChild(workingNode);
2529                      } else {
2530                          containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2531                      }
2532                      workingRange.moveToElementText(workingNode);
2533                      comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2534                      if (comparison == 0 || start == end) {
2535                          break;
2536                      } else if (comparison == -1) {
2537                          if (end == start + 1) {
2538                              // We know the endth child node is after the range boundary, so we must be done.
2539                              break;
2540                          } else {
2541                              start = nodeIndex;
2542                          }
2543                      } else {
2544                          end = (end == start + 1) ? start : nodeIndex;
2545                      }
2546                      nodeIndex = Math.floor((start + end) / 2);
2547                      containerElement.removeChild(workingNode);
2548                  }
2549  
2550  
2551                  // We've now reached or gone past the boundary of the text range we're interested in
2552                  // so have identified the node we want
2553                  boundaryNode = workingNode.nextSibling;
2554  
2555                  if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2556                      // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2557                      // the node containing the text range's boundary, so we move the end of the working range to the
2558                      // boundary point and measure the length of its text to get the boundary's offset within the node.
2559                      workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2560  
2561                      var offset;
2562  
2563                      if (/[\r\n]/.test(boundaryNode.data)) {
2564                          /*
2565                          For the particular case of a boundary within a text node containing rendered line breaks (within a
2566                          <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2567                          IE. The facts:
2568  
2569                          - Each line break is represented as \r in the text node's data/nodeValue properties
2570                          - Each line break is represented as \r\n in the TextRange's 'text' property
2571                          - The 'text' property of the TextRange does not contain trailing line breaks
2572  
2573                          To get round the problem presented by the final fact above, we can use the fact that TextRange's
2574                          moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2575                          necessarily the same as the number of characters it was instructed to move. The simplest approach is
2576                          to use this to store the characters moved when moving both the start and end of the range to the
2577                          start of the document body and subtracting the start offset from the end offset (the
2578                          "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2579                          the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2580                          the end of the document) has the same problem.
2581  
2582                          Another approach that works is to use moveStart() to move the start boundary of the range up to the
2583                          end boundary one character at a time and incrementing a counter with the value returned by the
2584                          moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2585                          expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2586                          by the location of the range within the document).
2587  
2588                          The approach used below is a hybrid of the two methods above. It uses the fact that a string
2589                          containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2590                          be longer than the text of the TextRange, so the start of the range is moved that length initially
2591                          and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2592                          property. This has good performance in most situations compared to the previous two methods.
2593                          */
2594                          var tempRange = workingRange.duplicate();
2595                          var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2596  
2597                          offset = tempRange.moveStart("character", rangeLength);
2598                          while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2599                              offset++;
2600                              tempRange.moveStart("character", 1);
2601                          }
2602                      } else {
2603                          offset = workingRange.text.length;
2604                      }
2605                      boundaryPosition = new DomPosition(boundaryNode, offset);
2606                  } else {
2607  
2608                      // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2609                      // a position within that, and likewise for a start boundary preceding a character data node
2610                      previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2611                      nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2612                      if (nextNode && isCharacterDataNode(nextNode)) {
2613                          boundaryPosition = new DomPosition(nextNode, 0);
2614                      } else if (previousNode && isCharacterDataNode(previousNode)) {
2615                          boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2616                      } else {
2617                          boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2618                      }
2619                  }
2620  
2621                  // Clean up
2622                  dom.removeNode(workingNode);
2623  
2624                  return {
2625                      boundaryPosition: boundaryPosition,
2626                      nodeInfo: {
2627                          nodeIndex: nodeIndex,
2628                          containerElement: containerElement
2629                      }
2630                  };
2631              };
2632  
2633              // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2634              // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2635              // (http://code.google.com/p/ierange/)
2636              var createBoundaryTextRange = function(boundaryPosition, isStart) {
2637                  var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2638                  var doc = dom.getDocument(boundaryPosition.node);
2639                  var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2640                  var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2641  
2642                  if (nodeIsDataNode) {
2643                      boundaryNode = boundaryPosition.node;
2644                      boundaryParent = boundaryNode.parentNode;
2645                  } else {
2646                      childNodes = boundaryPosition.node.childNodes;
2647                      boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2648                      boundaryParent = boundaryPosition.node;
2649                  }
2650  
2651                  // Position the range immediately before the node containing the boundary
2652                  workingNode = doc.createElement("span");
2653  
2654                  // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2655                  // the element rather than immediately before or after it
2656                  workingNode.innerHTML = "&#feff;";
2657  
2658                  // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2659                  // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2660                  if (boundaryNode) {
2661                      boundaryParent.insertBefore(workingNode, boundaryNode);
2662                  } else {
2663                      boundaryParent.appendChild(workingNode);
2664                  }
2665  
2666                  workingRange.moveToElementText(workingNode);
2667                  workingRange.collapse(!isStart);
2668  
2669                  // Clean up
2670                  boundaryParent.removeChild(workingNode);
2671  
2672                  // Move the working range to the text offset, if required
2673                  if (nodeIsDataNode) {
2674                      workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2675                  }
2676  
2677                  return workingRange;
2678              };
2679  
2680              /*------------------------------------------------------------------------------------------------------------*/
2681  
2682              // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2683              // prototype
2684  
2685              WrappedTextRange = function(textRange) {
2686                  this.textRange = textRange;
2687                  this.refresh();
2688              };
2689  
2690              WrappedTextRange.prototype = new DomRange(document);
2691  
2692              WrappedTextRange.prototype.refresh = function() {
2693                  var start, end, startBoundary;
2694  
2695                  // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2696                  var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2697  
2698                  if (textRangeIsCollapsed(this.textRange)) {
2699                      end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2700                          true).boundaryPosition;
2701                  } else {
2702                      startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2703                      start = startBoundary.boundaryPosition;
2704  
2705                      // An optimization used here is that if the start and end boundaries have the same parent element, the
2706                      // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2707                      // the start boundary
2708                      end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2709                          startBoundary.nodeInfo).boundaryPosition;
2710                  }
2711  
2712                  this.setStart(start.node, start.offset);
2713                  this.setEnd(end.node, end.offset);
2714              };
2715  
2716              WrappedTextRange.prototype.getName = function() {
2717                  return "WrappedTextRange";
2718              };
2719  
2720              DomRange.copyComparisonConstants(WrappedTextRange);
2721  
2722              var rangeToTextRange = function(range) {
2723                  if (range.collapsed) {
2724                      return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2725                  } else {
2726                      var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2727                      var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2728                      var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2729                      textRange.setEndPoint("StartToStart", startRange);
2730                      textRange.setEndPoint("EndToEnd", endRange);
2731                      return textRange;
2732                  }
2733              };
2734  
2735              WrappedTextRange.rangeToTextRange = rangeToTextRange;
2736  
2737              WrappedTextRange.prototype.toTextRange = function() {
2738                  return rangeToTextRange(this);
2739              };
2740  
2741              api.WrappedTextRange = WrappedTextRange;
2742  
2743              // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2744              // implementation to use by default.
2745              if (!api.features.implementsDomRange || api.config.preferTextRange) {
2746                  // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2747                  var globalObj = (function(f) { return f("return this;")(); })(Function);
2748                  if (typeof globalObj.Range == "undefined") {
2749                      globalObj.Range = WrappedTextRange;
2750                  }
2751  
2752                  api.createNativeRange = function(doc) {
2753                      doc = getContentDocument(doc, module, "createNativeRange");
2754                      return getBody(doc).createTextRange();
2755                  };
2756  
2757                  api.WrappedRange = WrappedTextRange;
2758              }
2759          }
2760  
2761          api.createRange = function(doc) {
2762              doc = getContentDocument(doc, module, "createRange");
2763              return new api.WrappedRange(api.createNativeRange(doc));
2764          };
2765  
2766          api.createRangyRange = function(doc) {
2767              doc = getContentDocument(doc, module, "createRangyRange");
2768              return new DomRange(doc);
2769          };
2770  
2771          util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
2772          util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
2773  
2774          api.addShimListener(function(win) {
2775              var doc = win.document;
2776              if (typeof doc.createRange == "undefined") {
2777                  doc.createRange = function() {
2778                      return api.createRange(doc);
2779                  };
2780              }
2781              doc = win = null;
2782          });
2783      });
2784  
2785      /*----------------------------------------------------------------------------------------------------------------*/

2786  
2787      // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2788      // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
2789      api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2790          api.config.checkSelectionRanges = true;
2791  
2792          var BOOLEAN = "boolean";
2793          var NUMBER = "number";
2794          var dom = api.dom;
2795          var util = api.util;
2796          var isHostMethod = util.isHostMethod;
2797          var DomRange = api.DomRange;
2798          var WrappedRange = api.WrappedRange;
2799          var DOMException = api.DOMException;
2800          var DomPosition = dom.DomPosition;
2801          var getNativeSelection;
2802          var selectionIsCollapsed;
2803          var features = api.features;
2804          var CONTROL = "Control";
2805          var getDocument = dom.getDocument;
2806          var getBody = dom.getBody;
2807          var rangesEqual = DomRange.rangesEqual;
2808  
2809  
2810          // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
2811          // "forward" or "forwards") or a Boolean (true for backwards).
2812          function isDirectionBackward(dir) {
2813              return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2814          }
2815  
2816          function getWindow(win, methodName) {
2817              if (!win) {
2818                  return window;
2819              } else if (dom.isWindow(win)) {
2820                  return win;
2821              } else if (win instanceof WrappedSelection) {
2822                  return win.win;
2823              } else {
2824                  var doc = dom.getContentDocument(win, module, methodName);
2825                  return dom.getWindow(doc);
2826              }
2827          }
2828  
2829          function getWinSelection(winParam) {
2830              return getWindow(winParam, "getWinSelection").getSelection();
2831          }
2832  
2833          function getDocSelection(winParam) {
2834              return getWindow(winParam, "getDocSelection").document.selection;
2835          }
2836  
2837          function winSelectionIsBackward(sel) {
2838              var backward = false;
2839              if (sel.anchorNode) {
2840                  backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2841              }
2842              return backward;
2843          }
2844  
2845          // Test for the Range/TextRange and Selection features required
2846          // Test for ability to retrieve selection
2847          var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2848              implementsDocSelection = util.isHostObject(document, "selection");
2849  
2850          features.implementsWinGetSelection = implementsWinGetSelection;
2851          features.implementsDocSelection = implementsDocSelection;
2852  
2853          var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2854  
2855          if (useDocumentSelection) {
2856              getNativeSelection = getDocSelection;
2857              api.isSelectionValid = function(winParam) {
2858                  var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2859  
2860                  // Check whether the selection TextRange is actually contained within the correct document
2861                  return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2862              };
2863          } else if (implementsWinGetSelection) {
2864              getNativeSelection = getWinSelection;
2865              api.isSelectionValid = function() {
2866                  return true;
2867              };
2868          } else {
2869              module.fail("Neither document.selection or window.getSelection() detected.");
2870              return false;
2871          }
2872  
2873          api.getNativeSelection = getNativeSelection;
2874  
2875          var testSelection = getNativeSelection();
2876  
2877          // In Firefox, the selection is null in an iframe with display: none. See issue #138.
2878          if (!testSelection) {
2879              module.fail("Native selection was null (possibly issue 138?)");
2880              return false;
2881          }
2882  
2883          var testRange = api.createNativeRange(document);
2884          var body = getBody(document);
2885  
2886          // Obtaining a range from a selection
2887          var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2888              ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2889  
2890          features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2891  
2892          // Test for existence of native selection extend() method
2893          var selectionHasExtend = isHostMethod(testSelection, "extend");
2894          features.selectionHasExtend = selectionHasExtend;
2895  
2896          // Test if rangeCount exists
2897          var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2898          features.selectionHasRangeCount = selectionHasRangeCount;
2899  
2900          var selectionSupportsMultipleRanges = false;
2901          var collapsedNonEditableSelectionsSupported = true;
2902  
2903          var addRangeBackwardToNative = selectionHasExtend ?
2904              function(nativeSelection, range) {
2905                  var doc = DomRange.getRangeDocument(range);
2906                  var endRange = api.createRange(doc);
2907                  endRange.collapseToPoint(range.endContainer, range.endOffset);
2908                  nativeSelection.addRange(getNativeRange(endRange));
2909                  nativeSelection.extend(range.startContainer, range.startOffset);
2910              } : null;
2911  
2912          if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2913                  typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2914  
2915              (function() {
2916                  // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2917                  // performed on the current document's selection. See issue 109.
2918  
2919                  // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
2920                  // will result in the selection direction begin reversed if the original selection was backwards and the
2921                  // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
2922                  var sel = window.getSelection();
2923                  if (sel) {
2924                      // Store the current selection
2925                      var originalSelectionRangeCount = sel.rangeCount;
2926                      var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2927                      var originalSelectionRanges = [];
2928                      var originalSelectionBackward = winSelectionIsBackward(sel);
2929                      for (var i = 0; i < originalSelectionRangeCount; ++i) {
2930                          originalSelectionRanges[i] = sel.getRangeAt(i);
2931                      }
2932  
2933                      // Create some test elements
2934                      var testEl = dom.createTestElement(document, "", false);
2935                      var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2936  
2937                      // Test whether the native selection will allow a collapsed selection within a non-editable element
2938                      var r1 = document.createRange();
2939  
2940                      r1.setStart(textNode, 1);
2941                      r1.collapse(true);
2942                      sel.removeAllRanges();
2943                      sel.addRange(r1);
2944                      collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2945                      sel.removeAllRanges();
2946  
2947                      // Test whether the native selection is capable of supporting multiple ranges.
2948                      if (!selectionHasMultipleRanges) {
2949                          // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2950                          // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2951                          // nothing we can do about this while retaining the feature test so we have to resort to a browser
2952                          // sniff. I'm not happy about it. See
2953                          // https://code.google.com/p/chromium/issues/detail?id=399791
2954                          var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2955                          if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2956                              selectionSupportsMultipleRanges = false;
2957                          } else {
2958                              var r2 = r1.cloneRange();
2959                              r1.setStart(textNode, 0);
2960                              r2.setEnd(textNode, 3);
2961                              r2.setStart(textNode, 2);
2962                              sel.addRange(r1);
2963                              sel.addRange(r2);
2964                              selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2965                          }
2966                      }
2967  
2968                      // Clean up
2969                      dom.removeNode(testEl);
2970                      sel.removeAllRanges();
2971  
2972                      for (i = 0; i < originalSelectionRangeCount; ++i) {
2973                          if (i == 0 && originalSelectionBackward) {
2974                              if (addRangeBackwardToNative) {
2975                                  addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2976                              } else {
2977                                  api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2978                                  sel.addRange(originalSelectionRanges[i]);
2979                              }
2980                          } else {
2981                              sel.addRange(originalSelectionRanges[i]);
2982                          }
2983                      }
2984                  }
2985              })();
2986          }
2987  
2988          features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2989          features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2990  
2991          // ControlRanges
2992          var implementsControlRange = false, testControlRange;
2993  
2994          if (body && isHostMethod(body, "createControlRange")) {
2995              testControlRange = body.createControlRange();
2996              if (util.areHostProperties(testControlRange, ["item", "add"])) {
2997                  implementsControlRange = true;
2998              }
2999          }
3000          features.implementsControlRange = implementsControlRange;
3001  
3002          // Selection collapsedness
3003          if (selectionHasAnchorAndFocus) {
3004              selectionIsCollapsed = function(sel) {
3005                  return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3006              };
3007          } else {
3008              selectionIsCollapsed = function(sel) {
3009                  return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3010              };
3011          }
3012  
3013          function updateAnchorAndFocusFromRange(sel, range, backward) {
3014              var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3015              sel.anchorNode = range[anchorPrefix + "Container"];
3016              sel.anchorOffset = range[anchorPrefix + "Offset"];
3017              sel.focusNode = range[focusPrefix + "Container"];
3018              sel.focusOffset = range[focusPrefix + "Offset"];
3019          }
3020  
3021          function updateAnchorAndFocusFromNativeSelection(sel) {
3022              var nativeSel = sel.nativeSelection;
3023              sel.anchorNode = nativeSel.anchorNode;
3024              sel.anchorOffset = nativeSel.anchorOffset;
3025              sel.focusNode = nativeSel.focusNode;
3026              sel.focusOffset = nativeSel.focusOffset;
3027          }
3028  
3029          function updateEmptySelection(sel) {
3030              sel.anchorNode = sel.focusNode = null;
3031              sel.anchorOffset = sel.focusOffset = 0;
3032              sel.rangeCount = 0;
3033              sel.isCollapsed = true;
3034              sel._ranges.length = 0;
3035          }
3036  
3037          function getNativeRange(range) {
3038              var nativeRange;
3039              if (range instanceof DomRange) {
3040                  nativeRange = api.createNativeRange(range.getDocument());
3041                  nativeRange.setEnd(range.endContainer, range.endOffset);
3042                  nativeRange.setStart(range.startContainer, range.startOffset);
3043              } else if (range instanceof WrappedRange) {
3044                  nativeRange = range.nativeRange;
3045              } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3046                  nativeRange = range;
3047              }
3048              return nativeRange;
3049          }
3050  
3051          function rangeContainsSingleElement(rangeNodes) {
3052              if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3053                  return false;
3054              }
3055              for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3056                  if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3057                      return false;
3058                  }
3059              }
3060              return true;
3061          }
3062  
3063          function getSingleElementFromRange(range) {
3064              var nodes = range.getNodes();
3065              if (!rangeContainsSingleElement(nodes)) {
3066                  throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3067              }
3068              return nodes[0];
3069          }
3070  
3071          // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3072          function isTextRange(range) {
3073              return !!range && typeof range.text != "undefined";
3074          }
3075  
3076          function updateFromTextRange(sel, range) {
3077              // Create a Range from the selected TextRange
3078              var wrappedRange = new WrappedRange(range);
3079              sel._ranges = [wrappedRange];
3080  
3081              updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3082              sel.rangeCount = 1;
3083              sel.isCollapsed = wrappedRange.collapsed;
3084          }
3085  
3086          function updateControlSelection(sel) {
3087              // Update the wrapped selection based on what's now in the native selection
3088              sel._ranges.length = 0;
3089              if (sel.docSelection.type == "None") {
3090                  updateEmptySelection(sel);
3091              } else {
3092                  var controlRange = sel.docSelection.createRange();
3093                  if (isTextRange(controlRange)) {
3094                      // This case (where the selection type is "Control" and calling createRange() on the selection returns
3095                      // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3096                      // ControlRange have been removed from the ControlRange and removed from the document.
3097                      updateFromTextRange(sel, controlRange);
3098                  } else {
3099                      sel.rangeCount = controlRange.length;
3100                      var range, doc = getDocument(controlRange.item(0));
3101                      for (var i = 0; i < sel.rangeCount; ++i) {
3102                          range = api.createRange(doc);
3103                          range.selectNode(controlRange.item(i));
3104                          sel._ranges.push(range);
3105                      }
3106                      sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3107                      updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3108                  }
3109              }
3110          }
3111  
3112          function addRangeToControlSelection(sel, range) {
3113              var controlRange = sel.docSelection.createRange();
3114              var rangeElement = getSingleElementFromRange(range);
3115  
3116              // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3117              // contained by the supplied range
3118              var doc = getDocument(controlRange.item(0));
3119              var newControlRange = getBody(doc).createControlRange();
3120              for (var i = 0, len = controlRange.length; i < len; ++i) {
3121                  newControlRange.add(controlRange.item(i));
3122              }
3123              try {
3124                  newControlRange.add(rangeElement);
3125              } catch (ex) {
3126                  throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3127              }
3128              newControlRange.select();
3129  
3130              // Update the wrapped selection based on what's now in the native selection
3131              updateControlSelection(sel);
3132          }
3133  
3134          var getSelectionRangeAt;
3135  
3136          if (isHostMethod(testSelection, "getRangeAt")) {
3137              // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3138              // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3139              // lesson to us all, especially me.
3140              getSelectionRangeAt = function(sel, index) {
3141                  try {
3142                      return sel.getRangeAt(index);
3143                  } catch (ex) {
3144                      return null;
3145                  }
3146              };
3147          } else if (selectionHasAnchorAndFocus) {
3148              getSelectionRangeAt = function(sel) {
3149                  var doc = getDocument(sel.anchorNode);
3150                  var range = api.createRange(doc);
3151                  range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3152  
3153                  // Handle the case when the selection was selected backwards (from the end to the start in the
3154                  // document)
3155                  if (range.collapsed !== this.isCollapsed) {
3156                      range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3157                  }
3158  
3159                  return range;
3160              };
3161          }
3162  
3163          function WrappedSelection(selection, docSelection, win) {
3164              this.nativeSelection = selection;
3165              this.docSelection = docSelection;
3166              this._ranges = [];
3167              this.win = win;
3168              this.refresh();
3169          }
3170  
3171          WrappedSelection.prototype = api.selectionPrototype;
3172  
3173          function deleteProperties(sel) {
3174              sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3175              sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3176              sel.detached = true;
3177          }
3178  
3179          var cachedRangySelections = [];
3180  
3181          function actOnCachedSelection(win, action) {
3182              var i = cachedRangySelections.length, cached, sel;
3183              while (i--) {
3184                  cached = cachedRangySelections[i];
3185                  sel = cached.selection;
3186                  if (action == "deleteAll") {
3187                      deleteProperties(sel);
3188                  } else if (cached.win == win) {
3189                      if (action == "delete") {
3190                          cachedRangySelections.splice(i, 1);
3191                          return true;
3192                      } else {
3193                          return sel;
3194                      }
3195                  }
3196              }
3197              if (action == "deleteAll") {
3198                  cachedRangySelections.length = 0;
3199              }
3200              return null;
3201          }
3202  
3203          var getSelection = function(win) {
3204              // Check if the parameter is a Rangy Selection object
3205              if (win && win instanceof WrappedSelection) {
3206                  win.refresh();
3207                  return win;
3208              }
3209  
3210              win = getWindow(win, "getNativeSelection");
3211  
3212              var sel = actOnCachedSelection(win);
3213              var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3214              if (sel) {
3215                  sel.nativeSelection = nativeSel;
3216                  sel.docSelection = docSel;
3217                  sel.refresh();
3218              } else {
3219                  sel = new WrappedSelection(nativeSel, docSel, win);
3220                  cachedRangySelections.push( { win: win, selection: sel } );
3221              }
3222              return sel;
3223          };
3224  
3225          api.getSelection = getSelection;
3226  
3227          util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3228  
3229          var selProto = WrappedSelection.prototype;
3230  
3231          function createControlSelection(sel, ranges) {
3232              // Ensure that the selection becomes of type "Control"
3233              var doc = getDocument(ranges[0].startContainer);
3234              var controlRange = getBody(doc).createControlRange();
3235              for (var i = 0, el, len = ranges.length; i < len; ++i) {
3236                  el = getSingleElementFromRange(ranges[i]);
3237                  try {
3238                      controlRange.add(el);
3239                  } catch (ex) {
3240                      throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3241                  }
3242              }
3243              controlRange.select();
3244  
3245              // Update the wrapped selection based on what's now in the native selection
3246              updateControlSelection(sel);
3247          }
3248  
3249          // Selecting a range
3250          if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3251              selProto.removeAllRanges = function() {
3252                  this.nativeSelection.removeAllRanges();
3253                  updateEmptySelection(this);
3254              };
3255  
3256              var addRangeBackward = function(sel, range) {
3257                  addRangeBackwardToNative(sel.nativeSelection, range);
3258                  sel.refresh();
3259              };
3260  
3261              if (selectionHasRangeCount) {
3262                  selProto.addRange = function(range, direction) {
3263                      if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3264                          addRangeToControlSelection(this, range);
3265                      } else {
3266                          if (isDirectionBackward(direction) && selectionHasExtend) {
3267                              addRangeBackward(this, range);
3268                          } else {
3269                              var previousRangeCount;
3270                              if (selectionSupportsMultipleRanges) {
3271                                  previousRangeCount = this.rangeCount;
3272                              } else {
3273                                  this.removeAllRanges();
3274                                  previousRangeCount = 0;
3275                              }
3276                              // Clone the native range so that changing the selected range does not affect the selection.
3277                              // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3278                              // issue 80.
3279                              var clonedNativeRange = getNativeRange(range).cloneRange();
3280                              try {
3281                                  this.nativeSelection.addRange(clonedNativeRange);
3282                              } catch (ex) {
3283                              }
3284  
3285                              // Check whether adding the range was successful
3286                              this.rangeCount = this.nativeSelection.rangeCount;
3287  
3288                              if (this.rangeCount == previousRangeCount + 1) {
3289                                  // The range was added successfully
3290  
3291                                  // Check whether the range that we added to the selection is reflected in the last range extracted from
3292                                  // the selection
3293                                  if (api.config.checkSelectionRanges) {
3294                                      var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3295                                      if (nativeRange && !rangesEqual(nativeRange, range)) {
3296                                          // Happens in WebKit with, for example, a selection placed at the start of a text node
3297                                          range = new WrappedRange(nativeRange);
3298                                      }
3299                                  }
3300                                  this._ranges[this.rangeCount - 1] = range;
3301                                  updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3302                                  this.isCollapsed = selectionIsCollapsed(this);
3303                              } else {
3304                                  // The range was not added successfully. The simplest thing is to refresh
3305                                  this.refresh();
3306                              }
3307                          }
3308                      }
3309                  };
3310              } else {
3311                  selProto.addRange = function(range, direction) {
3312                      if (isDirectionBackward(direction) && selectionHasExtend) {
3313                          addRangeBackward(this, range);
3314                      } else {
3315                          this.nativeSelection.addRange(getNativeRange(range));
3316                          this.refresh();
3317                      }
3318                  };
3319              }
3320  
3321              selProto.setRanges = function(ranges) {
3322                  if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3323                      createControlSelection(this, ranges);
3324                  } else {
3325                      this.removeAllRanges();
3326                      for (var i = 0, len = ranges.length; i < len; ++i) {
3327                          this.addRange(ranges[i]);
3328                      }
3329                  }
3330              };
3331          } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3332                     implementsControlRange && useDocumentSelection) {
3333  
3334              selProto.removeAllRanges = function() {
3335                  // Added try/catch as fix for issue #21
3336                  try {
3337                      this.docSelection.empty();
3338  
3339                      // Check for empty() not working (issue #24)
3340                      if (this.docSelection.type != "None") {
3341                          // Work around failure to empty a control selection by instead selecting a TextRange and then
3342                          // calling empty()
3343                          var doc;
3344                          if (this.anchorNode) {
3345                              doc = getDocument(this.anchorNode);
3346                          } else if (this.docSelection.type == CONTROL) {
3347                              var controlRange = this.docSelection.createRange();
3348                              if (controlRange.length) {
3349                                  doc = getDocument( controlRange.item(0) );
3350                              }
3351                          }
3352                          if (doc) {
3353                              var textRange = getBody(doc).createTextRange();
3354                              textRange.select();
3355                              this.docSelection.empty();
3356                          }
3357                      }
3358                  } catch(ex) {}
3359                  updateEmptySelection(this);
3360              };
3361  
3362              selProto.addRange = function(range) {
3363                  if (this.docSelection.type == CONTROL) {
3364                      addRangeToControlSelection(this, range);
3365                  } else {
3366                      api.WrappedTextRange.rangeToTextRange(range).select();
3367                      this._ranges[0] = range;
3368                      this.rangeCount = 1;
3369                      this.isCollapsed = this._ranges[0].collapsed;
3370                      updateAnchorAndFocusFromRange(this, range, false);
3371                  }
3372              };
3373  
3374              selProto.setRanges = function(ranges) {
3375                  this.removeAllRanges();
3376                  var rangeCount = ranges.length;
3377                  if (rangeCount > 1) {
3378                      createControlSelection(this, ranges);
3379                  } else if (rangeCount) {
3380                      this.addRange(ranges[0]);
3381                  }
3382              };
3383          } else {
3384              module.fail("No means of selecting a Range or TextRange was found");
3385              return false;
3386          }
3387  
3388          selProto.getRangeAt = function(index) {
3389              if (index < 0 || index >= this.rangeCount) {
3390                  throw new DOMException("INDEX_SIZE_ERR");
3391              } else {
3392                  // Clone the range to preserve selection-range independence. See issue 80.
3393                  return this._ranges[index].cloneRange();
3394              }
3395          };
3396  
3397          var refreshSelection;
3398  
3399          if (useDocumentSelection) {
3400              refreshSelection = function(sel) {
3401                  var range;
3402                  if (api.isSelectionValid(sel.win)) {
3403                      range = sel.docSelection.createRange();
3404                  } else {
3405                      range = getBody(sel.win.document).createTextRange();
3406                      range.collapse(true);
3407                  }
3408  
3409                  if (sel.docSelection.type == CONTROL) {
3410                      updateControlSelection(sel);
3411                  } else if (isTextRange(range)) {
3412                      updateFromTextRange(sel, range);
3413                  } else {
3414                      updateEmptySelection(sel);
3415                  }
3416              };
3417          } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3418              refreshSelection = function(sel) {
3419                  if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3420                      updateControlSelection(sel);
3421                  } else {
3422                      sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3423                      if (sel.rangeCount) {
3424                          for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3425                              sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3426                          }
3427                          updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3428                          sel.isCollapsed = selectionIsCollapsed(sel);
3429                      } else {
3430                          updateEmptySelection(sel);
3431                      }
3432                  }
3433              };
3434          } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3435              refreshSelection = function(sel) {
3436                  var range, nativeSel = sel.nativeSelection;
3437                  if (nativeSel.anchorNode) {
3438                      range = getSelectionRangeAt(nativeSel, 0);
3439                      sel._ranges = [range];
3440                      sel.rangeCount = 1;
3441                      updateAnchorAndFocusFromNativeSelection(sel);
3442                      sel.isCollapsed = selectionIsCollapsed(sel);
3443                  } else {
3444                      updateEmptySelection(sel);
3445                  }
3446              };
3447          } else {
3448              module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3449              return false;
3450          }
3451  
3452          selProto.refresh = function(checkForChanges) {
3453              var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3454              var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3455  
3456              refreshSelection(this);
3457              if (checkForChanges) {
3458                  // Check the range count first
3459                  var i = oldRanges.length;
3460                  if (i != this._ranges.length) {
3461                      return true;
3462                  }
3463  
3464                  // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3465                  // ranges after this
3466                  if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3467                      return true;
3468                  }
3469  
3470                  // Finally, compare each range in turn
3471                  while (i--) {
3472                      if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3473                          return true;
3474                      }
3475                  }
3476                  return false;
3477              }
3478          };
3479  
3480          // Removal of a single range
3481          var removeRangeManually = function(sel, range) {
3482              var ranges = sel.getAllRanges();
3483              sel.removeAllRanges();
3484              for (var i = 0, len = ranges.length; i < len; ++i) {
3485                  if (!rangesEqual(range, ranges[i])) {
3486                      sel.addRange(ranges[i]);
3487                  }
3488              }
3489              if (!sel.rangeCount) {
3490                  updateEmptySelection(sel);
3491              }
3492          };
3493  
3494          if (implementsControlRange && implementsDocSelection) {
3495              selProto.removeRange = function(range) {
3496                  if (this.docSelection.type == CONTROL) {
3497                      var controlRange = this.docSelection.createRange();
3498                      var rangeElement = getSingleElementFromRange(range);
3499  
3500                      // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3501                      // element contained by the supplied range
3502                      var doc = getDocument(controlRange.item(0));
3503                      var newControlRange = getBody(doc).createControlRange();
3504                      var el, removed = false;
3505                      for (var i = 0, len = controlRange.length; i < len; ++i) {
3506                          el = controlRange.item(i);
3507                          if (el !== rangeElement || removed) {
3508                              newControlRange.add(controlRange.item(i));
3509                          } else {
3510                              removed = true;
3511                          }
3512                      }
3513                      newControlRange.select();
3514  
3515                      // Update the wrapped selection based on what's now in the native selection
3516                      updateControlSelection(this);
3517                  } else {
3518                      removeRangeManually(this, range);
3519                  }
3520              };
3521          } else {
3522              selProto.removeRange = function(range) {
3523                  removeRangeManually(this, range);
3524              };
3525          }
3526  
3527          // Detecting if a selection is backward
3528          var selectionIsBackward;
3529          if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3530              selectionIsBackward = winSelectionIsBackward;
3531  
3532              selProto.isBackward = function() {
3533                  return selectionIsBackward(this);
3534              };
3535          } else {
3536              selectionIsBackward = selProto.isBackward = function() {
3537                  return false;
3538              };
3539          }
3540  
3541          // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3542          selProto.isBackwards = selProto.isBackward;
3543  
3544          // Selection stringifier
3545          // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3546          // The current spec does not yet define this method.
3547          selProto.toString = function() {
3548              var rangeTexts = [];
3549              for (var i = 0, len = this.rangeCount; i < len; ++i) {
3550                  rangeTexts[i] = "" + this._ranges[i];
3551              }
3552              return rangeTexts.join("");
3553          };
3554  
3555          function assertNodeInSameDocument(sel, node) {
3556              if (sel.win.document != getDocument(node)) {
3557                  throw new DOMException("WRONG_DOCUMENT_ERR");
3558              }
3559          }
3560  
3561          // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3562          selProto.collapse = function(node, offset) {
3563              assertNodeInSameDocument(this, node);
3564              var range = api.createRange(node);
3565              range.collapseToPoint(node, offset);
3566              this.setSingleRange(range);
3567              this.isCollapsed = true;
3568          };
3569  
3570          selProto.collapseToStart = function() {
3571              if (this.rangeCount) {
3572                  var range = this._ranges[0];
3573                  this.collapse(range.startContainer, range.startOffset);
3574              } else {
3575                  throw new DOMException("INVALID_STATE_ERR");
3576              }
3577          };
3578  
3579          selProto.collapseToEnd = function() {
3580              if (this.rangeCount) {
3581                  var range = this._ranges[this.rangeCount - 1];
3582                  this.collapse(range.endContainer, range.endOffset);
3583              } else {
3584                  throw new DOMException("INVALID_STATE_ERR");
3585              }
3586          };
3587  
3588          // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
3589          // specified so the native implementation is never used by Rangy.
3590          selProto.selectAllChildren = function(node) {
3591              assertNodeInSameDocument(this, node);
3592              var range = api.createRange(node);
3593              range.selectNodeContents(node);
3594              this.setSingleRange(range);
3595          };
3596  
3597          selProto.deleteFromDocument = function() {
3598              // Sepcial behaviour required for IE's control selections
3599              if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3600                  var controlRange = this.docSelection.createRange();
3601                  var element;
3602                  while (controlRange.length) {
3603                      element = controlRange.item(0);
3604                      controlRange.remove(element);
3605                      dom.removeNode(element);
3606                  }
3607                  this.refresh();
3608              } else if (this.rangeCount) {
3609                  var ranges = this.getAllRanges();
3610                  if (ranges.length) {
3611                      this.removeAllRanges();
3612                      for (var i = 0, len = ranges.length; i < len; ++i) {
3613                          ranges[i].deleteContents();
3614                      }
3615                      // The spec says nothing about what the selection should contain after calling deleteContents on each
3616                      // range. Firefox moves the selection to where the final selected range was, so we emulate that
3617                      this.addRange(ranges[len - 1]);
3618                  }
3619              }
3620          };
3621  
3622          // The following are non-standard extensions
3623          selProto.eachRange = function(func, returnValue) {
3624              for (var i = 0, len = this._ranges.length; i < len; ++i) {
3625                  if ( func( this.getRangeAt(i) ) ) {
3626                      return returnValue;
3627                  }
3628              }
3629          };
3630  
3631          selProto.getAllRanges = function() {
3632              var ranges = [];
3633              this.eachRange(function(range) {
3634                  ranges.push(range);
3635              });
3636              return ranges;
3637          };
3638  
3639          selProto.setSingleRange = function(range, direction) {
3640              this.removeAllRanges();
3641              this.addRange(range, direction);
3642          };
3643  
3644          selProto.callMethodOnEachRange = function(methodName, params) {
3645              var results = [];
3646              this.eachRange( function(range) {
3647                  results.push( range[methodName].apply(range, params || []) );
3648              } );
3649              return results;
3650          };
3651  
3652          function createStartOrEndSetter(isStart) {
3653              return function(node, offset) {
3654                  var range;
3655                  if (this.rangeCount) {
3656                      range = this.getRangeAt(0);
3657                      range["set" + (isStart ? "Start" : "End")](node, offset);
3658                  } else {
3659                      range = api.createRange(this.win.document);
3660                      range.setStartAndEnd(node, offset);
3661                  }
3662                  this.setSingleRange(range, this.isBackward());
3663              };
3664          }
3665  
3666          selProto.setStart = createStartOrEndSetter(true);
3667          selProto.setEnd = createStartOrEndSetter(false);
3668  
3669          // Add select() method to Range prototype. Any existing selection will be removed.
3670          api.rangePrototype.select = function(direction) {
3671              getSelection( this.getDocument() ).setSingleRange(this, direction);
3672          };
3673  
3674          selProto.changeEachRange = function(func) {
3675              var ranges = [];
3676              var backward = this.isBackward();
3677  
3678              this.eachRange(function(range) {
3679                  func(range);
3680                  ranges.push(range);
3681              });
3682  
3683              this.removeAllRanges();
3684              if (backward && ranges.length == 1) {
3685                  this.addRange(ranges[0], "backward");
3686              } else {
3687                  this.setRanges(ranges);
3688              }
3689          };
3690  
3691          selProto.containsNode = function(node, allowPartial) {
3692              return this.eachRange( function(range) {
3693                  return range.containsNode(node, allowPartial);
3694              }, true ) || false;
3695          };
3696  
3697          selProto.getBookmark = function(containerNode) {
3698              return {
3699                  backward: this.isBackward(),
3700                  rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3701              };
3702          };
3703  
3704          selProto.moveToBookmark = function(bookmark) {
3705              var selRanges = [];
3706              for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3707                  range = api.createRange(this.win);
3708                  range.moveToBookmark(rangeBookmark);
3709                  selRanges.push(range);
3710              }
3711              if (bookmark.backward) {
3712                  this.setSingleRange(selRanges[0], "backward");
3713              } else {
3714                  this.setRanges(selRanges);
3715              }
3716          };
3717  
3718          selProto.saveRanges = function() {
3719              return {
3720                  backward: this.isBackward(),
3721                  ranges: this.callMethodOnEachRange("cloneRange")
3722              };
3723          };
3724  
3725          selProto.restoreRanges = function(selRanges) {
3726              this.removeAllRanges();
3727              for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
3728                  this.addRange(range, (selRanges.backward && i == 0));
3729              }
3730          };
3731  
3732          selProto.toHtml = function() {
3733              var rangeHtmls = [];
3734              this.eachRange(function(range) {
3735                  rangeHtmls.push( DomRange.toHtml(range) );
3736              });
3737              return rangeHtmls.join("");
3738          };
3739  
3740          if (features.implementsTextRange) {
3741              selProto.getNativeTextRange = function() {
3742                  var sel, textRange;
3743                  if ( (sel = this.docSelection) ) {
3744                      var range = sel.createRange();
3745                      if (isTextRange(range)) {
3746                          return range;
3747                      } else {
3748                          throw module.createError("getNativeTextRange: selection is a control selection");
3749                      }
3750                  } else if (this.rangeCount > 0) {
3751                      return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3752                  } else {
3753                      throw module.createError("getNativeTextRange: selection contains no range");
3754                  }
3755              };
3756          }
3757  
3758          function inspect(sel) {
3759              var rangeInspects = [];
3760              var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3761              var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3762              var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3763  
3764              if (typeof sel.rangeCount != "undefined") {
3765                  for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3766                      rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3767                  }
3768              }
3769              return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3770                      ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3771          }
3772  
3773          selProto.getName = function() {
3774              return "WrappedSelection";
3775          };
3776  
3777          selProto.inspect = function() {
3778              return inspect(this);
3779          };
3780  
3781          selProto.detach = function() {
3782              actOnCachedSelection(this.win, "delete");
3783              deleteProperties(this);
3784          };
3785  
3786          WrappedSelection.detachAll = function() {
3787              actOnCachedSelection(null, "deleteAll");
3788          };
3789  
3790          WrappedSelection.inspect = inspect;
3791          WrappedSelection.isDirectionBackward = isDirectionBackward;
3792  
3793          api.Selection = WrappedSelection;
3794  
3795          api.selectionPrototype = selProto;
3796  
3797          api.addShimListener(function(win) {
3798              if (typeof win.getSelection == "undefined") {
3799                  win.getSelection = function() {
3800                      return getSelection(win);
3801                  };
3802              }
3803              win = null;
3804          });
3805      });
3806      
3807  
3808      /*----------------------------------------------------------------------------------------------------------------*/

3809  
3810      // Wait for document to load before initializing

3811      var docReady = false;
3812  
3813      var loadHandler = function(e) {
3814          if (!docReady) {
3815              docReady = true;
3816              if (!api.initialized && api.config.autoInitialize) {
3817                  init();
3818              }
3819          }
3820      };
3821  
3822      if (isBrowser) {
3823          // Test whether the document has already been loaded and initialize immediately if so

3824          if (document.readyState == "complete") {
3825              loadHandler();
3826          } else {
3827              if (isHostMethod(document, "addEventListener")) {
3828                  document.addEventListener("DOMContentLoaded", loadHandler, false);
3829              }
3830  
3831              // Add a fallback in case the DOMContentLoaded event isn't supported

3832              addListener(window, "load", loadHandler);
3833          }
3834      }
3835  
3836      return api;
3837  }, this);
3838  
3839  /**

3840   * Selection save and restore module for Rangy.

3841   * Saves and restores user selections using marker invisible elements in the DOM.

3842   *

3843   * Part of Rangy, a cross-browser JavaScript range and selection library

3844   * https://github.com/timdown/rangy

3845   *

3846   * Depends on Rangy core.

3847   *

3848   * Copyright 2015, Tim Down

3849   * Licensed under the MIT license.

3850   * Version: 1.3.0

3851   * Build date: 10 May 2015

3852   */
3853  (function(factory, root) {
3854      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
3855      factory(root.rangy);
3856  })(function(rangy) {
3857      rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
3858          var dom = api.dom;
3859          var removeNode = dom.removeNode;
3860          var isDirectionBackward = api.Selection.isDirectionBackward;
3861          var markerTextChar = "\ufeff";
3862  
3863          function gEBI(id, doc) {
3864              return (doc || document).getElementById(id);
3865          }
3866  
3867          function insertRangeBoundaryMarker(range, atStart) {
3868              var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
3869              var markerEl;
3870              var doc = dom.getDocument(range.startContainer);
3871  
3872              // Clone the Range and collapse to the appropriate boundary point
3873              var boundaryRange = range.cloneRange();
3874              boundaryRange.collapse(atStart);
3875  
3876              // Create the marker element containing a single invisible character using DOM methods and insert it
3877              markerEl = doc.createElement("span");
3878              markerEl.id = markerId;
3879              markerEl.style.lineHeight = "0";
3880              markerEl.style.display = "none";
3881              markerEl.className = "rangySelectionBoundary";
3882              markerEl.appendChild(doc.createTextNode(markerTextChar));
3883  
3884              boundaryRange.insertNode(markerEl);
3885              return markerEl;
3886          }
3887  
3888          function setRangeBoundary(doc, range, markerId, atStart) {
3889              var markerEl = gEBI(markerId, doc);
3890              if (markerEl) {
3891                  range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
3892                  removeNode(markerEl);
3893              } else {
3894                  module.warn("Marker element has been removed. Cannot restore selection.");
3895              }
3896          }
3897  
3898          function compareRanges(r1, r2) {
3899              return r2.compareBoundaryPoints(r1.START_TO_START, r1);
3900          }
3901  
3902          function saveRange(range, direction) {
3903              var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
3904              var backward = isDirectionBackward(direction);
3905  
3906              if (range.collapsed) {
3907                  endEl = insertRangeBoundaryMarker(range, false);
3908                  return {
3909                      document: doc,
3910                      markerId: endEl.id,
3911                      collapsed: true
3912                  };
3913              } else {
3914                  endEl = insertRangeBoundaryMarker(range, false);
3915                  startEl = insertRangeBoundaryMarker(range, true);
3916  
3917                  return {
3918                      document: doc,
3919                      startMarkerId: startEl.id,
3920                      endMarkerId: endEl.id,
3921                      collapsed: false,
3922                      backward: backward,
3923                      toString: function() {
3924                          return "original text: '" + text + "', new text: '" + range.toString() + "'";
3925                      }
3926                  };
3927              }
3928          }
3929  
3930          function restoreRange(rangeInfo, normalize) {
3931              var doc = rangeInfo.document;
3932              if (typeof normalize == "undefined") {
3933                  normalize = true;
3934              }
3935              var range = api.createRange(doc);
3936              if (rangeInfo.collapsed) {
3937                  var markerEl = gEBI(rangeInfo.markerId, doc);
3938                  if (markerEl) {
3939                      markerEl.style.display = "inline";
3940                      var previousNode = markerEl.previousSibling;
3941  
3942                      // Workaround for issue 17
3943                      if (previousNode && previousNode.nodeType == 3) {
3944                          removeNode(markerEl);
3945                          range.collapseToPoint(previousNode, previousNode.length);
3946                      } else {
3947                          range.collapseBefore(markerEl);
3948                          removeNode(markerEl);
3949                      }
3950                  } else {
3951                      module.warn("Marker element has been removed. Cannot restore selection.");
3952                  }
3953              } else {
3954                  setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
3955                  setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
3956              }
3957  
3958              if (normalize) {
3959                  range.normalizeBoundaries();
3960              }
3961  
3962              return range;
3963          }
3964  
3965          function saveRanges(ranges, direction) {
3966              var rangeInfos = [], range, doc;
3967              var backward = isDirectionBackward(direction);
3968  
3969              // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
3970              ranges = ranges.slice(0);
3971              ranges.sort(compareRanges);
3972  
3973              for (var i = 0, len = ranges.length; i < len; ++i) {
3974                  rangeInfos[i] = saveRange(ranges[i], backward);
3975              }
3976  
3977              // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
3978              // between its markers
3979              for (i = len - 1; i >= 0; --i) {
3980                  range = ranges[i];
3981                  doc = api.DomRange.getRangeDocument(range);
3982                  if (range.collapsed) {
3983                      range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
3984                  } else {
3985                      range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
3986                      range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
3987                  }
3988              }
3989  
3990              return rangeInfos;
3991          }
3992  
3993          function saveSelection(win) {
3994              if (!api.isSelectionValid(win)) {
3995                  module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
3996                  return null;
3997              }
3998              var sel = api.getSelection(win);
3999              var ranges = sel.getAllRanges();
4000              var backward = (ranges.length == 1 && sel.isBackward());
4001  
4002              var rangeInfos = saveRanges(ranges, backward);
4003  
4004              // Ensure current selection is unaffected
4005              if (backward) {
4006                  sel.setSingleRange(ranges[0], backward);
4007              } else {
4008                  sel.setRanges(ranges);
4009              }
4010  
4011              return {
4012                  win: win,
4013                  rangeInfos: rangeInfos,
4014                  restored: false
4015              };
4016          }
4017  
4018          function restoreRanges(rangeInfos) {
4019              var ranges = [];
4020  
4021              // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
4022              // normalization affecting previously restored ranges.
4023              var rangeCount = rangeInfos.length;
4024  
4025              for (var i = rangeCount - 1; i >= 0; i--) {
4026                  ranges[i] = restoreRange(rangeInfos[i], true);
4027              }
4028  
4029              return ranges;
4030          }
4031  
4032          function restoreSelection(savedSelection, preserveDirection) {
4033              if (!savedSelection.restored) {
4034                  var rangeInfos = savedSelection.rangeInfos;
4035                  var sel = api.getSelection(savedSelection.win);
4036                  var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
4037  
4038                  if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
4039                      sel.removeAllRanges();
4040                      sel.addRange(ranges[0], true);
4041                  } else {
4042                      sel.setRanges(ranges);
4043                  }
4044  
4045                  savedSelection.restored = true;
4046              }
4047          }
4048  
4049          function removeMarkerElement(doc, markerId) {
4050              var markerEl = gEBI(markerId, doc);
4051              if (markerEl) {
4052                  removeNode(markerEl);
4053              }
4054          }
4055  
4056          function removeMarkers(savedSelection) {
4057              var rangeInfos = savedSelection.rangeInfos;
4058              for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
4059                  rangeInfo = rangeInfos[i];
4060                  if (rangeInfo.collapsed) {
4061                      removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
4062                  } else {
4063                      removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
4064                      removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
4065                  }
4066              }
4067          }
4068  
4069          api.util.extend(api, {
4070              saveRange: saveRange,
4071              restoreRange: restoreRange,
4072              saveRanges: saveRanges,
4073              restoreRanges: restoreRanges,
4074              saveSelection: saveSelection,
4075              restoreSelection: restoreSelection,
4076              removeMarkerElement: removeMarkerElement,
4077              removeMarkers: removeMarkers
4078          });
4079      });
4080      
4081      return rangy;
4082  }, this);
4083  
4084  /**

4085   * Serializer module for Rangy.

4086   * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a

4087   * cookie or local storage and restore it on the user's next visit to the same page.

4088   *

4089   * Part of Rangy, a cross-browser JavaScript range and selection library

4090   * https://github.com/timdown/rangy

4091   *

4092   * Depends on Rangy core.

4093   *

4094   * Copyright 2015, Tim Down

4095   * Licensed under the MIT license.

4096   * Version: 1.3.0

4097   * Build date: 10 May 2015

4098   */
4099  (function(factory, root) {
4100      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
4101      factory(root.rangy);
4102  })(function(rangy) {
4103      rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
4104          var UNDEF = "undefined";
4105          var util = api.util;
4106  
4107          // encodeURIComponent and decodeURIComponent are required for cookie handling
4108          if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
4109              module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
4110          }
4111  
4112          // Checksum for checking whether range can be serialized
4113          var crc32 = (function() {
4114              function utf8encode(str) {
4115                  var utf8CharCodes = [];
4116  
4117                  for (var i = 0, len = str.length, c; i < len; ++i) {
4118                      c = str.charCodeAt(i);
4119                      if (c < 128) {
4120                          utf8CharCodes.push(c);
4121                      } else if (c < 2048) {
4122                          utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
4123                      } else {
4124                          utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
4125                      }
4126                  }
4127                  return utf8CharCodes;
4128              }
4129  
4130              var cachedCrcTable = null;
4131  
4132              function buildCRCTable() {
4133                  var table = [];
4134                  for (var i = 0, j, crc; i < 256; ++i) {
4135                      crc = i;
4136                      j = 8;
4137                      while (j--) {
4138                          if ((crc & 1) == 1) {
4139                              crc = (crc >>> 1) ^ 0xEDB88320;
4140                          } else {
4141                              crc >>>= 1;
4142                          }
4143                      }
4144                      table[i] = crc >>> 0;
4145                  }
4146                  return table;
4147              }
4148  
4149              function getCrcTable() {
4150                  if (!cachedCrcTable) {
4151                      cachedCrcTable = buildCRCTable();
4152                  }
4153                  return cachedCrcTable;
4154              }
4155  
4156              return function(str) {
4157                  var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
4158                  for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
4159                      y = (crc ^ utf8CharCodes[i]) & 0xFF;
4160                      crc = (crc >>> 8) ^ crcTable[y];
4161                  }
4162                  return (crc ^ -1) >>> 0;
4163              };
4164          })();
4165  
4166          var dom = api.dom;
4167  
4168          function escapeTextForHtml(str) {
4169              return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
4170          }
4171  
4172          function nodeToInfoString(node, infoParts) {
4173              infoParts = infoParts || [];
4174              var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
4175              var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
4176              var start = "", end = "";
4177              switch (nodeType) {
4178                  case 3: // Text node
4179                      start = escapeTextForHtml(node.nodeValue);
4180                      break;
4181                  case 8: // Comment
4182                      start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
4183                      break;
4184                  default:
4185                      start = "<" + nodeInfo + ">";
4186                      end = "</>";
4187                      break;
4188              }
4189              if (start) {
4190                  infoParts.push(start);
4191              }
4192              for (var i = 0; i < childCount; ++i) {
4193                  nodeToInfoString(children[i], infoParts);
4194              }
4195              if (end) {
4196                  infoParts.push(end);
4197              }
4198              return infoParts;
4199          }
4200  
4201          // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
4202          // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
4203          // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
4204          // innerHTML whenever the user changes an input within the element.
4205          function getElementChecksum(el) {
4206              var info = nodeToInfoString(el).join("");
4207              return crc32(info).toString(16);
4208          }
4209  
4210          function serializePosition(node, offset, rootNode) {
4211              var pathParts = [], n = node;
4212              rootNode = rootNode || dom.getDocument(node).documentElement;
4213              while (n && n != rootNode) {
4214                  pathParts.push(dom.getNodeIndex(n, true));
4215                  n = n.parentNode;
4216              }
4217              return pathParts.join("/") + ":" + offset;
4218          }
4219  
4220          function deserializePosition(serialized, rootNode, doc) {
4221              if (!rootNode) {
4222                  rootNode = (doc || document).documentElement;
4223              }
4224              var parts = serialized.split(":");
4225              var node = rootNode;
4226              var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
4227  
4228              while (i--) {
4229                  nodeIndex = parseInt(nodeIndices[i], 10);
4230                  if (nodeIndex < node.childNodes.length) {
4231                      node = node.childNodes[nodeIndex];
4232                  } else {
4233                      throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
4234                              " has no child with index " + nodeIndex + ", " + i);
4235                  }
4236              }
4237  
4238              return new dom.DomPosition(node, parseInt(parts[1], 10));
4239          }
4240  
4241          function serializeRange(range, omitChecksum, rootNode) {
4242              rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
4243              if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
4244                  throw module.createError("serializeRange(): range " + range.inspect() +
4245                      " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
4246              }
4247              var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
4248                  serializePosition(range.endContainer, range.endOffset, rootNode);
4249              if (!omitChecksum) {
4250                  serialized += "{" + getElementChecksum(rootNode) + "}";
4251              }
4252              return serialized;
4253          }
4254  
4255          var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
4256  
4257          function deserializeRange(serialized, rootNode, doc) {
4258              if (rootNode) {
4259                  doc = doc || dom.getDocument(rootNode);
4260              } else {
4261                  doc = doc || document;
4262                  rootNode = doc.documentElement;
4263              }
4264              var result = deserializeRegex.exec(serialized);
4265              var checksum = result[4];
4266              if (checksum) {
4267                  var rootNodeChecksum = getElementChecksum(rootNode);
4268                  if (checksum !== rootNodeChecksum) {
4269                      throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
4270                          ") and target root node (" + rootNodeChecksum + ") do not match");
4271                  }
4272              }
4273              var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
4274              var range = api.createRange(doc);
4275              range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
4276              return range;
4277          }
4278  
4279          function canDeserializeRange(serialized, rootNode, doc) {
4280              if (!rootNode) {
4281                  rootNode = (doc || document).documentElement;
4282              }
4283              var result = deserializeRegex.exec(serialized);
4284              var checksum = result[3];
4285              return !checksum || checksum === getElementChecksum(rootNode);
4286          }
4287  
4288          function serializeSelection(selection, omitChecksum, rootNode) {
4289              selection = api.getSelection(selection);
4290              var ranges = selection.getAllRanges(), serializedRanges = [];
4291              for (var i = 0, len = ranges.length; i < len; ++i) {
4292                  serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
4293              }
4294              return serializedRanges.join("|");
4295          }
4296  
4297          function deserializeSelection(serialized, rootNode, win) {
4298              if (rootNode) {
4299                  win = win || dom.getWindow(rootNode);
4300              } else {
4301                  win = win || window;
4302                  rootNode = win.document.documentElement;
4303              }
4304              var serializedRanges = serialized.split("|");
4305              var sel = api.getSelection(win);
4306              var ranges = [];
4307  
4308              for (var i = 0, len = serializedRanges.length; i < len; ++i) {
4309                  ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
4310              }
4311              sel.setRanges(ranges);
4312  
4313              return sel;
4314          }
4315  
4316          function canDeserializeSelection(serialized, rootNode, win) {
4317              var doc;
4318              if (rootNode) {
4319                  doc = win ? win.document : dom.getDocument(rootNode);
4320              } else {
4321                  win = win || window;
4322                  rootNode = win.document.documentElement;
4323              }
4324              var serializedRanges = serialized.split("|");
4325  
4326              for (var i = 0, len = serializedRanges.length; i < len; ++i) {
4327                  if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
4328                      return false;
4329                  }
4330              }
4331  
4332              return true;
4333          }
4334  
4335          var cookieName = "rangySerializedSelection";
4336  
4337          function getSerializedSelectionFromCookie(cookie) {
4338              var parts = cookie.split(/[;,]/);
4339              for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
4340                  nameVal = parts[i].split("=");
4341                  if (nameVal[0].replace(/^\s+/, "") == cookieName) {
4342                      val = nameVal[1];
4343                      if (val) {
4344                          return decodeURIComponent(val.replace(/\s+$/, ""));
4345                      }
4346                  }
4347              }
4348              return null;
4349          }
4350  
4351          function restoreSelectionFromCookie(win) {
4352              win = win || window;
4353              var serialized = getSerializedSelectionFromCookie(win.document.cookie);
4354              if (serialized) {
4355                  deserializeSelection(serialized, win.doc);
4356              }
4357          }
4358  
4359          function saveSelectionCookie(win, props) {
4360              win = win || window;
4361              props = (typeof props == "object") ? props : {};
4362              var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
4363              var path = props.path ? ";path=" + props.path : "";
4364              var domain = props.domain ? ";domain=" + props.domain : "";
4365              var secure = props.secure ? ";secure" : "";
4366              var serialized = serializeSelection(api.getSelection(win));
4367              win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
4368          }
4369  
4370          util.extend(api, {
4371              serializePosition: serializePosition,
4372              deserializePosition: deserializePosition,
4373              serializeRange: serializeRange,
4374              deserializeRange: deserializeRange,
4375              canDeserializeRange: canDeserializeRange,
4376              serializeSelection: serializeSelection,
4377              deserializeSelection: deserializeSelection,
4378              canDeserializeSelection: canDeserializeSelection,
4379              restoreSelectionFromCookie: restoreSelectionFromCookie,
4380              saveSelectionCookie: saveSelectionCookie,
4381              getElementChecksum: getElementChecksum,
4382              nodeToInfoString: nodeToInfoString
4383          });
4384  
4385          util.crc32 = crc32;
4386      });
4387      
4388      return rangy;
4389  }, this);
4390  
4391  /**

4392   * Class Applier module for Rangy.

4393   * Adds, removes and toggles classes on Ranges and Selections

4394   *

4395   * Part of Rangy, a cross-browser JavaScript range and selection library

4396   * https://github.com/timdown/rangy

4397   *

4398   * Depends on Rangy core.

4399   *

4400   * Copyright 2015, Tim Down

4401   * Licensed under the MIT license.

4402   * Version: 1.3.0

4403   * Build date: 10 May 2015

4404   */
4405  (function(factory, root) {
4406      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
4407      factory(root.rangy);
4408  })(function(rangy) {
4409      rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
4410          var dom = api.dom;
4411          var DomPosition = dom.DomPosition;
4412          var contains = dom.arrayContains;
4413          var util = api.util;
4414          var forEach = util.forEach;
4415  
4416  
4417          var defaultTagName = "span";
4418          var createElementNSSupported = util.isHostMethod(document, "createElementNS");
4419  
4420          function each(obj, func) {
4421              for (var i in obj) {
4422                  if (obj.hasOwnProperty(i)) {
4423                      if (func(i, obj[i]) === false) {
4424                          return false;
4425                      }
4426                  }
4427              }
4428              return true;
4429          }
4430  
4431          function trim(str) {
4432              return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
4433          }
4434  
4435          function classNameContainsClass(fullClassName, className) {
4436              return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
4437          }
4438  
4439          // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
4440          function hasClass(el, className) {
4441              if (typeof el.classList == "object") {
4442                  return el.classList.contains(className);
4443              } else {
4444                  var classNameSupported = (typeof el.className == "string");
4445                  var elClass = classNameSupported ? el.className : el.getAttribute("class");
4446                  return classNameContainsClass(elClass, className);
4447              }
4448          }
4449  
4450          function addClass(el, className) {
4451              if (typeof el.classList == "object") {
4452                  el.classList.add(className);
4453              } else {
4454                  var classNameSupported = (typeof el.className == "string");
4455                  var elClass = classNameSupported ? el.className : el.getAttribute("class");
4456                  if (elClass) {
4457                      if (!classNameContainsClass(elClass, className)) {
4458                          elClass += " " + className;
4459                      }
4460                  } else {
4461                      elClass = className;
4462                  }
4463                  if (classNameSupported) {
4464                      el.className = elClass;
4465                  } else {
4466                      el.setAttribute("class", elClass);
4467                  }
4468              }
4469          }
4470  
4471          var removeClass = (function() {
4472              function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
4473                  return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
4474              }
4475  
4476              return function(el, className) {
4477                  if (typeof el.classList == "object") {
4478                      el.classList.remove(className);
4479                  } else {
4480                      var classNameSupported = (typeof el.className == "string");
4481                      var elClass = classNameSupported ? el.className : el.getAttribute("class");
4482                      elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
4483                      if (classNameSupported) {
4484                          el.className = elClass;
4485                      } else {
4486                          el.setAttribute("class", elClass);
4487                      }
4488                  }
4489              };
4490          })();
4491  
4492          function getClass(el) {
4493              var classNameSupported = (typeof el.className == "string");
4494              return classNameSupported ? el.className : el.getAttribute("class");
4495          }
4496  
4497          function sortClassName(className) {
4498              return className && className.split(/\s+/).sort().join(" ");
4499          }
4500  
4501          function getSortedClassName(el) {
4502              return sortClassName( getClass(el) );
4503          }
4504  
4505          function haveSameClasses(el1, el2) {
4506              return getSortedClassName(el1) == getSortedClassName(el2);
4507          }
4508  
4509          function hasAllClasses(el, className) {
4510              var classes = className.split(/\s+/);
4511              for (var i = 0, len = classes.length; i < len; ++i) {
4512                  if (!hasClass(el, trim(classes[i]))) {
4513                      return false;
4514                  }
4515              }
4516              return true;
4517          }
4518  
4519          function canTextBeStyled(textNode) {
4520              var parent = textNode.parentNode;
4521              return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
4522          }
4523  
4524          function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
4525              var posNode = position.node, posOffset = position.offset;
4526              var newNode = posNode, newOffset = posOffset;
4527  
4528              if (posNode == newParent && posOffset > newIndex) {
4529                  ++newOffset;
4530              }
4531  
4532              if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
4533                  newNode = newParent;
4534                  newOffset += newIndex - oldIndex;
4535              }
4536  
4537              if (posNode == oldParent && posOffset > oldIndex + 1) {
4538                  --newOffset;
4539              }
4540  
4541              position.node = newNode;
4542              position.offset = newOffset;
4543          }
4544  
4545          function movePositionWhenRemovingNode(position, parentNode, index) {
4546              if (position.node == parentNode && position.offset > index) {
4547                  --position.offset;
4548              }
4549          }
4550  
4551          function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
4552              // For convenience, allow newIndex to be -1 to mean "insert at the end".
4553              if (newIndex == -1) {
4554                  newIndex = newParent.childNodes.length;
4555              }
4556  
4557              var oldParent = node.parentNode;
4558              var oldIndex = dom.getNodeIndex(node);
4559  
4560              forEach(positionsToPreserve, function(position) {
4561                  movePosition(position, oldParent, oldIndex, newParent, newIndex);
4562              });
4563  
4564              // Now actually move the node.
4565              if (newParent.childNodes.length == newIndex) {
4566                  newParent.appendChild(node);
4567              } else {
4568                  newParent.insertBefore(node, newParent.childNodes[newIndex]);
4569              }
4570          }
4571  
4572          function removePreservingPositions(node, positionsToPreserve) {
4573  
4574              var oldParent = node.parentNode;
4575              var oldIndex = dom.getNodeIndex(node);
4576  
4577              forEach(positionsToPreserve, function(position) {
4578                  movePositionWhenRemovingNode(position, oldParent, oldIndex);
4579              });
4580  
4581              dom.removeNode(node);
4582          }
4583  
4584          function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
4585              var child, children = [];
4586              while ( (child = node.firstChild) ) {
4587                  movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
4588                  children.push(child);
4589              }
4590              if (removeNode) {
4591                  removePreservingPositions(node, positionsToPreserve);
4592              }
4593              return children;
4594          }
4595  
4596          function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
4597              return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
4598          }
4599  
4600          function rangeSelectsAnyText(range, textNode) {
4601              var textNodeRange = range.cloneRange();
4602              textNodeRange.selectNodeContents(textNode);
4603  
4604              var intersectionRange = textNodeRange.intersection(range);
4605              var text = intersectionRange ? intersectionRange.toString() : "";
4606  
4607              return text != "";
4608          }
4609  
4610          function getEffectiveTextNodes(range) {
4611              var nodes = range.getNodes([3]);
4612  
4613              // Optimization as per issue 145
4614  
4615              // Remove non-intersecting text nodes from the start of the range
4616              var start = 0, node;
4617              while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
4618                  ++start;
4619              }
4620  
4621              // Remove non-intersecting text nodes from the start of the range
4622              var end = nodes.length - 1;
4623              while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
4624                  --end;
4625              }
4626  
4627              return nodes.slice(start, end + 1);
4628          }
4629  
4630          function elementsHaveSameNonClassAttributes(el1, el2) {
4631              if (el1.attributes.length != el2.attributes.length) return false;
4632              for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
4633                  attr1 = el1.attributes[i];
4634                  name = attr1.name;
4635                  if (name != "class") {
4636                      attr2 = el2.attributes.getNamedItem(name);
4637                      if ( (attr1 === null) != (attr2 === null) ) return false;
4638                      if (attr1.specified != attr2.specified) return false;
4639                      if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
4640                  }
4641              }
4642              return true;
4643          }
4644  
4645          function elementHasNonClassAttributes(el, exceptions) {
4646              for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
4647                  attrName = el.attributes[i].name;
4648                  if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
4649                      return true;
4650                  }
4651              }
4652              return false;
4653          }
4654  
4655          var getComputedStyleProperty = dom.getComputedStyleProperty;
4656          var isEditableElement = (function() {
4657              var testEl = document.createElement("div");
4658              return typeof testEl.isContentEditable == "boolean" ?
4659                  function (node) {
4660                      return node && node.nodeType == 1 && node.isContentEditable;
4661                  } :
4662                  function (node) {
4663                      if (!node || node.nodeType != 1 || node.contentEditable == "false") {
4664                          return false;
4665                      }
4666                      return node.contentEditable == "true" || isEditableElement(node.parentNode);
4667                  };
4668          })();
4669  
4670          function isEditingHost(node) {
4671              var parent;
4672              return node && node.nodeType == 1 &&
4673                  (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
4674                  (isEditableElement(node) && !isEditableElement(node.parentNode)));
4675          }
4676  
4677          function isEditable(node) {
4678              return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
4679          }
4680  
4681          var inlineDisplayRegex = /^inline(-block|-table)?$/i;
4682  
4683          function isNonInlineElement(node) {
4684              return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
4685          }
4686  
4687          // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
4688          var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
4689  
4690          function isUnrenderedWhiteSpaceNode(node) {
4691              if (node.data.length == 0) {
4692                  return true;
4693              }
4694              if (htmlNonWhiteSpaceRegex.test(node.data)) {
4695                  return false;
4696              }
4697              var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
4698              switch (cssWhiteSpace) {
4699                  case "pre":
4700                  case "pre-wrap":
4701                  case "-moz-pre-wrap":
4702                      return false;
4703                  case "pre-line":
4704                      if (/[\r\n]/.test(node.data)) {
4705                          return false;
4706                      }
4707              }
4708  
4709              // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
4710              // non-inline element, it will not be rendered. This seems to be a good enough definition.
4711              return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
4712          }
4713  
4714          function getRangeBoundaries(ranges) {
4715              var positions = [], i, range;
4716              for (i = 0; range = ranges[i++]; ) {
4717                  positions.push(
4718                      new DomPosition(range.startContainer, range.startOffset),
4719                      new DomPosition(range.endContainer, range.endOffset)
4720                  );
4721              }
4722              return positions;
4723          }
4724  
4725          function updateRangesFromBoundaries(ranges, positions) {
4726              for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
4727                  range = ranges[i];
4728                  start = positions[i * 2];
4729                  end = positions[i * 2 + 1];
4730                  range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
4731              }
4732          }
4733  
4734          function isSplitPoint(node, offset) {
4735              if (dom.isCharacterDataNode(node)) {
4736                  if (offset == 0) {
4737                      return !!node.previousSibling;
4738                  } else if (offset == node.length) {
4739                      return !!node.nextSibling;
4740                  } else {
4741                      return true;
4742                  }
4743              }
4744  
4745              return offset > 0 && offset < node.childNodes.length;
4746          }
4747  
4748          function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
4749              var newNode, parentNode;
4750              var splitAtStart = (descendantOffset == 0);
4751  
4752              if (dom.isAncestorOf(descendantNode, node)) {
4753                  return node;
4754              }
4755  
4756              if (dom.isCharacterDataNode(descendantNode)) {
4757                  var descendantIndex = dom.getNodeIndex(descendantNode);
4758                  if (descendantOffset == 0) {
4759                      descendantOffset = descendantIndex;
4760                  } else if (descendantOffset == descendantNode.length) {
4761                      descendantOffset = descendantIndex + 1;
4762                  } else {
4763                      throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
4764                          descendantOffset + " in " + descendantNode.data);
4765                  }
4766                  descendantNode = descendantNode.parentNode;
4767              }
4768  
4769              if (isSplitPoint(descendantNode, descendantOffset)) {
4770                  // descendantNode is now guaranteed not to be a text or other character node
4771                  newNode = descendantNode.cloneNode(false);
4772                  parentNode = descendantNode.parentNode;
4773                  if (newNode.id) {
4774                      newNode.removeAttribute("id");
4775                  }
4776                  var child, newChildIndex = 0;
4777  
4778                  while ( (child = descendantNode.childNodes[descendantOffset]) ) {
4779                      movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
4780                  }
4781                  movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
4782                  return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
4783              } else if (node != descendantNode) {
4784                  newNode = descendantNode.parentNode;
4785  
4786                  // Work out a new split point in the parent node
4787                  var newNodeIndex = dom.getNodeIndex(descendantNode);
4788  
4789                  if (!splitAtStart) {
4790                      newNodeIndex++;
4791                  }
4792                  return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
4793              }
4794              return node;
4795          }
4796  
4797          function areElementsMergeable(el1, el2) {
4798              return el1.namespaceURI == el2.namespaceURI &&
4799                  el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
4800                  haveSameClasses(el1, el2) &&
4801                  elementsHaveSameNonClassAttributes(el1, el2) &&
4802                  getComputedStyleProperty(el1, "display") == "inline" &&
4803                  getComputedStyleProperty(el2, "display") == "inline";
4804          }
4805  
4806          function createAdjacentMergeableTextNodeGetter(forward) {
4807              var siblingPropName = forward ? "nextSibling" : "previousSibling";
4808  
4809              return function(textNode, checkParentElement) {
4810                  var el = textNode.parentNode;
4811                  var adjacentNode = textNode[siblingPropName];
4812                  if (adjacentNode) {
4813                      // Can merge if the node's previous/next sibling is a text node
4814                      if (adjacentNode && adjacentNode.nodeType == 3) {
4815                          return adjacentNode;
4816                      }
4817                  } else if (checkParentElement) {
4818                      // Compare text node parent element with its sibling
4819                      adjacentNode = el[siblingPropName];
4820                      if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
4821                          var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
4822                          if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
4823                              return adjacentNodeChild;
4824                          }
4825                      }
4826                  }
4827                  return null;
4828              };
4829          }
4830  
4831          var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
4832              getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
4833  
4834      
4835          function Merge(firstNode) {
4836              this.isElementMerge = (firstNode.nodeType == 1);
4837              this.textNodes = [];
4838              var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
4839              if (firstTextNode) {
4840                  this.textNodes[0] = firstTextNode;
4841              }
4842          }
4843  
4844          Merge.prototype = {
4845              doMerge: function(positionsToPreserve) {
4846                  var textNodes = this.textNodes;
4847                  var firstTextNode = textNodes[0];
4848                  if (textNodes.length > 1) {
4849                      var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
4850                      var textParts = [], combinedTextLength = 0, textNode, parent;
4851                      forEach(textNodes, function(textNode, i) {
4852                          parent = textNode.parentNode;
4853                          if (i > 0) {
4854                              parent.removeChild(textNode);
4855                              if (!parent.hasChildNodes()) {
4856                                  dom.removeNode(parent);
4857                              }
4858                              if (positionsToPreserve) {
4859                                  forEach(positionsToPreserve, function(position) {
4860                                      // Handle case where position is inside the text node being merged into a preceding node
4861                                      if (position.node == textNode) {
4862                                          position.node = firstTextNode;
4863                                          position.offset += combinedTextLength;
4864                                      }
4865                                      // Handle case where both text nodes precede the position within the same parent node
4866                                      if (position.node == parent && position.offset > firstTextNodeIndex) {
4867                                          --position.offset;
4868                                          if (position.offset == firstTextNodeIndex + 1 && i < len - 1) {
4869                                              position.node = firstTextNode;
4870                                              position.offset = combinedTextLength;
4871                                          }
4872                                      }
4873                                  });
4874                              }
4875                          }
4876                          textParts[i] = textNode.data;
4877                          combinedTextLength += textNode.data.length;
4878                      });
4879                      firstTextNode.data = textParts.join("");
4880                  }
4881                  return firstTextNode.data;
4882              },
4883  
4884              getLength: function() {
4885                  var i = this.textNodes.length, len = 0;
4886                  while (i--) {
4887                      len += this.textNodes[i].length;
4888                  }
4889                  return len;
4890              },
4891  
4892              toString: function() {
4893                  var textParts = [];
4894                  forEach(this.textNodes, function(textNode, i) {
4895                      textParts[i] = "'" + textNode.data + "'";
4896                  });
4897                  return "[Merge(" + textParts.join(",") + ")]";
4898              }
4899          };
4900  
4901          var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
4902              "removeEmptyElements", "onElementCreate"];
4903  
4904          // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
4905          var attrNamesForProperties = {};
4906  
4907          function ClassApplier(className, options, tagNames) {
4908              var normalize, i, len, propName, applier = this;
4909              applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
4910  
4911              var elementPropertiesFromOptions = null, elementAttributes = {};
4912  
4913              // Initialize from options object
4914              if (typeof options == "object" && options !== null) {
4915                  if (typeof options.elementTagName !== "undefined") {
4916                      options.elementTagName = options.elementTagName.toLowerCase();
4917                  }
4918                  tagNames = options.tagNames;
4919                  elementPropertiesFromOptions = options.elementProperties;
4920                  elementAttributes = options.elementAttributes;
4921  
4922                  for (i = 0; propName = optionProperties[i++]; ) {
4923                      if (options.hasOwnProperty(propName)) {
4924                          applier[propName] = options[propName];
4925                      }
4926                  }
4927                  normalize = options.normalize;
4928              } else {
4929                  normalize = options;
4930              }
4931  
4932              // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
4933              applier.normalize = (typeof normalize == "undefined") ? true : normalize;
4934  
4935              // Initialize element properties and attribute exceptions
4936              applier.attrExceptions = [];
4937              var el = document.createElement(applier.elementTagName);
4938              applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
4939              each(elementAttributes, function(attrName, attrValue) {
4940                  applier.attrExceptions.push(attrName);
4941                  // Ensure each attribute value is a string
4942                  elementAttributes[attrName] = "" + attrValue;
4943              });
4944              applier.elementAttributes = elementAttributes;
4945  
4946              applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
4947                  sortClassName(applier.elementProperties.className + " " + className) : className;
4948  
4949              // Initialize tag names
4950              applier.applyToAnyTagName = false;
4951              var type = typeof tagNames;
4952              if (type == "string") {
4953                  if (tagNames == "*") {
4954                      applier.applyToAnyTagName = true;
4955                  } else {
4956                      applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
4957                  }
4958              } else if (type == "object" && typeof tagNames.length == "number") {
4959                  applier.tagNames = [];
4960                  for (i = 0, len = tagNames.length; i < len; ++i) {
4961                      if (tagNames[i] == "*") {
4962                          applier.applyToAnyTagName = true;
4963                      } else {
4964                          applier.tagNames.push(tagNames[i].toLowerCase());
4965                      }
4966                  }
4967              } else {
4968                  applier.tagNames = [applier.elementTagName];
4969              }
4970          }
4971  
4972          ClassApplier.prototype = {
4973              elementTagName: defaultTagName,
4974              elementProperties: {},
4975              elementAttributes: {},
4976              ignoreWhiteSpace: true,
4977              applyToEditableOnly: false,
4978              useExistingElements: true,
4979              removeEmptyElements: true,
4980              onElementCreate: null,
4981  
4982              copyPropertiesToElement: function(props, el, createCopy) {
4983                  var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
4984  
4985                  for (var p in props) {
4986                      if (props.hasOwnProperty(p)) {
4987                          propValue = props[p];
4988                          elPropValue = el[p];
4989  
4990                          // Special case for class. The copied properties object has the applier's class as well as its own
4991                          // to simplify checks when removing styling elements
4992                          if (p == "className") {
4993                              addClass(el, propValue);
4994                              addClass(el, this.className);
4995                              el[p] = sortClassName(el[p]);
4996                              if (createCopy) {
4997                                  elProps[p] = propValue;
4998                              }
4999                          }
5000  
5001                          // Special case for style
5002                          else if (p == "style") {
5003                              elStyle = elPropValue;
5004                              if (createCopy) {
5005                                  elProps[p] = elPropsStyle = {};
5006                              }
5007                              for (s in props[p]) {
5008                                  if (props[p].hasOwnProperty(s)) {
5009                                      elStyle[s] = propValue[s];
5010                                      if (createCopy) {
5011                                          elPropsStyle[s] = elStyle[s];
5012                                      }
5013                                  }
5014                              }
5015                              this.attrExceptions.push(p);
5016                          } else {
5017                              el[p] = propValue;
5018                              // Copy the property back from the dummy element so that later comparisons to check whether
5019                              // elements may be removed are checking against the right value. For example, the href property
5020                              // of an element returns a fully qualified URL even if it was previously assigned a relative
5021                              // URL.
5022                              if (createCopy) {
5023                                  elProps[p] = el[p];
5024  
5025                                  // Not all properties map to identically-named attributes
5026                                  attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
5027                                  this.attrExceptions.push(attrName);
5028                              }
5029                          }
5030                      }
5031                  }
5032  
5033                  return createCopy ? elProps : "";
5034              },
5035  
5036              copyAttributesToElement: function(attrs, el) {
5037                  for (var attrName in attrs) {
5038                      if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
5039                          el.setAttribute(attrName, attrs[attrName]);
5040                      }
5041                  }
5042              },
5043  
5044              appliesToElement: function(el) {
5045                  return contains(this.tagNames, el.tagName.toLowerCase());
5046              },
5047  
5048              getEmptyElements: function(range) {
5049                  var applier = this;
5050                  return range.getNodes([1], function(el) {
5051                      return applier.appliesToElement(el) && !el.hasChildNodes();
5052                  });
5053              },
5054  
5055              hasClass: function(node) {
5056                  return node.nodeType == 1 &&
5057                      (this.applyToAnyTagName || this.appliesToElement(node)) &&
5058                      hasClass(node, this.className);
5059              },
5060  
5061              getSelfOrAncestorWithClass: function(node) {
5062                  while (node) {
5063                      if (this.hasClass(node)) {
5064                          return node;
5065                      }
5066                      node = node.parentNode;
5067                  }
5068                  return null;
5069              },
5070  
5071              isModifiable: function(node) {
5072                  return !this.applyToEditableOnly || isEditable(node);
5073              },
5074  
5075              // White space adjacent to an unwrappable node can be ignored for wrapping
5076              isIgnorableWhiteSpaceNode: function(node) {
5077                  return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
5078              },
5079  
5080              // Normalizes nodes after applying a class to a Range.
5081              postApply: function(textNodes, range, positionsToPreserve, isUndo) {
5082                  var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
5083  
5084                  var merges = [], currentMerge;
5085  
5086                  var rangeStartNode = firstNode, rangeEndNode = lastNode;
5087                  var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
5088  
5089                  var textNode, precedingTextNode;
5090  
5091                  // Check for every required merge and create a Merge object for each
5092                  forEach(textNodes, function(textNode) {
5093                      precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
5094                      if (precedingTextNode) {
5095                          if (!currentMerge) {
5096                              currentMerge = new Merge(precedingTextNode);
5097                              merges.push(currentMerge);
5098                          }
5099                          currentMerge.textNodes.push(textNode);
5100                          if (textNode === firstNode) {
5101                              rangeStartNode = currentMerge.textNodes[0];
5102                              rangeStartOffset = rangeStartNode.length;
5103                          }
5104                          if (textNode === lastNode) {
5105                              rangeEndNode = currentMerge.textNodes[0];
5106                              rangeEndOffset = currentMerge.getLength();
5107                          }
5108                      } else {
5109                          currentMerge = null;
5110                      }
5111                  });
5112  
5113                  // Test whether the first node after the range needs merging
5114                  var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
5115  
5116                  if (nextTextNode) {
5117                      if (!currentMerge) {
5118                          currentMerge = new Merge(lastNode);
5119                          merges.push(currentMerge);
5120                      }
5121                      currentMerge.textNodes.push(nextTextNode);
5122                  }
5123  
5124                  // Apply the merges
5125                  if (merges.length) {
5126                      for (i = 0, len = merges.length; i < len; ++i) {
5127                          merges[i].doMerge(positionsToPreserve);
5128                      }
5129  
5130                      // Set the range boundaries
5131                      range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
5132                  }
5133              },
5134  
5135              createContainer: function(parentNode) {
5136                  var doc = dom.getDocument(parentNode);
5137                  var namespace;
5138                  var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
5139                      doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
5140                      doc.createElement(this.elementTagName);
5141  
5142                  this.copyPropertiesToElement(this.elementProperties, el, false);
5143                  this.copyAttributesToElement(this.elementAttributes, el);
5144                  addClass(el, this.className);
5145                  if (this.onElementCreate) {
5146                      this.onElementCreate(el, this);
5147                  }
5148                  return el;
5149              },
5150  
5151              elementHasProperties: function(el, props) {
5152                  var applier = this;
5153                  return each(props, function(p, propValue) {
5154                      if (p == "className") {
5155                          // For checking whether we should reuse an existing element, we just want to check that the element
5156                          // has all the classes specified in the className property. When deciding whether the element is
5157                          // removable when unapplying a class, there is separate special handling to check whether the
5158                          // element has extra classes so the same simple check will do.
5159                          return hasAllClasses(el, propValue);
5160                      } else if (typeof propValue == "object") {
5161                          if (!applier.elementHasProperties(el[p], propValue)) {
5162                              return false;
5163                          }
5164                      } else if (el[p] !== propValue) {
5165                          return false;
5166                      }
5167                  });
5168              },
5169  
5170              elementHasAttributes: function(el, attrs) {
5171                  return each(attrs, function(name, value) {
5172                      if (el.getAttribute(name) !== value) {
5173                          return false;
5174                      }
5175                  });
5176              },
5177  
5178              applyToTextNode: function(textNode, positionsToPreserve) {
5179  
5180                  // Check whether the text node can be styled. Text within a <style> or <script> element, for example,
5181                  // should not be styled. See issue 283.
5182                  if (canTextBeStyled(textNode)) {
5183                      var parent = textNode.parentNode;
5184                      if (parent.childNodes.length == 1 &&
5185                          this.useExistingElements &&
5186                          this.appliesToElement(parent) &&
5187                          this.elementHasProperties(parent, this.elementProperties) &&
5188                          this.elementHasAttributes(parent, this.elementAttributes)) {
5189  
5190                          addClass(parent, this.className);
5191                      } else {
5192                          var textNodeParent = textNode.parentNode;
5193                          var el = this.createContainer(textNodeParent);
5194                          textNodeParent.insertBefore(el, textNode);
5195                          el.appendChild(textNode);
5196                      }
5197                  }
5198  
5199              },
5200  
5201              isRemovable: function(el) {
5202                  return el.tagName.toLowerCase() == this.elementTagName &&
5203                      getSortedClassName(el) == this.elementSortedClassName &&
5204                      this.elementHasProperties(el, this.elementProperties) &&
5205                      !elementHasNonClassAttributes(el, this.attrExceptions) &&
5206                      this.elementHasAttributes(el, this.elementAttributes) &&
5207                      this.isModifiable(el);
5208              },
5209  
5210              isEmptyContainer: function(el) {
5211                  var childNodeCount = el.childNodes.length;
5212                  return el.nodeType == 1 &&
5213                      this.isRemovable(el) &&
5214                      (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
5215              },
5216  
5217              removeEmptyContainers: function(range) {
5218                  var applier = this;
5219                  var nodesToRemove = range.getNodes([1], function(el) {
5220                      return applier.isEmptyContainer(el);
5221                  });
5222  
5223                  var rangesToPreserve = [range];
5224                  var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
5225  
5226                  forEach(nodesToRemove, function(node) {
5227                      removePreservingPositions(node, positionsToPreserve);
5228                  });
5229  
5230                  // Update the range from the preserved boundary positions
5231                  updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
5232              },
5233  
5234              undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
5235                  if (!range.containsNode(ancestorWithClass)) {
5236                      // Split out the portion of the ancestor from which we can remove the class
5237                      //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
5238                      var ancestorRange = range.cloneRange();
5239                      ancestorRange.selectNode(ancestorWithClass);
5240                      if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
5241                          splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
5242                          range.setEndAfter(ancestorWithClass);
5243                      }
5244                      if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
5245                          ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
5246                      }
5247                  }
5248  
5249                  if (this.isRemovable(ancestorWithClass)) {
5250                      replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
5251                  } else {
5252                      removeClass(ancestorWithClass, this.className);
5253                  }
5254              },
5255  
5256              splitAncestorWithClass: function(container, offset, positionsToPreserve) {
5257                  var ancestorWithClass = this.getSelfOrAncestorWithClass(container);
5258                  if (ancestorWithClass) {
5259                      splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve);
5260                  }
5261              },
5262  
5263              undoToAncestor: function(ancestorWithClass, positionsToPreserve) {
5264                  if (this.isRemovable(ancestorWithClass)) {
5265                      replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
5266                  } else {
5267                      removeClass(ancestorWithClass, this.className);
5268                  }
5269              },
5270  
5271              applyToRange: function(range, rangesToPreserve) {
5272                  var applier = this;
5273                  rangesToPreserve = rangesToPreserve || [];
5274  
5275                  // Create an array of range boundaries to preserve
5276                  var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
5277  
5278                  range.splitBoundariesPreservingPositions(positionsToPreserve);
5279  
5280                  // Tidy up the DOM by removing empty containers
5281                  if (applier.removeEmptyElements) {
5282                      applier.removeEmptyContainers(range);
5283                  }
5284  
5285                  var textNodes = getEffectiveTextNodes(range);
5286  
5287                  if (textNodes.length) {
5288                      forEach(textNodes, function(textNode) {
5289                          if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) &&
5290                                  applier.isModifiable(textNode)) {
5291                              applier.applyToTextNode(textNode, positionsToPreserve);
5292                          }
5293                      });
5294                      var lastTextNode = textNodes[textNodes.length - 1];
5295                      range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
5296                      if (applier.normalize) {
5297                          applier.postApply(textNodes, range, positionsToPreserve, false);
5298                      }
5299  
5300                      // Update the ranges from the preserved boundary positions
5301                      updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
5302                  }
5303  
5304                  // Apply classes to any appropriate empty elements
5305                  var emptyElements = applier.getEmptyElements(range);
5306  
5307                  forEach(emptyElements, function(el) {
5308                      addClass(el, applier.className);
5309                  });
5310              },
5311  
5312              applyToRanges: function(ranges) {
5313  
5314                  var i = ranges.length;
5315                  while (i--) {
5316                      this.applyToRange(ranges[i], ranges);
5317                  }
5318  
5319  
5320                  return ranges;
5321              },
5322  
5323              applyToSelection: function(win) {
5324                  var sel = api.getSelection(win);
5325                  sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
5326              },
5327  
5328              undoToRange: function(range, rangesToPreserve) {
5329                  var applier = this;
5330                  // Create an array of range boundaries to preserve
5331                  rangesToPreserve = rangesToPreserve || [];
5332                  var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
5333  
5334  
5335                  range.splitBoundariesPreservingPositions(positionsToPreserve);
5336  
5337                  // Tidy up the DOM by removing empty containers
5338                  if (applier.removeEmptyElements) {
5339                      applier.removeEmptyContainers(range, positionsToPreserve);
5340                  }
5341  
5342                  var textNodes = getEffectiveTextNodes(range);
5343                  var textNode, ancestorWithClass;
5344                  var lastTextNode = textNodes[textNodes.length - 1];
5345  
5346                  if (textNodes.length) {
5347                      applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve);
5348                      applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve);
5349                      for (var i = 0, len = textNodes.length; i < len; ++i) {
5350                          textNode = textNodes[i];
5351                          ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode);
5352                          if (ancestorWithClass && applier.isModifiable(textNode)) {
5353                              applier.undoToAncestor(ancestorWithClass, positionsToPreserve);
5354                          }
5355                      }
5356                      // Ensure the range is still valid
5357                      range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
5358  
5359  
5360                      if (applier.normalize) {
5361                          applier.postApply(textNodes, range, positionsToPreserve, true);
5362                      }
5363  
5364                      // Update the ranges from the preserved boundary positions
5365                      updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
5366                  }
5367  
5368                  // Remove class from any appropriate empty elements
5369                  var emptyElements = applier.getEmptyElements(range);
5370  
5371                  forEach(emptyElements, function(el) {
5372                      removeClass(el, applier.className);
5373                  });
5374              },
5375  
5376              undoToRanges: function(ranges) {
5377                  // Get ranges returned in document order
5378                  var i = ranges.length;
5379  
5380                  while (i--) {
5381                      this.undoToRange(ranges[i], ranges);
5382                  }
5383  
5384                  return ranges;
5385              },
5386  
5387              undoToSelection: function(win) {
5388                  var sel = api.getSelection(win);
5389                  var ranges = api.getSelection(win).getAllRanges();
5390                  this.undoToRanges(ranges);
5391                  sel.setRanges(ranges);
5392              },
5393  
5394              isAppliedToRange: function(range) {
5395                  if (range.collapsed || range.toString() == "") {
5396                      return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
5397                  } else {
5398                      var textNodes = range.getNodes( [3] );
5399                      if (textNodes.length)
5400                      for (var i = 0, textNode; textNode = textNodes[i++]; ) {
5401                          if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) &&
5402                                  this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
5403                              return false;
5404                          }
5405                      }
5406                      return true;
5407                  }
5408              },
5409  
5410              isAppliedToRanges: function(ranges) {
5411                  var i = ranges.length;
5412                  if (i == 0) {
5413                      return false;
5414                  }
5415                  while (i--) {
5416                      if (!this.isAppliedToRange(ranges[i])) {
5417                          return false;
5418                      }
5419                  }
5420                  return true;
5421              },
5422  
5423              isAppliedToSelection: function(win) {
5424                  var sel = api.getSelection(win);
5425                  return this.isAppliedToRanges(sel.getAllRanges());
5426              },
5427  
5428              toggleRange: function(range) {
5429                  if (this.isAppliedToRange(range)) {
5430                      this.undoToRange(range);
5431                  } else {
5432                      this.applyToRange(range);
5433                  }
5434              },
5435  
5436              toggleSelection: function(win) {
5437                  if (this.isAppliedToSelection(win)) {
5438                      this.undoToSelection(win);
5439                  } else {
5440                      this.applyToSelection(win);
5441                  }
5442              },
5443  
5444              getElementsWithClassIntersectingRange: function(range) {
5445                  var elements = [];
5446                  var applier = this;
5447                  range.getNodes([3], function(textNode) {
5448                      var el = applier.getSelfOrAncestorWithClass(textNode);
5449                      if (el && !contains(elements, el)) {
5450                          elements.push(el);
5451                      }
5452                  });
5453                  return elements;
5454              },
5455  
5456              detach: function() {}
5457          };
5458  
5459          function createClassApplier(className, options, tagNames) {
5460              return new ClassApplier(className, options, tagNames);
5461          }
5462  
5463          ClassApplier.util = {
5464              hasClass: hasClass,
5465              addClass: addClass,
5466              removeClass: removeClass,
5467              getClass: getClass,
5468              hasSameClasses: haveSameClasses,
5469              hasAllClasses: hasAllClasses,
5470              replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
5471              elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
5472              elementHasNonClassAttributes: elementHasNonClassAttributes,
5473              splitNodeAt: splitNodeAt,
5474              isEditableElement: isEditableElement,
5475              isEditingHost: isEditingHost,
5476              isEditable: isEditable
5477          };
5478  
5479          api.CssClassApplier = api.ClassApplier = ClassApplier;
5480          api.createClassApplier = createClassApplier;
5481          util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module);
5482      });
5483      
5484      return rangy;
5485  }, this);
5486  
5487  /**

5488   * Highlighter module for Rangy, a cross-browser JavaScript range and selection library

5489   * https://github.com/timdown/rangy

5490   *

5491   * Depends on Rangy core, ClassApplier and optionally TextRange modules.

5492   *

5493   * Copyright 2015, Tim Down

5494   * Licensed under the MIT license.

5495   * Version: 1.3.0

5496   * Build date: 10 May 2015

5497   */
5498  (function(factory, root) {
5499      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
5500      factory(root.rangy);
5501  })(function(rangy) {
5502      rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
5503          var dom = api.dom;
5504          var contains = dom.arrayContains;
5505          var getBody = dom.getBody;
5506          var createOptions = api.util.createOptions;
5507          var forEach = api.util.forEach;
5508          var nextHighlightId = 1;
5509  
5510          // Puts highlights in order, last in document first.
5511          function compareHighlights(h1, h2) {
5512              return h1.characterRange.start - h2.characterRange.start;
5513          }
5514  
5515          function getContainerElement(doc, id) {
5516              return id ? doc.getElementById(id) : getBody(doc);
5517          }
5518  
5519          /*----------------------------------------------------------------------------------------------------------------*/
5520  
5521          var highlighterTypes = {};
5522  
5523          function HighlighterType(type, converterCreator) {
5524              this.type = type;
5525              this.converterCreator = converterCreator;
5526          }
5527  
5528          HighlighterType.prototype.create = function() {
5529              var converter = this.converterCreator();
5530              converter.type = this.type;
5531              return converter;
5532          };
5533  
5534          function registerHighlighterType(type, converterCreator) {
5535              highlighterTypes[type] = new HighlighterType(type, converterCreator);
5536          }
5537  
5538          function getConverter(type) {
5539              var highlighterType = highlighterTypes[type];
5540              if (highlighterType instanceof HighlighterType) {
5541                  return highlighterType.create();
5542              } else {
5543                  throw new Error("Highlighter type '" + type + "' is not valid");
5544              }
5545          }
5546  
5547          api.registerHighlighterType = registerHighlighterType;
5548  
5549          /*----------------------------------------------------------------------------------------------------------------*/
5550  
5551          function CharacterRange(start, end) {
5552              this.start = start;
5553              this.end = end;
5554          }
5555  
5556          CharacterRange.prototype = {
5557              intersects: function(charRange) {
5558                  return this.start < charRange.end && this.end > charRange.start;
5559              },
5560  
5561              isContiguousWith: function(charRange) {
5562                  return this.start == charRange.end || this.end == charRange.start;
5563              },
5564  
5565              union: function(charRange) {
5566                  return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
5567              },
5568  
5569              intersection: function(charRange) {
5570                  return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
5571              },
5572  
5573              getComplements: function(charRange) {
5574                  var ranges = [];
5575                  if (this.start >= charRange.start) {
5576                      if (this.end <= charRange.end) {
5577                          return [];
5578                      }
5579                      ranges.push(new CharacterRange(charRange.end, this.end));
5580                  } else {
5581                      ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start)));
5582                      if (this.end > charRange.end) {
5583                          ranges.push(new CharacterRange(charRange.end, this.end));
5584                      }
5585                  }
5586                  return ranges;
5587              },
5588  
5589              toString: function() {
5590                  return "[CharacterRange(" + this.start + ", " + this.end + ")]";
5591              }
5592          };
5593  
5594          CharacterRange.fromCharacterRange = function(charRange) {
5595              return new CharacterRange(charRange.start, charRange.end);
5596          };
5597  
5598          /*----------------------------------------------------------------------------------------------------------------*/
5599  
5600          var textContentConverter = {
5601              rangeToCharacterRange: function(range, containerNode) {
5602                  var bookmark = range.getBookmark(containerNode);
5603                  return new CharacterRange(bookmark.start, bookmark.end);
5604              },
5605  
5606              characterRangeToRange: function(doc, characterRange, containerNode) {
5607                  var range = api.createRange(doc);
5608                  range.moveToBookmark({
5609                      start: characterRange.start,
5610                      end: characterRange.end,
5611                      containerNode: containerNode
5612                  });
5613  
5614                  return range;
5615              },
5616  
5617              serializeSelection: function(selection, containerNode) {
5618                  var ranges = selection.getAllRanges(), rangeCount = ranges.length;
5619                  var rangeInfos = [];
5620  
5621                  var backward = rangeCount == 1 && selection.isBackward();
5622  
5623                  for (var i = 0, len = ranges.length; i < len; ++i) {
5624                      rangeInfos[i] = {
5625                          characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
5626                          backward: backward
5627                      };
5628                  }
5629  
5630                  return rangeInfos;
5631              },
5632  
5633              restoreSelection: function(selection, savedSelection, containerNode) {
5634                  selection.removeAllRanges();
5635                  var doc = selection.win.document;
5636                  for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
5637                      rangeInfo = savedSelection[i];
5638                      characterRange = rangeInfo.characterRange;
5639                      range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
5640                      selection.addRange(range, rangeInfo.backward);
5641                  }
5642              }
5643          };
5644  
5645          registerHighlighterType("textContent", function() {
5646              return textContentConverter;
5647          });
5648  
5649          /*----------------------------------------------------------------------------------------------------------------*/
5650  
5651          // Lazily load the TextRange-based converter so that the dependency is only checked when required.
5652          registerHighlighterType("TextRange", (function() {
5653              var converter;
5654  
5655              return function() {
5656                  if (!converter) {
5657                      // Test that textRangeModule exists and is supported
5658                      var textRangeModule = api.modules.TextRange;
5659                      if (!textRangeModule) {
5660                          throw new Error("TextRange module is missing.");
5661                      } else if (!textRangeModule.supported) {
5662                          throw new Error("TextRange module is present but not supported.");
5663                      }
5664  
5665                      converter = {
5666                          rangeToCharacterRange: function(range, containerNode) {
5667                              return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
5668                          },
5669  
5670                          characterRangeToRange: function(doc, characterRange, containerNode) {
5671                              var range = api.createRange(doc);
5672                              range.selectCharacters(containerNode, characterRange.start, characterRange.end);
5673                              return range;
5674                          },
5675  
5676                          serializeSelection: function(selection, containerNode) {
5677                              return selection.saveCharacterRanges(containerNode);
5678                          },
5679  
5680                          restoreSelection: function(selection, savedSelection, containerNode) {
5681                              selection.restoreCharacterRanges(containerNode, savedSelection);
5682                          }
5683                      };
5684                  }
5685  
5686                  return converter;
5687              };
5688          })());
5689  
5690          /*----------------------------------------------------------------------------------------------------------------*/
5691  
5692          function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
5693              if (id) {
5694                  this.id = id;
5695                  nextHighlightId = Math.max(nextHighlightId, id + 1);
5696              } else {
5697                  this.id = nextHighlightId++;
5698              }
5699              this.characterRange = characterRange;
5700              this.doc = doc;
5701              this.classApplier = classApplier;
5702              this.converter = converter;
5703              this.containerElementId = containerElementId || null;
5704              this.applied = false;
5705          }
5706  
5707          Highlight.prototype = {
5708              getContainerElement: function() {
5709                  return getContainerElement(this.doc, this.containerElementId);
5710              },
5711  
5712              getRange: function() {
5713                  return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
5714              },
5715  
5716              fromRange: function(range) {
5717                  this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
5718              },
5719  
5720              getText: function() {
5721                  return this.getRange().toString();
5722              },
5723  
5724              containsElement: function(el) {
5725                  return this.getRange().containsNodeContents(el.firstChild);
5726              },
5727  
5728              unapply: function() {
5729                  this.classApplier.undoToRange(this.getRange());
5730                  this.applied = false;
5731              },
5732  
5733              apply: function() {
5734                  this.classApplier.applyToRange(this.getRange());
5735                  this.applied = true;
5736              },
5737  
5738              getHighlightElements: function() {
5739                  return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
5740              },
5741  
5742              toString: function() {
5743                  return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " +
5744                      this.characterRange.start + " - " + this.characterRange.end + ")]";
5745              }
5746          };
5747  
5748          /*----------------------------------------------------------------------------------------------------------------*/
5749  
5750          function Highlighter(doc, type) {
5751              type = type || "textContent";
5752              this.doc = doc || document;
5753              this.classAppliers = {};
5754              this.highlights = [];
5755              this.converter = getConverter(type);
5756          }
5757  
5758          Highlighter.prototype = {
5759              addClassApplier: function(classApplier) {
5760                  this.classAppliers[classApplier.className] = classApplier;
5761              },
5762  
5763              getHighlightForElement: function(el) {
5764                  var highlights = this.highlights;
5765                  for (var i = 0, len = highlights.length; i < len; ++i) {
5766                      if (highlights[i].containsElement(el)) {
5767                          return highlights[i];
5768                      }
5769                  }
5770                  return null;
5771              },
5772  
5773              removeHighlights: function(highlights) {
5774                  for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
5775                      highlight = this.highlights[i];
5776                      if (contains(highlights, highlight)) {
5777                          highlight.unapply();
5778                          this.highlights.splice(i--, 1);
5779                      }
5780                  }
5781              },
5782  
5783              removeAllHighlights: function() {
5784                  this.removeHighlights(this.highlights);
5785              },
5786  
5787              getIntersectingHighlights: function(ranges) {
5788                  // Test each range against each of the highlighted ranges to see whether they overlap
5789                  var intersectingHighlights = [], highlights = this.highlights;
5790                  forEach(ranges, function(range) {
5791                      //var selCharRange = converter.rangeToCharacterRange(range);
5792                      forEach(highlights, function(highlight) {
5793                          if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
5794                              intersectingHighlights.push(highlight);
5795                          }
5796                      });
5797                  });
5798  
5799                  return intersectingHighlights;
5800              },
5801  
5802              highlightCharacterRanges: function(className, charRanges, options) {
5803                  var i, len, j;
5804                  var highlights = this.highlights;
5805                  var converter = this.converter;
5806                  var doc = this.doc;
5807                  var highlightsToRemove = [];
5808                  var classApplier = className ? this.classAppliers[className] : null;
5809  
5810                  options = createOptions(options, {
5811                      containerElementId: null,
5812                      exclusive: true
5813                  });
5814  
5815                  var containerElementId = options.containerElementId;
5816                  var exclusive = options.exclusive;
5817  
5818                  var containerElement, containerElementRange, containerElementCharRange;
5819                  if (containerElementId) {
5820                      containerElement = this.doc.getElementById(containerElementId);
5821                      if (containerElement) {
5822                          containerElementRange = api.createRange(this.doc);
5823                          containerElementRange.selectNodeContents(containerElement);
5824                          containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
5825                      }
5826                  }
5827  
5828                  var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight;
5829  
5830                  for (i = 0, len = charRanges.length; i < len; ++i) {
5831                      charRange = charRanges[i];
5832                      highlightsToKeep = [];
5833  
5834                      // Restrict character range to container element, if it exists
5835                      if (containerElementCharRange) {
5836                          charRange = charRange.intersection(containerElementCharRange);
5837                      }
5838  
5839                      // Ignore empty ranges
5840                      if (charRange.start == charRange.end) {
5841                          continue;
5842                      }
5843  
5844                      // Check for intersection with existing highlights. For each intersection, create a new highlight
5845                      // which is the union of the highlight range and the selected range
5846                      for (j = 0; j < highlights.length; ++j) {
5847                          removeHighlight = false;
5848  
5849                          if (containerElementId == highlights[j].containerElementId) {
5850                              highlightCharRange = highlights[j].characterRange;
5851                              isSameClassApplier = (classApplier == highlights[j].classApplier);
5852                              splitHighlight = !isSameClassApplier && exclusive;
5853  
5854                              // Replace the existing highlight if it needs to be:
5855                              //  1. merged (isSameClassApplier)
5856                              //  2. partially or entirely erased (className === null)
5857                              //  3. partially or entirely replaced (isSameClassApplier == false && exclusive == true)
5858                              if (    (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) &&
5859                                      (isSameClassApplier || splitHighlight) ) {
5860  
5861                                  // Remove existing highlights, keeping the unselected parts
5862                                  if (splitHighlight) {
5863                                      forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) {
5864                                          highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) );
5865                                      });
5866                                  }
5867  
5868                                  removeHighlight = true;
5869                                  if (isSameClassApplier) {
5870                                      charRange = highlightCharRange.union(charRange);
5871                                  }
5872                              }
5873                          }
5874  
5875                          if (removeHighlight) {
5876                              highlightsToRemove.push(highlights[j]);
5877                              highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
5878                          } else {
5879                              highlightsToKeep.push(highlights[j]);
5880                          }
5881                      }
5882  
5883                      // Add new range
5884                      if (classApplier) {
5885                          highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));
5886                      }
5887                      this.highlights = highlights = highlightsToKeep;
5888                  }
5889  
5890                  // Remove the old highlights
5891                  forEach(highlightsToRemove, function(highlightToRemove) {
5892                      highlightToRemove.unapply();
5893                  });
5894  
5895                  // Apply new highlights
5896                  var newHighlights = [];
5897                  forEach(highlights, function(highlight) {
5898                      if (!highlight.applied) {
5899                          highlight.apply();
5900                          newHighlights.push(highlight);
5901                      }
5902                  });
5903  
5904                  return newHighlights;
5905              },
5906  
5907              highlightRanges: function(className, ranges, options) {
5908                  var selCharRanges = [];
5909                  var converter = this.converter;
5910  
5911                  options = createOptions(options, {
5912                      containerElement: null,
5913                      exclusive: true
5914                  });
5915  
5916                  var containerElement = options.containerElement;
5917                  var containerElementId = containerElement ? containerElement.id : null;
5918                  var containerElementRange;
5919                  if (containerElement) {
5920                      containerElementRange = api.createRange(containerElement);
5921                      containerElementRange.selectNodeContents(containerElement);
5922                  }
5923  
5924                  forEach(ranges, function(range) {
5925                      var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
5926                      selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
5927                  });
5928  
5929                  return this.highlightCharacterRanges(className, selCharRanges, {
5930                      containerElementId: containerElementId,
5931                      exclusive: options.exclusive
5932                  });
5933              },
5934  
5935              highlightSelection: function(className, options) {
5936                  var converter = this.converter;
5937                  var classApplier = className ? this.classAppliers[className] : false;
5938  
5939                  options = createOptions(options, {
5940                      containerElementId: null,
5941                      selection: api.getSelection(this.doc),
5942                      exclusive: true
5943                  });
5944  
5945                  var containerElementId = options.containerElementId;
5946                  var exclusive = options.exclusive;
5947                  var selection = options.selection;
5948                  var doc = selection.win.document;
5949                  var containerElement = getContainerElement(doc, containerElementId);
5950  
5951                  if (!classApplier && className !== false) {
5952                      throw new Error("No class applier found for class '" + className + "'");
5953                  }
5954  
5955                  // Store the existing selection as character ranges
5956                  var serializedSelection = converter.serializeSelection(selection, containerElement);
5957  
5958                  // Create an array of selected character ranges
5959                  var selCharRanges = [];
5960                  forEach(serializedSelection, function(rangeInfo) {
5961                      selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
5962                  });
5963  
5964                  var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {
5965                      containerElementId: containerElementId,
5966                      exclusive: exclusive
5967                  });
5968  
5969                  // Restore selection
5970                  converter.restoreSelection(selection, serializedSelection, containerElement);
5971  
5972                  return newHighlights;
5973              },
5974  
5975              unhighlightSelection: function(selection) {
5976                  selection = selection || api.getSelection(this.doc);
5977                  var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
5978                  this.removeHighlights(intersectingHighlights);
5979                  selection.removeAllRanges();
5980                  return intersectingHighlights;
5981              },
5982  
5983              getHighlightsInSelection: function(selection) {
5984                  selection = selection || api.getSelection(this.doc);
5985                  return this.getIntersectingHighlights(selection.getAllRanges());
5986              },
5987  
5988              selectionOverlapsHighlight: function(selection) {
5989                  return this.getHighlightsInSelection(selection).length > 0;
5990              },
5991  
5992              serialize: function(options) {
5993                  var highlighter = this;
5994                  var highlights = highlighter.highlights;
5995                  var serializedType, serializedHighlights, convertType, serializationConverter;
5996  
5997                  highlights.sort(compareHighlights);
5998                  options = createOptions(options, {
5999                      serializeHighlightText: false,
6000                      type: highlighter.converter.type
6001                  });
6002  
6003                  serializedType = options.type;
6004                  convertType = (serializedType != highlighter.converter.type);
6005  
6006                  if (convertType) {
6007                      serializationConverter = getConverter(serializedType);
6008                  }
6009  
6010                  serializedHighlights = ["type:" + serializedType];
6011  
6012                  forEach(highlights, function(highlight) {
6013                      var characterRange = highlight.characterRange;
6014                      var containerElement;
6015  
6016                      // Convert to the current Highlighter's type, if different from the serialization type
6017                      if (convertType) {
6018                          containerElement = highlight.getContainerElement();
6019                          characterRange = serializationConverter.rangeToCharacterRange(
6020                              highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement),
6021                              containerElement
6022                          );
6023                      }
6024  
6025                      var parts = [
6026                          characterRange.start,
6027                          characterRange.end,
6028                          highlight.id,
6029                          highlight.classApplier.className,
6030                          highlight.containerElementId
6031                      ];
6032  
6033                      if (options.serializeHighlightText) {
6034                          parts.push(highlight.getText());
6035                      }
6036                      serializedHighlights.push( parts.join("$") );
6037                  });
6038  
6039                  return serializedHighlights.join("|");
6040              },
6041  
6042              deserialize: function(serialized) {
6043                  var serializedHighlights = serialized.split("|");
6044                  var highlights = [];
6045  
6046                  var firstHighlight = serializedHighlights[0];
6047                  var regexResult;
6048                  var serializationType, serializationConverter, convertType = false;
6049                  if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
6050                      serializationType = regexResult[1];
6051                      if (serializationType != this.converter.type) {
6052                          serializationConverter = getConverter(serializationType);
6053                          convertType = true;
6054                      }
6055                      serializedHighlights.shift();
6056                  } else {
6057                      throw new Error("Serialized highlights are invalid.");
6058                  }
6059  
6060                  var classApplier, highlight, characterRange, containerElementId, containerElement;
6061  
6062                  for (var i = serializedHighlights.length, parts; i-- > 0; ) {
6063                      parts = serializedHighlights[i].split("$");
6064                      characterRange = new CharacterRange(+parts[0], +parts[1]);
6065                      containerElementId = parts[4] || null;
6066  
6067                      // Convert to the current Highlighter's type, if different from the serialization type
6068                      if (convertType) {
6069                          containerElement = getContainerElement(this.doc, containerElementId);
6070                          characterRange = this.converter.rangeToCharacterRange(
6071                              serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
6072                              containerElement
6073                          );
6074                      }
6075  
6076                      classApplier = this.classAppliers[ parts[3] ];
6077  
6078                      if (!classApplier) {
6079                          throw new Error("No class applier found for class '" + parts[3] + "'");
6080                      }
6081  
6082                      highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
6083                      highlight.apply();
6084                      highlights.push(highlight);
6085                  }
6086                  this.highlights = highlights;
6087              }
6088          };
6089  
6090          api.Highlighter = Highlighter;
6091  
6092          api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
6093              return new Highlighter(doc, rangeCharacterOffsetConverterType);
6094          };
6095      });
6096      
6097      return rangy;
6098  }, this);
6099  
6100  /**

6101   * Text range module for Rangy.

6102   * Text-based manipulation and searching of ranges and selections.

6103   *

6104   * Features

6105   *

6106   * - Ability to move range boundaries by character or word offsets

6107   * - Customizable word tokenizer

6108   * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties

6109   * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case

6110   *   sensitivity

6111   * - Selection and range save/restore as text offsets within a node

6112   * - Methods to return visible text within a range or selection

6113   * - innerText method for elements

6114   *

6115   * References

6116   *

6117   * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145

6118   * http://aryeh.name/spec/innertext/innertext.html

6119   * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html

6120   *

6121   * Part of Rangy, a cross-browser JavaScript range and selection library

6122   * https://github.com/timdown/rangy

6123   *

6124   * Depends on Rangy core.

6125   *

6126   * Copyright 2015, Tim Down

6127   * Licensed under the MIT license.

6128   * Version: 1.3.0

6129   * Build date: 10 May 2015

6130   */
6131  
6132  /**

6133   * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.

6134   *

6135   * First, a <br>: this is relatively simple. For the following HTML:

6136   *

6137   * 1 <br>2

6138   *

6139   * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a

6140   *   textarea, the space is present) and allow the caret to be placed after it.

6141   * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.

6142   * - Opera does not render the space but has two separate caret positions on either side of the space (left and right

6143   *   arrow keys show this) and includes the space in the selection.

6144   *

6145   * The other case is the line break or breaks implied by block elements. For the following HTML:

6146   *

6147   * <p>1 </p><p>2<p>

6148   *

6149   * - WebKit does not acknowledge the space in any way

6150   * - Firefox, IE and Opera as per <br>

6151   *

6152   * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:

6153   *

6154   * <p style="white-space: pre-line">1

6155   * 2</p>

6156   *

6157   * - Firefox and WebKit include the space in caret positions

6158   * - IE does not support pre-line up to and including version 9

6159   * - Opera ignores the space

6160   * - Trailing space only renders if there is a non-collapsed character in the line

6161   *

6162   * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be

6163   * feature-tested

6164   */
6165  (function(factory, root) {
6166      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
6167      factory(root.rangy);
6168  })(function(rangy) {
6169      rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
6170          var UNDEF = "undefined";
6171          var CHARACTER = "character", WORD = "word";
6172          var dom = api.dom, util = api.util;
6173          var extend = util.extend;
6174          var createOptions = util.createOptions;
6175          var getBody = dom.getBody;
6176  
6177  
6178          var spacesRegex = /^[ \t\f\r\n]+$/;
6179          var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
6180          var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
6181          var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
6182          var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
6183  
6184          var defaultLanguage = "en";
6185  
6186          var isDirectionBackward = api.Selection.isDirectionBackward;
6187  
6188          // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
6189          // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
6190          var trailingSpaceInBlockCollapses = false;
6191          var trailingSpaceBeforeBrCollapses = false;
6192          var trailingSpaceBeforeBlockCollapses = false;
6193          var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
6194  
6195          (function() {
6196              var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
6197              var p = el.firstChild;
6198              var sel = api.getSelection();
6199              sel.collapse(p.lastChild, 2);
6200              sel.setStart(p.firstChild, 0);
6201              trailingSpaceInBlockCollapses = ("" + sel).length == 1;
6202  
6203              el.innerHTML = "1 <br />";
6204              sel.collapse(el, 2);
6205              sel.setStart(el.firstChild, 0);
6206              trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
6207  
6208              el.innerHTML = "1 <p>1</p>";
6209              sel.collapse(el, 2);
6210              sel.setStart(el.firstChild, 0);
6211              trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
6212  
6213              dom.removeNode(el);
6214              sel.removeAllRanges();
6215          })();
6216  
6217          /*----------------------------------------------------------------------------------------------------------------*/
6218  
6219          // This function must create word and non-word tokens for the whole of the text supplied to it
6220          function defaultTokenizer(chars, wordOptions) {
6221              var word = chars.join(""), result, tokenRanges = [];
6222  
6223              function createTokenRange(start, end, isWord) {
6224                  tokenRanges.push( { start: start, end: end, isWord: isWord } );
6225              }
6226  
6227              // Match words and mark characters
6228              var lastWordEnd = 0, wordStart, wordEnd;
6229              while ( (result = wordOptions.wordRegex.exec(word)) ) {
6230                  wordStart = result.index;
6231                  wordEnd = wordStart + result[0].length;
6232  
6233                  // Create token for non-word characters preceding this word
6234                  if (wordStart > lastWordEnd) {
6235                      createTokenRange(lastWordEnd, wordStart, false);
6236                  }
6237  
6238                  // Get trailing space characters for word
6239                  if (wordOptions.includeTrailingSpace) {
6240                      while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
6241                          ++wordEnd;
6242                      }
6243                  }
6244                  createTokenRange(wordStart, wordEnd, true);
6245                  lastWordEnd = wordEnd;
6246              }
6247  
6248              // Create token for trailing non-word characters, if any exist
6249              if (lastWordEnd < chars.length) {
6250                  createTokenRange(lastWordEnd, chars.length, false);
6251              }
6252  
6253              return tokenRanges;
6254          }
6255  
6256          function convertCharRangeToToken(chars, tokenRange) {
6257              var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
6258              var token = {
6259                  isWord: tokenRange.isWord,
6260                  chars: tokenChars,
6261                  toString: function() {
6262                      return tokenChars.join("");
6263                  }
6264              };
6265              for (var i = 0, len = tokenChars.length; i < len; ++i) {
6266                  tokenChars[i].token = token;
6267              }
6268              return token;
6269          }
6270  
6271          function tokenize(chars, wordOptions, tokenizer) {
6272              var tokenRanges = tokenizer(chars, wordOptions);
6273              var tokens = [];
6274              for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
6275                  tokens.push( convertCharRangeToToken(chars, tokenRange) );
6276              }
6277              return tokens;
6278          }
6279  
6280          var defaultCharacterOptions = {
6281              includeBlockContentTrailingSpace: true,
6282              includeSpaceBeforeBr: true,
6283              includeSpaceBeforeBlock: true,
6284              includePreLineTrailingSpace: true,
6285              ignoreCharacters: ""
6286          };
6287  
6288          function normalizeIgnoredCharacters(ignoredCharacters) {
6289              // Check if character is ignored
6290              var ignoredChars = ignoredCharacters || "";
6291  
6292              // Normalize ignored characters into a string consisting of characters in ascending order of character code
6293              var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
6294              ignoredCharsArray.sort(function(char1, char2) {
6295                  return char1.charCodeAt(0) - char2.charCodeAt(0);
6296              });
6297  
6298              /// Convert back to a string and remove duplicates
6299              return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
6300          }
6301  
6302          var defaultCaretCharacterOptions = {
6303              includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
6304              includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
6305              includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
6306              includePreLineTrailingSpace: true
6307          };
6308  
6309          var defaultWordOptions = {
6310              "en": {
6311                  wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
6312                  includeTrailingSpace: false,
6313                  tokenizer: defaultTokenizer
6314              }
6315          };
6316  
6317          var defaultFindOptions = {
6318              caseSensitive: false,
6319              withinRange: null,
6320              wholeWordsOnly: false,
6321              wrap: false,
6322              direction: "forward",
6323              wordOptions: null,
6324              characterOptions: null
6325          };
6326  
6327          var defaultMoveOptions = {
6328              wordOptions: null,
6329              characterOptions: null
6330          };
6331  
6332          var defaultExpandOptions = {
6333              wordOptions: null,
6334              characterOptions: null,
6335              trim: false,
6336              trimStart: true,
6337              trimEnd: true
6338          };
6339  
6340          var defaultWordIteratorOptions = {
6341              wordOptions: null,
6342              characterOptions: null,
6343              direction: "forward"
6344          };
6345  
6346          function createWordOptions(options) {
6347              var lang, defaults;
6348              if (!options) {
6349                  return defaultWordOptions[defaultLanguage];
6350              } else {
6351                  lang = options.language || defaultLanguage;
6352                  defaults = {};
6353                  extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
6354                  extend(defaults, options);
6355                  return defaults;
6356              }
6357          }
6358  
6359          function createNestedOptions(optionsParam, defaults) {
6360              var options = createOptions(optionsParam, defaults);
6361              if (defaults.hasOwnProperty("wordOptions")) {
6362                  options.wordOptions = createWordOptions(options.wordOptions);
6363              }
6364              if (defaults.hasOwnProperty("characterOptions")) {
6365                  options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
6366              }
6367              return options;
6368          }
6369  
6370          /*----------------------------------------------------------------------------------------------------------------*/
6371  
6372          /* DOM utility functions */
6373          var getComputedStyleProperty = dom.getComputedStyleProperty;
6374  
6375          // Create cachable versions of DOM functions
6376  
6377          // Test for old IE's incorrect display properties
6378          var tableCssDisplayBlock;
6379          (function() {
6380              var table = document.createElement("table");
6381              var body = getBody(document);
6382              body.appendChild(table);
6383              tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
6384              body.removeChild(table);
6385          })();
6386  
6387          var defaultDisplayValueForTag = {
6388              table: "table",
6389              caption: "table-caption",
6390              colgroup: "table-column-group",
6391              col: "table-column",
6392              thead: "table-header-group",
6393              tbody: "table-row-group",
6394              tfoot: "table-footer-group",
6395              tr: "table-row",
6396              td: "table-cell",
6397              th: "table-cell"
6398          };
6399  
6400          // Corrects IE's "block" value for table-related elements
6401          function getComputedDisplay(el, win) {
6402              var display = getComputedStyleProperty(el, "display", win);
6403              var tagName = el.tagName.toLowerCase();
6404              return (display == "block" &&
6405                      tableCssDisplayBlock &&
6406                      defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
6407                  defaultDisplayValueForTag[tagName] : display;
6408          }
6409  
6410          function isHidden(node) {
6411              var ancestors = getAncestorsAndSelf(node);
6412              for (var i = 0, len = ancestors.length; i < len; ++i) {
6413                  if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
6414                      return true;
6415                  }
6416              }
6417  
6418              return false;
6419          }
6420  
6421          function isVisibilityHiddenTextNode(textNode) {
6422              var el;
6423              return textNode.nodeType == 3 &&
6424                  (el = textNode.parentNode) &&
6425                  getComputedStyleProperty(el, "visibility") == "hidden";
6426          }
6427  
6428          /*----------------------------------------------------------------------------------------------------------------*/
6429  
6430      
6431          // "A block node is either an Element whose "display" property does not have
6432          // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
6433          // Document, or a DocumentFragment."
6434          function isBlockNode(node) {
6435              return node &&
6436                  ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
6437                  node.nodeType == 9 || node.nodeType == 11);
6438          }
6439  
6440          function getLastDescendantOrSelf(node) {
6441              var lastChild = node.lastChild;
6442              return lastChild ? getLastDescendantOrSelf(lastChild) : node;
6443          }
6444  
6445          function containsPositions(node) {
6446              return dom.isCharacterDataNode(node) ||
6447                  !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
6448          }
6449  
6450          function getAncestors(node) {
6451              var ancestors = [];
6452              while (node.parentNode) {
6453                  ancestors.unshift(node.parentNode);
6454                  node = node.parentNode;
6455              }
6456              return ancestors;
6457          }
6458  
6459          function getAncestorsAndSelf(node) {
6460              return getAncestors(node).concat([node]);
6461          }
6462  
6463          function nextNodeDescendants(node) {
6464              while (node && !node.nextSibling) {
6465                  node = node.parentNode;
6466              }
6467              if (!node) {
6468                  return null;
6469              }
6470              return node.nextSibling;
6471          }
6472  
6473          function nextNode(node, excludeChildren) {
6474              if (!excludeChildren && node.hasChildNodes()) {
6475                  return node.firstChild;
6476              }
6477              return nextNodeDescendants(node);
6478          }
6479  
6480          function previousNode(node) {
6481              var previous = node.previousSibling;
6482              if (previous) {
6483                  node = previous;
6484                  while (node.hasChildNodes()) {
6485                      node = node.lastChild;
6486                  }
6487                  return node;
6488              }
6489              var parent = node.parentNode;
6490              if (parent && parent.nodeType == 1) {
6491                  return parent;
6492              }
6493              return null;
6494          }
6495  
6496          // Adpated from Aryeh's code.
6497          // "A whitespace node is either a Text node whose data is the empty string; or
6498          // a Text node whose data consists only of one or more tabs (0x0009), line
6499          // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
6500          // parent is an Element whose resolved value for "white-space" is "normal" or
6501          // "nowrap"; or a Text node whose data consists only of one or more tabs
6502          // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
6503          // parent is an Element whose resolved value for "white-space" is "pre-line"."
6504          function isWhitespaceNode(node) {
6505              if (!node || node.nodeType != 3) {
6506                  return false;
6507              }
6508              var text = node.data;
6509              if (text === "") {
6510                  return true;
6511              }
6512              var parent = node.parentNode;
6513              if (!parent || parent.nodeType != 1) {
6514                  return false;
6515              }
6516              var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
6517  
6518              return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
6519                  (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
6520          }
6521  
6522          // Adpated from Aryeh's code.
6523          // "node is a collapsed whitespace node if the following algorithm returns
6524          // true:"
6525          function isCollapsedWhitespaceNode(node) {
6526              // "If node's data is the empty string, return true."
6527              if (node.data === "") {
6528                  return true;
6529              }
6530  
6531              // "If node is not a whitespace node, return false."
6532              if (!isWhitespaceNode(node)) {
6533                  return false;
6534              }
6535  
6536              // "Let ancestor be node's parent."
6537              var ancestor = node.parentNode;
6538  
6539              // "If ancestor is null, return true."
6540              if (!ancestor) {
6541                  return true;
6542              }
6543  
6544              // "If the "display" property of some ancestor of node has resolved value "none", return true."
6545              if (isHidden(node)) {
6546                  return true;
6547              }
6548  
6549              return false;
6550          }
6551  
6552          function isCollapsedNode(node) {
6553              var type = node.nodeType;
6554              return type == 7 /* PROCESSING_INSTRUCTION */ ||
6555                  type == 8 /* COMMENT */ ||
6556                  isHidden(node) ||
6557                  /^(script|style)$/i.test(node.nodeName) ||
6558                  isVisibilityHiddenTextNode(node) ||
6559                  isCollapsedWhitespaceNode(node);
6560          }
6561  
6562          function isIgnoredNode(node, win) {
6563              var type = node.nodeType;
6564              return type == 7 /* PROCESSING_INSTRUCTION */ ||
6565                  type == 8 /* COMMENT */ ||
6566                  (type == 1 && getComputedDisplay(node, win) == "none");
6567          }
6568  
6569          /*----------------------------------------------------------------------------------------------------------------*/
6570  
6571          // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
6572  
6573          function Cache() {
6574              this.store = {};
6575          }
6576  
6577          Cache.prototype = {
6578              get: function(key) {
6579                  return this.store.hasOwnProperty(key) ? this.store[key] : null;
6580              },
6581  
6582              set: function(key, value) {
6583                  return this.store[key] = value;
6584              }
6585          };
6586  
6587          var cachedCount = 0, uncachedCount = 0;
6588  
6589          function createCachingGetter(methodName, func, objProperty) {
6590              return function(args) {
6591                  var cache = this.cache;
6592                  if (cache.hasOwnProperty(methodName)) {
6593                      cachedCount++;
6594                      return cache[methodName];
6595                  } else {
6596                      uncachedCount++;
6597                      var value = func.call(this, objProperty ? this[objProperty] : this, args);
6598                      cache[methodName] = value;
6599                      return value;
6600                  }
6601              };
6602          }
6603  
6604          /*----------------------------------------------------------------------------------------------------------------*/
6605  
6606          function NodeWrapper(node, session) {
6607              this.node = node;
6608              this.session = session;
6609              this.cache = new Cache();
6610              this.positions = new Cache();
6611          }
6612  
6613          var nodeProto = {
6614              getPosition: function(offset) {
6615                  var positions = this.positions;
6616                  return positions.get(offset) || positions.set(offset, new Position(this, offset));
6617              },
6618  
6619              toString: function() {
6620                  return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
6621              }
6622          };
6623  
6624          NodeWrapper.prototype = nodeProto;
6625  
6626          var EMPTY = "EMPTY",
6627              NON_SPACE = "NON_SPACE",
6628              UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
6629              COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
6630              TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
6631              TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
6632              TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
6633              PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
6634              TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
6635              INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
6636  
6637          extend(nodeProto, {
6638              isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
6639              getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
6640              getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
6641              containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
6642              isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
6643              isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
6644              getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
6645              isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
6646              isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
6647              next: createCachingGetter("nextPos", nextNode, "node"),
6648              previous: createCachingGetter("previous", previousNode, "node"),
6649  
6650              getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
6651                  var spaceRegex = null, collapseSpaces = false;
6652                  var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
6653                  var preLine = (cssWhitespace == "pre-line");
6654                  if (preLine) {
6655                      spaceRegex = spacesMinusLineBreaksRegex;
6656                      collapseSpaces = true;
6657                  } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
6658                      spaceRegex = spacesRegex;
6659                      collapseSpaces = true;
6660                  }
6661  
6662                  return {
6663                      node: textNode,
6664                      text: textNode.data,
6665                      spaceRegex: spaceRegex,
6666                      collapseSpaces: collapseSpaces,
6667                      preLine: preLine
6668                  };
6669              }, "node"),
6670  
6671              hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
6672                  var session = this.session;
6673                  var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
6674                  var firstPosInEl = session.getPosition(el, 0);
6675  
6676                  var pos = backward ? posAfterEl : firstPosInEl;
6677                  var endPos = backward ? firstPosInEl : posAfterEl;
6678  
6679                  /*
6680                   <body><p>X  </p><p>Y</p></body>
6681  
6682                   Positions:
6683  
6684                   body:0:""
6685                   p:0:""
6686                   text:0:""
6687                   text:1:"X"
6688                   text:2:TRAILING_SPACE_IN_BLOCK
6689                   text:3:COLLAPSED_SPACE
6690                   p:1:""
6691                   body:1:"\n"
6692                   p:0:""
6693                   text:0:""
6694                   text:1:"Y"
6695  
6696                   A character is a TRAILING_SPACE_IN_BLOCK iff:
6697  
6698                   - There is no uncollapsed character after it within the visible containing block element
6699  
6700                   A character is a TRAILING_SPACE_BEFORE_BR iff:
6701  
6702                   - There is no uncollapsed character after it preceding a <br> element
6703  
6704                   An element has inner text iff
6705  
6706                   - It is not hidden
6707                   - It contains an uncollapsed character
6708  
6709                   All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
6710                   */
6711  
6712                  while (pos !== endPos) {
6713                      pos.prepopulateChar();
6714                      if (pos.isDefinitelyNonEmpty()) {
6715                          return true;
6716                      }
6717                      pos = backward ? pos.previousVisible() : pos.nextVisible();
6718                  }
6719  
6720                  return false;
6721              }, "node"),
6722  
6723              isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
6724                  // Ensure that a block element containing a <br> is considered to have inner text
6725                  var brs = el.getElementsByTagName("br");
6726                  for (var i = 0, len = brs.length; i < len; ++i) {
6727                      if (!isCollapsedNode(brs[i])) {
6728                          return true;
6729                      }
6730                  }
6731                  return this.hasInnerText();
6732              }, "node"),
6733  
6734              getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
6735                  if (el.tagName.toLowerCase() == "br") {
6736                      return "";
6737                  } else {
6738                      switch (this.getComputedDisplay()) {
6739                          case "inline":
6740                              var child = el.lastChild;
6741                              while (child) {
6742                                  if (!isIgnoredNode(child)) {
6743                                      return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
6744                                  }
6745                                  child = child.previousSibling;
6746                              }
6747                              break;
6748                          case "inline-block":
6749                          case "inline-table":
6750                          case "none":
6751                          case "table-column":
6752                          case "table-column-group":
6753                              break;
6754                          case "table-cell":
6755                              return "\t";
6756                          default:
6757                              return this.isRenderedBlock(true) ? "\n" : "";
6758                      }
6759                  }
6760                  return "";
6761              }, "node"),
6762  
6763              getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
6764                  switch (this.getComputedDisplay()) {
6765                      case "inline":
6766                      case "inline-block":
6767                      case "inline-table":
6768                      case "none":
6769                      case "table-column":
6770                      case "table-column-group":
6771                      case "table-cell":
6772                          break;
6773                      default:
6774                          return this.isRenderedBlock(false) ? "\n" : "";
6775                  }
6776                  return "";
6777              }, "node")
6778          });
6779  
6780          /*----------------------------------------------------------------------------------------------------------------*/
6781  
6782          function Position(nodeWrapper, offset) {
6783              this.offset = offset;
6784              this.nodeWrapper = nodeWrapper;
6785              this.node = nodeWrapper.node;
6786              this.session = nodeWrapper.session;
6787              this.cache = new Cache();
6788          }
6789  
6790          function inspectPosition() {
6791              return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
6792          }
6793  
6794          var positionProto = {
6795              character: "",
6796              characterType: EMPTY,
6797              isBr: false,
6798  
6799              /*
6800              This method:
6801              - Fully populates positions that have characters that can be determined independently of any other characters.
6802              - Populates most types of space positions with a provisional character. The character is finalized later.
6803               */
6804              prepopulateChar: function() {
6805                  var pos = this;
6806                  if (!pos.prepopulatedChar) {
6807                      var node = pos.node, offset = pos.offset;
6808                      var visibleChar = "", charType = EMPTY;
6809                      var finalizedChar = false;
6810                      if (offset > 0) {
6811                          if (node.nodeType == 3) {
6812                              var text = node.data;
6813                              var textChar = text.charAt(offset - 1);
6814  
6815                              var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
6816                              var spaceRegex = nodeInfo.spaceRegex;
6817                              if (nodeInfo.collapseSpaces) {
6818                                  if (spaceRegex.test(textChar)) {
6819                                      // "If the character at position is from set, append a single space (U+0020) to newdata and advance
6820                                      // position until the character at position is not from set."
6821  
6822                                      // We also need to check for the case where we're in a pre-line and we have a space preceding a
6823                                      // line break, because such spaces are collapsed in some browsers
6824                                      if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
6825                                      } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
6826                                          visibleChar = " ";
6827                                          charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
6828                                      } else {
6829                                          visibleChar = " ";
6830                                          //pos.checkForFollowingLineBreak = true;
6831                                          charType = COLLAPSIBLE_SPACE;
6832                                      }
6833                                  } else {
6834                                      visibleChar = textChar;
6835                                      charType = NON_SPACE;
6836                                      finalizedChar = true;
6837                                  }
6838                              } else {
6839                                  visibleChar = textChar;
6840                                  charType = UNCOLLAPSIBLE_SPACE;
6841                                  finalizedChar = true;
6842                              }
6843                          } else {
6844                              var nodePassed = node.childNodes[offset - 1];
6845                              if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
6846                                  if (nodePassed.tagName.toLowerCase() == "br") {
6847                                      visibleChar = "\n";
6848                                      pos.isBr = true;
6849                                      charType = COLLAPSIBLE_SPACE;
6850                                      finalizedChar = false;
6851                                  } else {
6852                                      pos.checkForTrailingSpace = true;
6853                                  }
6854                              }
6855  
6856                              // Check the leading space of the next node for the case when a block element follows an inline
6857                              // element or text node. In that case, there is an implied line break between the two nodes.
6858                              if (!visibleChar) {
6859                                  var nextNode = node.childNodes[offset];
6860                                  if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
6861                                      pos.checkForLeadingSpace = true;
6862                                  }
6863                              }
6864                          }
6865                      }
6866  
6867                      pos.prepopulatedChar = true;
6868                      pos.character = visibleChar;
6869                      pos.characterType = charType;
6870                      pos.isCharInvariant = finalizedChar;
6871                  }
6872              },
6873  
6874              isDefinitelyNonEmpty: function() {
6875                  var charType = this.characterType;
6876                  return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
6877              },
6878  
6879              // Resolve leading and trailing spaces, which may involve prepopulating other positions
6880              resolveLeadingAndTrailingSpaces: function() {
6881                  if (!this.prepopulatedChar) {
6882                      this.prepopulateChar();
6883                  }
6884                  if (this.checkForTrailingSpace) {
6885                      var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
6886                      if (trailingSpace) {
6887                          this.isTrailingSpace = true;
6888                          this.character = trailingSpace;
6889                          this.characterType = COLLAPSIBLE_SPACE;
6890                      }
6891                      this.checkForTrailingSpace = false;
6892                  }
6893                  if (this.checkForLeadingSpace) {
6894                      var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
6895                      if (leadingSpace) {
6896                          this.isLeadingSpace = true;
6897                          this.character = leadingSpace;
6898                          this.characterType = COLLAPSIBLE_SPACE;
6899                      }
6900                      this.checkForLeadingSpace = false;
6901                  }
6902              },
6903  
6904              getPrecedingUncollapsedPosition: function(characterOptions) {
6905                  var pos = this, character;
6906                  while ( (pos = pos.previousVisible()) ) {
6907                      character = pos.getCharacter(characterOptions);
6908                      if (character !== "") {
6909                          return pos;
6910                      }
6911                  }
6912  
6913                  return null;
6914              },
6915  
6916              getCharacter: function(characterOptions) {
6917                  this.resolveLeadingAndTrailingSpaces();
6918  
6919                  var thisChar = this.character, returnChar;
6920  
6921                  // Check if character is ignored
6922                  var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
6923                  var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
6924  
6925                  // Check if this position's  character is invariant (i.e. not dependent on character options) and return it
6926                  // if so
6927                  if (this.isCharInvariant) {
6928                      returnChar = isIgnoredCharacter ? "" : thisChar;
6929                      return returnChar;
6930                  }
6931  
6932                  var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
6933                  var cachedChar = this.cache.get(cacheKey);
6934                  if (cachedChar !== null) {
6935                      return cachedChar;
6936                  }
6937  
6938                  // We need to actually get the character now
6939                  var character = "";
6940                  var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
6941  
6942                  var nextPos, previousPos;
6943                  var gotPreviousPos = false;
6944                  var pos = this;
6945  
6946                  function getPreviousPos() {
6947                      if (!gotPreviousPos) {
6948                          previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
6949                          gotPreviousPos = true;
6950                      }
6951                      return previousPos;
6952                  }
6953  
6954                  // Disallow a collapsible space that is followed by a line break or is the last character
6955                  if (collapsible) {
6956                      // Allow a trailing space that we've previously determined should be included
6957                      if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
6958                          character = "\n";
6959                      }
6960                      // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
6961                      // or follows a collapsible included space
6962                      else if (thisChar == " " &&
6963                              (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
6964                      }
6965                      // Allow a leading line break unless it follows a line break
6966                      else if (thisChar == "\n" && this.isLeadingSpace) {
6967                          if (getPreviousPos() && previousPos.character != "\n") {
6968                              character = "\n";
6969                          } else {
6970                          }
6971                      } else {
6972                          nextPos = this.nextUncollapsed();
6973                          if (nextPos) {
6974                              if (nextPos.isBr) {
6975                                  this.type = TRAILING_SPACE_BEFORE_BR;
6976                              } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
6977                                  this.type = TRAILING_SPACE_IN_BLOCK;
6978                              } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
6979                                  this.type = TRAILING_SPACE_BEFORE_BLOCK;
6980                              }
6981  
6982                              if (nextPos.character == "\n") {
6983                                  if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
6984                                  } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
6985                                  } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
6986                                  } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
6987                                  } else if (thisChar == "\n") {
6988                                      if (nextPos.isTrailingSpace) {
6989                                          if (this.isTrailingSpace) {
6990                                          } else if (this.isBr) {
6991                                              nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
6992  
6993                                              if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
6994                                                  nextPos.character = "";
6995                                              } else {
6996                                                  nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
6997                                              }
6998                                          }
6999                                      } else {
7000                                          character = "\n";
7001                                      }
7002                                  } else if (thisChar == " ") {
7003                                      character = " ";
7004                                  } else {
7005                                  }
7006                              } else {
7007                                  character = thisChar;
7008                              }
7009                          } else {
7010                          }
7011                      }
7012                  }
7013  
7014                  if (ignoredChars.indexOf(character) > -1) {
7015                      character = "";
7016                  }
7017  
7018  
7019                  this.cache.set(cacheKey, character);
7020  
7021                  return character;
7022              },
7023  
7024              equals: function(pos) {
7025                  return !!pos && this.node === pos.node && this.offset === pos.offset;
7026              },
7027  
7028              inspect: inspectPosition,
7029  
7030              toString: function() {
7031                  return this.character;
7032              }
7033          };
7034  
7035          Position.prototype = positionProto;
7036  
7037          extend(positionProto, {
7038              next: createCachingGetter("nextPos", function(pos) {
7039                  var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
7040                  if (!node) {
7041                      return null;
7042                  }
7043                  var nextNode, nextOffset, child;
7044                  if (offset == nodeWrapper.getLength()) {
7045                      // Move onto the next node
7046                      nextNode = node.parentNode;
7047                      nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
7048                  } else {
7049                      if (nodeWrapper.isCharacterDataNode()) {
7050                          nextNode = node;
7051                          nextOffset = offset + 1;
7052                      } else {
7053                          child = node.childNodes[offset];
7054                          // Go into the children next, if children there are
7055                          if (session.getNodeWrapper(child).containsPositions()) {
7056                              nextNode = child;
7057                              nextOffset = 0;
7058                          } else {
7059                              nextNode = node;
7060                              nextOffset = offset + 1;
7061                          }
7062                      }
7063                  }
7064  
7065                  return nextNode ? session.getPosition(nextNode, nextOffset) : null;
7066              }),
7067  
7068              previous: createCachingGetter("previous", function(pos) {
7069                  var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
7070                  var previousNode, previousOffset, child;
7071                  if (offset == 0) {
7072                      previousNode = node.parentNode;
7073                      previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
7074                  } else {
7075                      if (nodeWrapper.isCharacterDataNode()) {
7076                          previousNode = node;
7077                          previousOffset = offset - 1;
7078                      } else {
7079                          child = node.childNodes[offset - 1];
7080                          // Go into the children next, if children there are
7081                          if (session.getNodeWrapper(child).containsPositions()) {
7082                              previousNode = child;
7083                              previousOffset = dom.getNodeLength(child);
7084                          } else {
7085                              previousNode = node;
7086                              previousOffset = offset - 1;
7087                          }
7088                      }
7089                  }
7090                  return previousNode ? session.getPosition(previousNode, previousOffset) : null;
7091              }),
7092  
7093              /*
7094               Next and previous position moving functions that filter out
7095  
7096               - Hidden (CSS visibility/display) elements
7097               - Script and style elements
7098               */
7099              nextVisible: createCachingGetter("nextVisible", function(pos) {
7100                  var next = pos.next();
7101                  if (!next) {
7102                      return null;
7103                  }
7104                  var nodeWrapper = next.nodeWrapper, node = next.node;
7105                  var newPos = next;
7106                  if (nodeWrapper.isCollapsed()) {
7107                      // We're skipping this node and all its descendants
7108                      newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
7109                  }
7110                  return newPos;
7111              }),
7112  
7113              nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
7114                  var nextPos = pos;
7115                  while ( (nextPos = nextPos.nextVisible()) ) {
7116                      nextPos.resolveLeadingAndTrailingSpaces();
7117                      if (nextPos.character !== "") {
7118                          return nextPos;
7119                      }
7120                  }
7121                  return null;
7122              }),
7123  
7124              previousVisible: createCachingGetter("previousVisible", function(pos) {
7125                  var previous = pos.previous();
7126                  if (!previous) {
7127                      return null;
7128                  }
7129                  var nodeWrapper = previous.nodeWrapper, node = previous.node;
7130                  var newPos = previous;
7131                  if (nodeWrapper.isCollapsed()) {
7132                      // We're skipping this node and all its descendants
7133                      newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
7134                  }
7135                  return newPos;
7136              })
7137          });
7138  
7139          /*----------------------------------------------------------------------------------------------------------------*/
7140  
7141          var currentSession = null;
7142  
7143          var Session = (function() {
7144              function createWrapperCache(nodeProperty) {
7145                  var cache = new Cache();
7146  
7147                  return {
7148                      get: function(node) {
7149                          var wrappersByProperty = cache.get(node[nodeProperty]);
7150                          if (wrappersByProperty) {
7151                              for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
7152                                  if (wrapper.node === node) {
7153                                      return wrapper;
7154                                  }
7155                              }
7156                          }
7157                          return null;
7158                      },
7159  
7160                      set: function(nodeWrapper) {
7161                          var property = nodeWrapper.node[nodeProperty];
7162                          var wrappersByProperty = cache.get(property) || cache.set(property, []);
7163                          wrappersByProperty.push(nodeWrapper);
7164                      }
7165                  };
7166              }
7167  
7168              var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
7169  
7170              function Session() {
7171                  this.initCaches();
7172              }
7173  
7174              Session.prototype = {
7175                  initCaches: function() {
7176                      this.elementCache = uniqueIDSupported ? (function() {
7177                          var elementsCache = new Cache();
7178  
7179                          return {
7180                              get: function(el) {
7181                                  return elementsCache.get(el.uniqueID);
7182                              },
7183  
7184                              set: function(elWrapper) {
7185                                  elementsCache.set(elWrapper.node.uniqueID, elWrapper);
7186                              }
7187                          };
7188                      })() : createWrapperCache("tagName");
7189  
7190                      // Store text nodes keyed by data, although we may need to truncate this
7191                      this.textNodeCache = createWrapperCache("data");
7192                      this.otherNodeCache = createWrapperCache("nodeName");
7193                  },
7194  
7195                  getNodeWrapper: function(node) {
7196                      var wrapperCache;
7197                      switch (node.nodeType) {
7198                          case 1:
7199                              wrapperCache = this.elementCache;
7200                              break;
7201                          case 3:
7202                              wrapperCache = this.textNodeCache;
7203                              break;
7204                          default:
7205                              wrapperCache = this.otherNodeCache;
7206                              break;
7207                      }
7208  
7209                      var wrapper = wrapperCache.get(node);
7210                      if (!wrapper) {
7211                          wrapper = new NodeWrapper(node, this);
7212                          wrapperCache.set(wrapper);
7213                      }
7214                      return wrapper;
7215                  },
7216  
7217                  getPosition: function(node, offset) {
7218                      return this.getNodeWrapper(node).getPosition(offset);
7219                  },
7220  
7221                  getRangeBoundaryPosition: function(range, isStart) {
7222                      var prefix = isStart ? "start" : "end";
7223                      return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
7224                  },
7225  
7226                  detach: function() {
7227                      this.elementCache = this.textNodeCache = this.otherNodeCache = null;
7228                  }
7229              };
7230  
7231              return Session;
7232          })();
7233  
7234          /*----------------------------------------------------------------------------------------------------------------*/
7235  
7236          function startSession() {
7237              endSession();
7238              return (currentSession = new Session());
7239          }
7240  
7241          function getSession() {
7242              return currentSession || startSession();
7243          }
7244  
7245          function endSession() {
7246              if (currentSession) {
7247                  currentSession.detach();
7248              }
7249              currentSession = null;
7250          }
7251  
7252          /*----------------------------------------------------------------------------------------------------------------*/
7253  
7254          // Extensions to the rangy.dom utility object
7255  
7256          extend(dom, {
7257              nextNode: nextNode,
7258              previousNode: previousNode
7259          });
7260  
7261          /*----------------------------------------------------------------------------------------------------------------*/
7262  
7263          function createCharacterIterator(startPos, backward, endPos, characterOptions) {
7264  
7265              // Adjust the end position to ensure that it is actually reached
7266              if (endPos) {
7267                  if (backward) {
7268                      if (isCollapsedNode(endPos.node)) {
7269                          endPos = startPos.previousVisible();
7270                      }
7271                  } else {
7272                      if (isCollapsedNode(endPos.node)) {
7273                          endPos = endPos.nextVisible();
7274                      }
7275                  }
7276              }
7277  
7278              var pos = startPos, finished = false;
7279  
7280              function next() {
7281                  var charPos = null;
7282                  if (backward) {
7283                      charPos = pos;
7284                      if (!finished) {
7285                          pos = pos.previousVisible();
7286                          finished = !pos || (endPos && pos.equals(endPos));
7287                      }
7288                  } else {
7289                      if (!finished) {
7290                          charPos = pos = pos.nextVisible();
7291                          finished = !pos || (endPos && pos.equals(endPos));
7292                      }
7293                  }
7294                  if (finished) {
7295                      pos = null;
7296                  }
7297                  return charPos;
7298              }
7299  
7300              var previousTextPos, returnPreviousTextPos = false;
7301  
7302              return {
7303                  next: function() {
7304                      if (returnPreviousTextPos) {
7305                          returnPreviousTextPos = false;
7306                          return previousTextPos;
7307                      } else {
7308                          var pos, character;
7309                          while ( (pos = next()) ) {
7310                              character = pos.getCharacter(characterOptions);
7311                              if (character) {
7312                                  previousTextPos = pos;
7313                                  return pos;
7314                              }
7315                          }
7316                          return null;
7317                      }
7318                  },
7319  
7320                  rewind: function() {
7321                      if (previousTextPos) {
7322                          returnPreviousTextPos = true;
7323                      } else {
7324                          throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
7325                      }
7326                  },
7327  
7328                  dispose: function() {
7329                      startPos = endPos = null;
7330                  }
7331              };
7332          }
7333  
7334          var arrayIndexOf = Array.prototype.indexOf ?
7335              function(arr, val) {
7336                  return arr.indexOf(val);
7337              } :
7338              function(arr, val) {
7339                  for (var i = 0, len = arr.length; i < len; ++i) {
7340                      if (arr[i] === val) {
7341                          return i;
7342                      }
7343                  }
7344                  return -1;
7345              };
7346  
7347          // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
7348          // is called and there is no more tokenized text
7349          function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
7350              var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
7351              var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
7352              var tokenizer = wordOptions.tokenizer;
7353  
7354              // Consumes a word and the whitespace beyond it
7355              function consumeWord(forward) {
7356                  var pos, textChar;
7357                  var newChars = [], it = forward ? forwardIterator : backwardIterator;
7358  
7359                  var passedWordBoundary = false, insideWord = false;
7360  
7361                  while ( (pos = it.next()) ) {
7362                      textChar = pos.character;
7363  
7364  
7365                      if (allWhiteSpaceRegex.test(textChar)) {
7366                          if (insideWord) {
7367                              insideWord = false;
7368                              passedWordBoundary = true;
7369                          }
7370                      } else {
7371                          if (passedWordBoundary) {
7372                              it.rewind();
7373                              break;
7374                          } else {
7375                              insideWord = true;
7376                          }
7377                      }
7378                      newChars.push(pos);
7379                  }
7380  
7381  
7382                  return newChars;
7383              }
7384  
7385              // Get initial word surrounding initial position and tokenize it
7386              var forwardChars = consumeWord(true);
7387              var backwardChars = consumeWord(false).reverse();
7388              var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
7389  
7390              // Create initial token buffers
7391              var forwardTokensBuffer = forwardChars.length ?
7392                  tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
7393  
7394              var backwardTokensBuffer = backwardChars.length ?
7395                  tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
7396  
7397              function inspectBuffer(buffer) {
7398                  var textPositions = ["[" + buffer.length + "]"];
7399                  for (var i = 0; i < buffer.length; ++i) {
7400                      textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
7401                  }
7402                  return textPositions;
7403              }
7404  
7405  
7406              return {
7407                  nextEndToken: function() {
7408                      var lastToken, forwardChars;
7409  
7410                      // If we're down to the last token, consume character chunks until we have a word or run out of
7411                      // characters to consume
7412                      while ( forwardTokensBuffer.length == 1 &&
7413                          !(lastToken = forwardTokensBuffer[0]).isWord &&
7414                          (forwardChars = consumeWord(true)).length > 0) {
7415  
7416                          // Merge trailing non-word into next word and tokenize
7417                          forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
7418                      }
7419  
7420                      return forwardTokensBuffer.shift();
7421                  },
7422  
7423                  previousStartToken: function() {
7424                      var lastToken, backwardChars;
7425  
7426                      // If we're down to the last token, consume character chunks until we have a word or run out of
7427                      // characters to consume
7428                      while ( backwardTokensBuffer.length == 1 &&
7429                          !(lastToken = backwardTokensBuffer[0]).isWord &&
7430                          (backwardChars = consumeWord(false)).length > 0) {
7431  
7432                          // Merge leading non-word into next word and tokenize
7433                          backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
7434                      }
7435  
7436                      return backwardTokensBuffer.pop();
7437                  },
7438  
7439                  dispose: function() {
7440                      forwardIterator.dispose();
7441                      backwardIterator.dispose();
7442                      forwardTokensBuffer = backwardTokensBuffer = null;
7443                  }
7444              };
7445          }
7446  
7447          function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
7448              var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
7449              if (count !== 0) {
7450                  var backward = (count < 0);
7451  
7452                  switch (unit) {
7453                      case CHARACTER:
7454                          charIterator = createCharacterIterator(pos, backward, null, characterOptions);
7455                          while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
7456                              ++unitsMoved;
7457                              newPos = currentPos;
7458                          }
7459                          nextPos = currentPos;
7460                          charIterator.dispose();
7461                          break;
7462                      case WORD:
7463                          var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
7464                          var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
7465  
7466                          while ( (token = next()) && unitsMoved < absCount ) {
7467                              if (token.isWord) {
7468                                  ++unitsMoved;
7469                                  newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
7470                              }
7471                          }
7472                          break;
7473                      default:
7474                          throw new Error("movePositionBy: unit '" + unit + "' not implemented");
7475                  }
7476  
7477                  // Perform any necessary position tweaks
7478                  if (backward) {
7479                      newPos = newPos.previousVisible();
7480                      unitsMoved = -unitsMoved;
7481                  } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
7482                      // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
7483                      // before a block element (for example, the line break between "1" and "2" in the following HTML:
7484                      // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
7485                      // corresponds with a different selection position in most browsers from the one we want (i.e. at the
7486                      // start of the contents of the block element). We get round this by advancing the position returned to
7487                      // the last possible equivalent visible position.
7488                      if (unit == WORD) {
7489                          charIterator = createCharacterIterator(pos, false, null, characterOptions);
7490                          nextPos = charIterator.next();
7491                          charIterator.dispose();
7492                      }
7493                      if (nextPos) {
7494                          newPos = nextPos.previousVisible();
7495                      }
7496                  }
7497              }
7498  
7499  
7500              return {
7501                  position: newPos,
7502                  unitsMoved: unitsMoved
7503              };
7504          }
7505  
7506          function createRangeCharacterIterator(session, range, characterOptions, backward) {
7507              var rangeStart = session.getRangeBoundaryPosition(range, true);
7508              var rangeEnd = session.getRangeBoundaryPosition(range, false);
7509              var itStart = backward ? rangeEnd : rangeStart;
7510              var itEnd = backward ? rangeStart : rangeEnd;
7511  
7512              return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
7513          }
7514  
7515          function getRangeCharacters(session, range, characterOptions) {
7516  
7517              var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
7518              while ( (pos = it.next()) ) {
7519                  chars.push(pos);
7520              }
7521  
7522              it.dispose();
7523              return chars;
7524          }
7525  
7526          function isWholeWord(startPos, endPos, wordOptions) {
7527              var range = api.createRange(startPos.node);
7528              range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
7529              return !range.expand("word", { wordOptions: wordOptions });
7530          }
7531  
7532          function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
7533              var backward = isDirectionBackward(findOptions.direction);
7534              var it = createCharacterIterator(
7535                  initialPos,
7536                  backward,
7537                  initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
7538                  findOptions.characterOptions
7539              );
7540              var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
7541              var result, insideRegexMatch;
7542              var returnValue = null;
7543  
7544              function handleMatch(startIndex, endIndex) {
7545                  var startPos = chars[startIndex].previousVisible();
7546                  var endPos = chars[endIndex - 1];
7547                  var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
7548  
7549                  return {
7550                      startPos: startPos,
7551                      endPos: endPos,
7552                      valid: valid
7553                  };
7554              }
7555  
7556              while ( (pos = it.next()) ) {
7557                  currentChar = pos.character;
7558                  if (!isRegex && !findOptions.caseSensitive) {
7559                      currentChar = currentChar.toLowerCase();
7560                  }
7561  
7562                  if (backward) {
7563                      chars.unshift(pos);
7564                      text = currentChar + text;
7565                  } else {
7566                      chars.push(pos);
7567                      text += currentChar;
7568                  }
7569  
7570                  if (isRegex) {
7571                      result = searchTerm.exec(text);
7572                      if (result) {
7573                          matchStartIndex = result.index;
7574                          matchEndIndex = matchStartIndex + result[0].length;
7575                          if (insideRegexMatch) {
7576                              // Check whether the match is now over
7577                              if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
7578                                  returnValue = handleMatch(matchStartIndex, matchEndIndex);
7579                                  break;
7580                              }
7581                          } else {
7582                              insideRegexMatch = true;
7583                          }
7584                      }
7585                  } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
7586                      returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
7587                      break;
7588                  }
7589              }
7590  
7591              // Check whether regex match extends to the end of the range
7592              if (insideRegexMatch) {
7593                  returnValue = handleMatch(matchStartIndex, matchEndIndex);
7594              }
7595              it.dispose();
7596  
7597              return returnValue;
7598          }
7599  
7600          function createEntryPointFunction(func) {
7601              return function() {
7602                  var sessionRunning = !!currentSession;
7603                  var session = getSession();
7604                  var args = [session].concat( util.toArray(arguments) );
7605                  var returnValue = func.apply(this, args);
7606                  if (!sessionRunning) {
7607                      endSession();
7608                  }
7609                  return returnValue;
7610              };
7611          }
7612  
7613          /*----------------------------------------------------------------------------------------------------------------*/
7614  
7615          // Extensions to the Rangy Range object
7616  
7617          function createRangeBoundaryMover(isStart, collapse) {
7618              /*
7619               Unit can be "character" or "word"
7620               Options:
7621  
7622               - includeTrailingSpace
7623               - wordRegex
7624               - tokenizer
7625               - collapseSpaceBeforeLineBreak
7626               */
7627              return createEntryPointFunction(
7628                  function(session, unit, count, moveOptions) {
7629                      if (typeof count == UNDEF) {
7630                          count = unit;
7631                          unit = CHARACTER;
7632                      }
7633                      moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
7634  
7635                      var boundaryIsStart = isStart;
7636                      if (collapse) {
7637                          boundaryIsStart = (count >= 0);
7638                          this.collapse(!boundaryIsStart);
7639                      }
7640                      var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
7641                      var newPos = moveResult.position;
7642                      this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
7643                      return moveResult.unitsMoved;
7644                  }
7645              );
7646          }
7647  
7648          function createRangeTrimmer(isStart) {
7649              return createEntryPointFunction(
7650                  function(session, characterOptions) {
7651                      characterOptions = createOptions(characterOptions, defaultCharacterOptions);
7652                      var pos;
7653                      var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
7654                      var trimCharCount = 0;
7655                      while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
7656                          ++trimCharCount;
7657                      }
7658                      it.dispose();
7659                      var trimmed = (trimCharCount > 0);
7660                      if (trimmed) {
7661                          this[isStart ? "moveStart" : "moveEnd"](
7662                              "character",
7663                              isStart ? trimCharCount : -trimCharCount,
7664                              { characterOptions: characterOptions }
7665                          );
7666                      }
7667                      return trimmed;
7668                  }
7669              );
7670          }
7671  
7672          extend(api.rangePrototype, {
7673              moveStart: createRangeBoundaryMover(true, false),
7674  
7675              moveEnd: createRangeBoundaryMover(false, false),
7676  
7677              move: createRangeBoundaryMover(true, true),
7678  
7679              trimStart: createRangeTrimmer(true),
7680  
7681              trimEnd: createRangeTrimmer(false),
7682  
7683              trim: createEntryPointFunction(
7684                  function(session, characterOptions) {
7685                      var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
7686                      return startTrimmed || endTrimmed;
7687                  }
7688              ),
7689  
7690              expand: createEntryPointFunction(
7691                  function(session, unit, expandOptions) {
7692                      var moved = false;
7693                      expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
7694                      var characterOptions = expandOptions.characterOptions;
7695                      if (!unit) {
7696                          unit = CHARACTER;
7697                      }
7698                      if (unit == WORD) {
7699                          var wordOptions = expandOptions.wordOptions;
7700                          var startPos = session.getRangeBoundaryPosition(this, true);
7701                          var endPos = session.getRangeBoundaryPosition(this, false);
7702  
7703                          var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
7704                          var startToken = startTokenizedTextProvider.nextEndToken();
7705                          var newStartPos = startToken.chars[0].previousVisible();
7706                          var endToken, newEndPos;
7707  
7708                          if (this.collapsed) {
7709                              endToken = startToken;
7710                          } else {
7711                              var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
7712                              endToken = endTokenizedTextProvider.previousStartToken();
7713                          }
7714                          newEndPos = endToken.chars[endToken.chars.length - 1];
7715  
7716                          if (!newStartPos.equals(startPos)) {
7717                              this.setStart(newStartPos.node, newStartPos.offset);
7718                              moved = true;
7719                          }
7720                          if (newEndPos && !newEndPos.equals(endPos)) {
7721                              this.setEnd(newEndPos.node, newEndPos.offset);
7722                              moved = true;
7723                          }
7724  
7725                          if (expandOptions.trim) {
7726                              if (expandOptions.trimStart) {
7727                                  moved = this.trimStart(characterOptions) || moved;
7728                              }
7729                              if (expandOptions.trimEnd) {
7730                                  moved = this.trimEnd(characterOptions) || moved;
7731                              }
7732                          }
7733  
7734                          return moved;
7735                      } else {
7736                          return this.moveEnd(CHARACTER, 1, expandOptions);
7737                      }
7738                  }
7739              ),
7740  
7741              text: createEntryPointFunction(
7742                  function(session, characterOptions) {
7743                      return this.collapsed ?
7744                          "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
7745                  }
7746              ),
7747  
7748              selectCharacters: createEntryPointFunction(
7749                  function(session, containerNode, startIndex, endIndex, characterOptions) {
7750                      var moveOptions = { characterOptions: characterOptions };
7751                      if (!containerNode) {
7752                          containerNode = getBody( this.getDocument() );
7753                      }
7754                      this.selectNodeContents(containerNode);
7755                      this.collapse(true);
7756                      this.moveStart("character", startIndex, moveOptions);
7757                      this.collapse(true);
7758                      this.moveEnd("character", endIndex - startIndex, moveOptions);
7759                  }
7760              ),
7761  
7762              // Character indexes are relative to the start of node
7763              toCharacterRange: createEntryPointFunction(
7764                  function(session, containerNode, characterOptions) {
7765                      if (!containerNode) {
7766                          containerNode = getBody( this.getDocument() );
7767                      }
7768                      var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
7769                      var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
7770                      var rangeBetween = this.cloneRange();
7771                      var startIndex, endIndex;
7772                      if (rangeStartsBeforeNode) {
7773                          rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
7774                          startIndex = -rangeBetween.text(characterOptions).length;
7775                      } else {
7776                          rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
7777                          startIndex = rangeBetween.text(characterOptions).length;
7778                      }
7779                      endIndex = startIndex + this.text(characterOptions).length;
7780  
7781                      return {
7782                          start: startIndex,
7783                          end: endIndex
7784                      };
7785                  }
7786              ),
7787  
7788              findText: createEntryPointFunction(
7789                  function(session, searchTermParam, findOptions) {
7790                      // Set up options
7791                      findOptions = createNestedOptions(findOptions, defaultFindOptions);
7792  
7793                      // Create word options if we're matching whole words only
7794                      if (findOptions.wholeWordsOnly) {
7795                          // We don't ever want trailing spaces for search results
7796                          findOptions.wordOptions.includeTrailingSpace = false;
7797                      }
7798  
7799                      var backward = isDirectionBackward(findOptions.direction);
7800  
7801                      // Create a range representing the search scope if none was provided
7802                      var searchScopeRange = findOptions.withinRange;
7803                      if (!searchScopeRange) {
7804                          searchScopeRange = api.createRange();
7805                          searchScopeRange.selectNodeContents(this.getDocument());
7806                      }
7807  
7808                      // Examine and prepare the search term
7809                      var searchTerm = searchTermParam, isRegex = false;
7810                      if (typeof searchTerm == "string") {
7811                          if (!findOptions.caseSensitive) {
7812                              searchTerm = searchTerm.toLowerCase();
7813                          }
7814                      } else {
7815                          isRegex = true;
7816                      }
7817  
7818                      var initialPos = session.getRangeBoundaryPosition(this, !backward);
7819  
7820                      // Adjust initial position if it lies outside the search scope
7821                      var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
7822  
7823                      if (comparison === -1) {
7824                          initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
7825                      } else if (comparison === 1) {
7826                          initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
7827                      }
7828  
7829                      var pos = initialPos;
7830                      var wrappedAround = false;
7831  
7832                      // Try to find a match and ignore invalid ones
7833                      var findResult;
7834                      while (true) {
7835                          findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
7836  
7837                          if (findResult) {
7838                              if (findResult.valid) {
7839                                  this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
7840                                  return true;
7841                              } else {
7842                                  // We've found a match that is not a whole word, so we carry on searching from the point immediately
7843                                  // after the match
7844                                  pos = backward ? findResult.startPos : findResult.endPos;
7845                              }
7846                          } else if (findOptions.wrap && !wrappedAround) {
7847                              // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
7848                              searchScopeRange = searchScopeRange.cloneRange();
7849                              pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
7850                              searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
7851                              wrappedAround = true;
7852                          } else {
7853                              // Nothing found and we can't wrap around, so we're done
7854                              return false;
7855                          }
7856                      }
7857                  }
7858              ),
7859  
7860              pasteHtml: function(html) {
7861                  this.deleteContents();
7862                  if (html) {
7863                      var frag = this.createContextualFragment(html);
7864                      var lastChild = frag.lastChild;
7865                      this.insertNode(frag);
7866                      this.collapseAfter(lastChild);
7867                  }
7868              }
7869          });
7870  
7871          /*----------------------------------------------------------------------------------------------------------------*/
7872  
7873          // Extensions to the Rangy Selection object
7874  
7875          function createSelectionTrimmer(methodName) {
7876              return createEntryPointFunction(
7877                  function(session, characterOptions) {
7878                      var trimmed = false;
7879                      this.changeEachRange(function(range) {
7880                          trimmed = range[methodName](characterOptions) || trimmed;
7881                      });
7882                      return trimmed;
7883                  }
7884              );
7885          }
7886  
7887          extend(api.selectionPrototype, {
7888              expand: createEntryPointFunction(
7889                  function(session, unit, expandOptions) {
7890                      this.changeEachRange(function(range) {
7891                          range.expand(unit, expandOptions);
7892                      });
7893                  }
7894              ),
7895  
7896              move: createEntryPointFunction(
7897                  function(session, unit, count, options) {
7898                      var unitsMoved = 0;
7899                      if (this.focusNode) {
7900                          this.collapse(this.focusNode, this.focusOffset);
7901                          var range = this.getRangeAt(0);
7902                          if (!options) {
7903                              options = {};
7904                          }
7905                          options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
7906                          unitsMoved = range.move(unit, count, options);
7907                          this.setSingleRange(range);
7908                      }
7909                      return unitsMoved;
7910                  }
7911              ),
7912  
7913              trimStart: createSelectionTrimmer("trimStart"),
7914              trimEnd: createSelectionTrimmer("trimEnd"),
7915              trim: createSelectionTrimmer("trim"),
7916  
7917              selectCharacters: createEntryPointFunction(
7918                  function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
7919                      var range = api.createRange(containerNode);
7920                      range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
7921                      this.setSingleRange(range, direction);
7922                  }
7923              ),
7924  
7925              saveCharacterRanges: createEntryPointFunction(
7926                  function(session, containerNode, characterOptions) {
7927                      var ranges = this.getAllRanges(), rangeCount = ranges.length;
7928                      var rangeInfos = [];
7929  
7930                      var backward = rangeCount == 1 && this.isBackward();
7931  
7932                      for (var i = 0, len = ranges.length; i < len; ++i) {
7933                          rangeInfos[i] = {
7934                              characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
7935                              backward: backward,
7936                              characterOptions: characterOptions
7937                          };
7938                      }
7939  
7940                      return rangeInfos;
7941                  }
7942              ),
7943  
7944              restoreCharacterRanges: createEntryPointFunction(
7945                  function(session, containerNode, saved) {
7946                      this.removeAllRanges();
7947                      for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
7948                          rangeInfo = saved[i];
7949                          characterRange = rangeInfo.characterRange;
7950                          range = api.createRange(containerNode);
7951                          range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
7952                          this.addRange(range, rangeInfo.backward);
7953                      }
7954                  }
7955              ),
7956  
7957              text: createEntryPointFunction(
7958                  function(session, characterOptions) {
7959                      var rangeTexts = [];
7960                      for (var i = 0, len = this.rangeCount; i < len; ++i) {
7961                          rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
7962                      }
7963                      return rangeTexts.join("");
7964                  }
7965              )
7966          });
7967  
7968          /*----------------------------------------------------------------------------------------------------------------*/
7969  
7970          // Extensions to the core rangy object
7971  
7972          api.innerText = function(el, characterOptions) {
7973              var range = api.createRange(el);
7974              range.selectNodeContents(el);
7975              var text = range.text(characterOptions);
7976              return text;
7977          };
7978  
7979          api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
7980              var session = getSession();
7981              iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
7982              var startPos = session.getPosition(startNode, startOffset);
7983              var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
7984              var backward = isDirectionBackward(iteratorOptions.direction);
7985  
7986              return {
7987                  next: function() {
7988                      return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
7989                  },
7990  
7991                  dispose: function() {
7992                      tokenizedTextProvider.dispose();
7993                      this.next = function() {};
7994                  }
7995              };
7996          };
7997  
7998          /*----------------------------------------------------------------------------------------------------------------*/
7999  
8000          api.noMutation = function(func) {
8001              var session = getSession();
8002              func(session);
8003              endSession();
8004          };
8005  
8006          api.noMutation.createEntryPointFunction = createEntryPointFunction;
8007  
8008          api.textRange = {
8009              isBlockNode: isBlockNode,
8010              isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
8011  
8012              createPosition: createEntryPointFunction(
8013                  function(session, node, offset) {
8014                      return session.getPosition(node, offset);
8015                  }
8016              )
8017          };
8018      });
8019      
8020      return rangy;
8021  }, this);
8022  YUI.add('moodle-editor_atto-rangy', function (Y, NAME) {
8023  
8024  // This file is part of Moodle - http://moodle.org/
8025  //
8026  // Moodle is free software: you can redistribute it and/or modify
8027  // it under the terms of the GNU General Public License as published by
8028  // the Free Software Foundation, either version 3 of the License, or
8029  // (at your option) any later version.
8030  //
8031  // Moodle is distributed in the hope that it will be useful,
8032  // but WITHOUT ANY WARRANTY; without even the implied warranty of
8033  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8034  // GNU General Public License for more details.
8035  //
8036  // You should have received a copy of the GNU General Public License
8037  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
8038  
8039  if (!rangy.initialized) {
8040      rangy.init();
8041  }
8042  
8043  
8044  }, '@VERSION@', {"requires": []});


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1