[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/src/rangy/js/ -> rangy-textrange.js (source)

   1  /**

   2   * Text range module for Rangy.

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

   4   *

   5   * Features

   6   *

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

   8   * - Customizable word tokenizer

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

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

  11   *   sensitivity

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

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

  14   * - innerText method for elements

  15   *

  16   * References

  17   *

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

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

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

  21   *

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

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

  24   *

  25   * Depends on Rangy core.

  26   *

  27   * Copyright 2015, Tim Down

  28   * Licensed under the MIT license.

  29   * Version: 1.3.0

  30   * Build date: 10 May 2015

  31   */
  32  
  33  /**

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

  35   *

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

  37   *

  38   * 1 <br>2

  39   *

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

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

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

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

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

  45   *

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

  47   *

  48   * <p>1 </p><p>2<p>

  49   *

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

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

  52   *

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

  54   *

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

  56   * 2</p>

  57   *

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

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

  60   * - Opera ignores the space

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

  62   *

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

  64   * feature-tested

  65   */
  66  (function(factory, root) {
  67      // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
  68      factory(root.rangy);
  69  })(function(rangy) {
  70      rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
  71          var UNDEF = "undefined";
  72          var CHARACTER = "character", WORD = "word";
  73          var dom = api.dom, util = api.util;
  74          var extend = util.extend;
  75          var createOptions = util.createOptions;
  76          var getBody = dom.getBody;
  77  
  78  
  79          var spacesRegex = /^[ \t\f\r\n]+$/;
  80          var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
  81          var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
  82          var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
  83          var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
  84  
  85          var defaultLanguage = "en";
  86  
  87          var isDirectionBackward = api.Selection.isDirectionBackward;
  88  
  89          // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
  90          // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
  91          var trailingSpaceInBlockCollapses = false;
  92          var trailingSpaceBeforeBrCollapses = false;
  93          var trailingSpaceBeforeBlockCollapses = false;
  94          var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
  95  
  96          (function() {
  97              var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
  98              var p = el.firstChild;
  99              var sel = api.getSelection();
 100              sel.collapse(p.lastChild, 2);
 101              sel.setStart(p.firstChild, 0);
 102              trailingSpaceInBlockCollapses = ("" + sel).length == 1;
 103  
 104              el.innerHTML = "1 <br />";
 105              sel.collapse(el, 2);
 106              sel.setStart(el.firstChild, 0);
 107              trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
 108  
 109              el.innerHTML = "1 <p>1</p>";
 110              sel.collapse(el, 2);
 111              sel.setStart(el.firstChild, 0);
 112              trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
 113  
 114              dom.removeNode(el);
 115              sel.removeAllRanges();
 116          })();
 117  
 118          /*----------------------------------------------------------------------------------------------------------------*/
 119  
 120          // This function must create word and non-word tokens for the whole of the text supplied to it
 121          function defaultTokenizer(chars, wordOptions) {
 122              var word = chars.join(""), result, tokenRanges = [];
 123  
 124              function createTokenRange(start, end, isWord) {
 125                  tokenRanges.push( { start: start, end: end, isWord: isWord } );
 126              }
 127  
 128              // Match words and mark characters
 129              var lastWordEnd = 0, wordStart, wordEnd;
 130              while ( (result = wordOptions.wordRegex.exec(word)) ) {
 131                  wordStart = result.index;
 132                  wordEnd = wordStart + result[0].length;
 133  
 134                  // Create token for non-word characters preceding this word
 135                  if (wordStart > lastWordEnd) {
 136                      createTokenRange(lastWordEnd, wordStart, false);
 137                  }
 138  
 139                  // Get trailing space characters for word
 140                  if (wordOptions.includeTrailingSpace) {
 141                      while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
 142                          ++wordEnd;
 143                      }
 144                  }
 145                  createTokenRange(wordStart, wordEnd, true);
 146                  lastWordEnd = wordEnd;
 147              }
 148  
 149              // Create token for trailing non-word characters, if any exist
 150              if (lastWordEnd < chars.length) {
 151                  createTokenRange(lastWordEnd, chars.length, false);
 152              }
 153  
 154              return tokenRanges;
 155          }
 156  
 157          function convertCharRangeToToken(chars, tokenRange) {
 158              var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
 159              var token = {
 160                  isWord: tokenRange.isWord,
 161                  chars: tokenChars,
 162                  toString: function() {
 163                      return tokenChars.join("");
 164                  }
 165              };
 166              for (var i = 0, len = tokenChars.length; i < len; ++i) {
 167                  tokenChars[i].token = token;
 168              }
 169              return token;
 170          }
 171  
 172          function tokenize(chars, wordOptions, tokenizer) {
 173              var tokenRanges = tokenizer(chars, wordOptions);
 174              var tokens = [];
 175              for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
 176                  tokens.push( convertCharRangeToToken(chars, tokenRange) );
 177              }
 178              return tokens;
 179          }
 180  
 181          var defaultCharacterOptions = {
 182              includeBlockContentTrailingSpace: true,
 183              includeSpaceBeforeBr: true,
 184              includeSpaceBeforeBlock: true,
 185              includePreLineTrailingSpace: true,
 186              ignoreCharacters: ""
 187          };
 188  
 189          function normalizeIgnoredCharacters(ignoredCharacters) {
 190              // Check if character is ignored
 191              var ignoredChars = ignoredCharacters || "";
 192  
 193              // Normalize ignored characters into a string consisting of characters in ascending order of character code
 194              var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
 195              ignoredCharsArray.sort(function(char1, char2) {
 196                  return char1.charCodeAt(0) - char2.charCodeAt(0);
 197              });
 198  
 199              /// Convert back to a string and remove duplicates
 200              return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
 201          }
 202  
 203          var defaultCaretCharacterOptions = {
 204              includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
 205              includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
 206              includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
 207              includePreLineTrailingSpace: true
 208          };
 209  
 210          var defaultWordOptions = {
 211              "en": {
 212                  wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
 213                  includeTrailingSpace: false,
 214                  tokenizer: defaultTokenizer
 215              }
 216          };
 217  
 218          var defaultFindOptions = {
 219              caseSensitive: false,
 220              withinRange: null,
 221              wholeWordsOnly: false,
 222              wrap: false,
 223              direction: "forward",
 224              wordOptions: null,
 225              characterOptions: null
 226          };
 227  
 228          var defaultMoveOptions = {
 229              wordOptions: null,
 230              characterOptions: null
 231          };
 232  
 233          var defaultExpandOptions = {
 234              wordOptions: null,
 235              characterOptions: null,
 236              trim: false,
 237              trimStart: true,
 238              trimEnd: true
 239          };
 240  
 241          var defaultWordIteratorOptions = {
 242              wordOptions: null,
 243              characterOptions: null,
 244              direction: "forward"
 245          };
 246  
 247          function createWordOptions(options) {
 248              var lang, defaults;
 249              if (!options) {
 250                  return defaultWordOptions[defaultLanguage];
 251              } else {
 252                  lang = options.language || defaultLanguage;
 253                  defaults = {};
 254                  extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
 255                  extend(defaults, options);
 256                  return defaults;
 257              }
 258          }
 259  
 260          function createNestedOptions(optionsParam, defaults) {
 261              var options = createOptions(optionsParam, defaults);
 262              if (defaults.hasOwnProperty("wordOptions")) {
 263                  options.wordOptions = createWordOptions(options.wordOptions);
 264              }
 265              if (defaults.hasOwnProperty("characterOptions")) {
 266                  options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
 267              }
 268              return options;
 269          }
 270  
 271          /*----------------------------------------------------------------------------------------------------------------*/
 272  
 273          /* DOM utility functions */
 274          var getComputedStyleProperty = dom.getComputedStyleProperty;
 275  
 276          // Create cachable versions of DOM functions
 277  
 278          // Test for old IE's incorrect display properties
 279          var tableCssDisplayBlock;
 280          (function() {
 281              var table = document.createElement("table");
 282              var body = getBody(document);
 283              body.appendChild(table);
 284              tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
 285              body.removeChild(table);
 286          })();
 287  
 288          var defaultDisplayValueForTag = {
 289              table: "table",
 290              caption: "table-caption",
 291              colgroup: "table-column-group",
 292              col: "table-column",
 293              thead: "table-header-group",
 294              tbody: "table-row-group",
 295              tfoot: "table-footer-group",
 296              tr: "table-row",
 297              td: "table-cell",
 298              th: "table-cell"
 299          };
 300  
 301          // Corrects IE's "block" value for table-related elements
 302          function getComputedDisplay(el, win) {
 303              var display = getComputedStyleProperty(el, "display", win);
 304              var tagName = el.tagName.toLowerCase();
 305              return (display == "block" &&
 306                      tableCssDisplayBlock &&
 307                      defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
 308                  defaultDisplayValueForTag[tagName] : display;
 309          }
 310  
 311          function isHidden(node) {
 312              var ancestors = getAncestorsAndSelf(node);
 313              for (var i = 0, len = ancestors.length; i < len; ++i) {
 314                  if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
 315                      return true;
 316                  }
 317              }
 318  
 319              return false;
 320          }
 321  
 322          function isVisibilityHiddenTextNode(textNode) {
 323              var el;
 324              return textNode.nodeType == 3 &&
 325                  (el = textNode.parentNode) &&
 326                  getComputedStyleProperty(el, "visibility") == "hidden";
 327          }
 328  
 329          /*----------------------------------------------------------------------------------------------------------------*/
 330  
 331      
 332          // "A block node is either an Element whose "display" property does not have
 333          // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
 334          // Document, or a DocumentFragment."
 335          function isBlockNode(node) {
 336              return node &&
 337                  ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
 338                  node.nodeType == 9 || node.nodeType == 11);
 339          }
 340  
 341          function getLastDescendantOrSelf(node) {
 342              var lastChild = node.lastChild;
 343              return lastChild ? getLastDescendantOrSelf(lastChild) : node;
 344          }
 345  
 346          function containsPositions(node) {
 347              return dom.isCharacterDataNode(node) ||
 348                  !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
 349          }
 350  
 351          function getAncestors(node) {
 352              var ancestors = [];
 353              while (node.parentNode) {
 354                  ancestors.unshift(node.parentNode);
 355                  node = node.parentNode;
 356              }
 357              return ancestors;
 358          }
 359  
 360          function getAncestorsAndSelf(node) {
 361              return getAncestors(node).concat([node]);
 362          }
 363  
 364          function nextNodeDescendants(node) {
 365              while (node && !node.nextSibling) {
 366                  node = node.parentNode;
 367              }
 368              if (!node) {
 369                  return null;
 370              }
 371              return node.nextSibling;
 372          }
 373  
 374          function nextNode(node, excludeChildren) {
 375              if (!excludeChildren && node.hasChildNodes()) {
 376                  return node.firstChild;
 377              }
 378              return nextNodeDescendants(node);
 379          }
 380  
 381          function previousNode(node) {
 382              var previous = node.previousSibling;
 383              if (previous) {
 384                  node = previous;
 385                  while (node.hasChildNodes()) {
 386                      node = node.lastChild;
 387                  }
 388                  return node;
 389              }
 390              var parent = node.parentNode;
 391              if (parent && parent.nodeType == 1) {
 392                  return parent;
 393              }
 394              return null;
 395          }
 396  
 397          // Adpated from Aryeh's code.
 398          // "A whitespace node is either a Text node whose data is the empty string; or
 399          // a Text node whose data consists only of one or more tabs (0x0009), line
 400          // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
 401          // parent is an Element whose resolved value for "white-space" is "normal" or
 402          // "nowrap"; or a Text node whose data consists only of one or more tabs
 403          // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
 404          // parent is an Element whose resolved value for "white-space" is "pre-line"."
 405          function isWhitespaceNode(node) {
 406              if (!node || node.nodeType != 3) {
 407                  return false;
 408              }
 409              var text = node.data;
 410              if (text === "") {
 411                  return true;
 412              }
 413              var parent = node.parentNode;
 414              if (!parent || parent.nodeType != 1) {
 415                  return false;
 416              }
 417              var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
 418  
 419              return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
 420                  (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
 421          }
 422  
 423          // Adpated from Aryeh's code.
 424          // "node is a collapsed whitespace node if the following algorithm returns
 425          // true:"
 426          function isCollapsedWhitespaceNode(node) {
 427              // "If node's data is the empty string, return true."
 428              if (node.data === "") {
 429                  return true;
 430              }
 431  
 432              // "If node is not a whitespace node, return false."
 433              if (!isWhitespaceNode(node)) {
 434                  return false;
 435              }
 436  
 437              // "Let ancestor be node's parent."
 438              var ancestor = node.parentNode;
 439  
 440              // "If ancestor is null, return true."
 441              if (!ancestor) {
 442                  return true;
 443              }
 444  
 445              // "If the "display" property of some ancestor of node has resolved value "none", return true."
 446              if (isHidden(node)) {
 447                  return true;
 448              }
 449  
 450              return false;
 451          }
 452  
 453          function isCollapsedNode(node) {
 454              var type = node.nodeType;
 455              return type == 7 /* PROCESSING_INSTRUCTION */ ||
 456                  type == 8 /* COMMENT */ ||
 457                  isHidden(node) ||
 458                  /^(script|style)$/i.test(node.nodeName) ||
 459                  isVisibilityHiddenTextNode(node) ||
 460                  isCollapsedWhitespaceNode(node);
 461          }
 462  
 463          function isIgnoredNode(node, win) {
 464              var type = node.nodeType;
 465              return type == 7 /* PROCESSING_INSTRUCTION */ ||
 466                  type == 8 /* COMMENT */ ||
 467                  (type == 1 && getComputedDisplay(node, win) == "none");
 468          }
 469  
 470          /*----------------------------------------------------------------------------------------------------------------*/
 471  
 472          // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
 473  
 474          function Cache() {
 475              this.store = {};
 476          }
 477  
 478          Cache.prototype = {
 479              get: function(key) {
 480                  return this.store.hasOwnProperty(key) ? this.store[key] : null;
 481              },
 482  
 483              set: function(key, value) {
 484                  return this.store[key] = value;
 485              }
 486          };
 487  
 488          var cachedCount = 0, uncachedCount = 0;
 489  
 490          function createCachingGetter(methodName, func, objProperty) {
 491              return function(args) {
 492                  var cache = this.cache;
 493                  if (cache.hasOwnProperty(methodName)) {
 494                      cachedCount++;
 495                      return cache[methodName];
 496                  } else {
 497                      uncachedCount++;
 498                      var value = func.call(this, objProperty ? this[objProperty] : this, args);
 499                      cache[methodName] = value;
 500                      return value;
 501                  }
 502              };
 503          }
 504  
 505          /*----------------------------------------------------------------------------------------------------------------*/
 506  
 507          function NodeWrapper(node, session) {
 508              this.node = node;
 509              this.session = session;
 510              this.cache = new Cache();
 511              this.positions = new Cache();
 512          }
 513  
 514          var nodeProto = {
 515              getPosition: function(offset) {
 516                  var positions = this.positions;
 517                  return positions.get(offset) || positions.set(offset, new Position(this, offset));
 518              },
 519  
 520              toString: function() {
 521                  return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
 522              }
 523          };
 524  
 525          NodeWrapper.prototype = nodeProto;
 526  
 527          var EMPTY = "EMPTY",
 528              NON_SPACE = "NON_SPACE",
 529              UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
 530              COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
 531              TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
 532              TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
 533              TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
 534              PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
 535              TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
 536              INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
 537  
 538          extend(nodeProto, {
 539              isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
 540              getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
 541              getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
 542              containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
 543              isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
 544              isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
 545              getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
 546              isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
 547              isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
 548              next: createCachingGetter("nextPos", nextNode, "node"),
 549              previous: createCachingGetter("previous", previousNode, "node"),
 550  
 551              getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
 552                  var spaceRegex = null, collapseSpaces = false;
 553                  var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
 554                  var preLine = (cssWhitespace == "pre-line");
 555                  if (preLine) {
 556                      spaceRegex = spacesMinusLineBreaksRegex;
 557                      collapseSpaces = true;
 558                  } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
 559                      spaceRegex = spacesRegex;
 560                      collapseSpaces = true;
 561                  }
 562  
 563                  return {
 564                      node: textNode,
 565                      text: textNode.data,
 566                      spaceRegex: spaceRegex,
 567                      collapseSpaces: collapseSpaces,
 568                      preLine: preLine
 569                  };
 570              }, "node"),
 571  
 572              hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
 573                  var session = this.session;
 574                  var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
 575                  var firstPosInEl = session.getPosition(el, 0);
 576  
 577                  var pos = backward ? posAfterEl : firstPosInEl;
 578                  var endPos = backward ? firstPosInEl : posAfterEl;
 579  
 580                  /*
 581                   <body><p>X  </p><p>Y</p></body>
 582  
 583                   Positions:
 584  
 585                   body:0:""
 586                   p:0:""
 587                   text:0:""
 588                   text:1:"X"
 589                   text:2:TRAILING_SPACE_IN_BLOCK
 590                   text:3:COLLAPSED_SPACE
 591                   p:1:""
 592                   body:1:"\n"
 593                   p:0:""
 594                   text:0:""
 595                   text:1:"Y"
 596  
 597                   A character is a TRAILING_SPACE_IN_BLOCK iff:
 598  
 599                   - There is no uncollapsed character after it within the visible containing block element
 600  
 601                   A character is a TRAILING_SPACE_BEFORE_BR iff:
 602  
 603                   - There is no uncollapsed character after it preceding a <br> element
 604  
 605                   An element has inner text iff
 606  
 607                   - It is not hidden
 608                   - It contains an uncollapsed character
 609  
 610                   All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
 611                   */
 612  
 613                  while (pos !== endPos) {
 614                      pos.prepopulateChar();
 615                      if (pos.isDefinitelyNonEmpty()) {
 616                          return true;
 617                      }
 618                      pos = backward ? pos.previousVisible() : pos.nextVisible();
 619                  }
 620  
 621                  return false;
 622              }, "node"),
 623  
 624              isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
 625                  // Ensure that a block element containing a <br> is considered to have inner text
 626                  var brs = el.getElementsByTagName("br");
 627                  for (var i = 0, len = brs.length; i < len; ++i) {
 628                      if (!isCollapsedNode(brs[i])) {
 629                          return true;
 630                      }
 631                  }
 632                  return this.hasInnerText();
 633              }, "node"),
 634  
 635              getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
 636                  if (el.tagName.toLowerCase() == "br") {
 637                      return "";
 638                  } else {
 639                      switch (this.getComputedDisplay()) {
 640                          case "inline":
 641                              var child = el.lastChild;
 642                              while (child) {
 643                                  if (!isIgnoredNode(child)) {
 644                                      return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
 645                                  }
 646                                  child = child.previousSibling;
 647                              }
 648                              break;
 649                          case "inline-block":
 650                          case "inline-table":
 651                          case "none":
 652                          case "table-column":
 653                          case "table-column-group":
 654                              break;
 655                          case "table-cell":
 656                              return "\t";
 657                          default:
 658                              return this.isRenderedBlock(true) ? "\n" : "";
 659                      }
 660                  }
 661                  return "";
 662              }, "node"),
 663  
 664              getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
 665                  switch (this.getComputedDisplay()) {
 666                      case "inline":
 667                      case "inline-block":
 668                      case "inline-table":
 669                      case "none":
 670                      case "table-column":
 671                      case "table-column-group":
 672                      case "table-cell":
 673                          break;
 674                      default:
 675                          return this.isRenderedBlock(false) ? "\n" : "";
 676                  }
 677                  return "";
 678              }, "node")
 679          });
 680  
 681          /*----------------------------------------------------------------------------------------------------------------*/
 682  
 683          function Position(nodeWrapper, offset) {
 684              this.offset = offset;
 685              this.nodeWrapper = nodeWrapper;
 686              this.node = nodeWrapper.node;
 687              this.session = nodeWrapper.session;
 688              this.cache = new Cache();
 689          }
 690  
 691          function inspectPosition() {
 692              return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
 693          }
 694  
 695          var positionProto = {
 696              character: "",
 697              characterType: EMPTY,
 698              isBr: false,
 699  
 700              /*
 701              This method:
 702              - Fully populates positions that have characters that can be determined independently of any other characters.
 703              - Populates most types of space positions with a provisional character. The character is finalized later.
 704               */
 705              prepopulateChar: function() {
 706                  var pos = this;
 707                  if (!pos.prepopulatedChar) {
 708                      var node = pos.node, offset = pos.offset;
 709                      var visibleChar = "", charType = EMPTY;
 710                      var finalizedChar = false;
 711                      if (offset > 0) {
 712                          if (node.nodeType == 3) {
 713                              var text = node.data;
 714                              var textChar = text.charAt(offset - 1);
 715  
 716                              var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
 717                              var spaceRegex = nodeInfo.spaceRegex;
 718                              if (nodeInfo.collapseSpaces) {
 719                                  if (spaceRegex.test(textChar)) {
 720                                      // "If the character at position is from set, append a single space (U+0020) to newdata and advance
 721                                      // position until the character at position is not from set."
 722  
 723                                      // We also need to check for the case where we're in a pre-line and we have a space preceding a
 724                                      // line break, because such spaces are collapsed in some browsers
 725                                      if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
 726                                      } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
 727                                          visibleChar = " ";
 728                                          charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
 729                                      } else {
 730                                          visibleChar = " ";
 731                                          //pos.checkForFollowingLineBreak = true;
 732                                          charType = COLLAPSIBLE_SPACE;
 733                                      }
 734                                  } else {
 735                                      visibleChar = textChar;
 736                                      charType = NON_SPACE;
 737                                      finalizedChar = true;
 738                                  }
 739                              } else {
 740                                  visibleChar = textChar;
 741                                  charType = UNCOLLAPSIBLE_SPACE;
 742                                  finalizedChar = true;
 743                              }
 744                          } else {
 745                              var nodePassed = node.childNodes[offset - 1];
 746                              if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
 747                                  if (nodePassed.tagName.toLowerCase() == "br") {
 748                                      visibleChar = "\n";
 749                                      pos.isBr = true;
 750                                      charType = COLLAPSIBLE_SPACE;
 751                                      finalizedChar = false;
 752                                  } else {
 753                                      pos.checkForTrailingSpace = true;
 754                                  }
 755                              }
 756  
 757                              // Check the leading space of the next node for the case when a block element follows an inline
 758                              // element or text node. In that case, there is an implied line break between the two nodes.
 759                              if (!visibleChar) {
 760                                  var nextNode = node.childNodes[offset];
 761                                  if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
 762                                      pos.checkForLeadingSpace = true;
 763                                  }
 764                              }
 765                          }
 766                      }
 767  
 768                      pos.prepopulatedChar = true;
 769                      pos.character = visibleChar;
 770                      pos.characterType = charType;
 771                      pos.isCharInvariant = finalizedChar;
 772                  }
 773              },
 774  
 775              isDefinitelyNonEmpty: function() {
 776                  var charType = this.characterType;
 777                  return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
 778              },
 779  
 780              // Resolve leading and trailing spaces, which may involve prepopulating other positions
 781              resolveLeadingAndTrailingSpaces: function() {
 782                  if (!this.prepopulatedChar) {
 783                      this.prepopulateChar();
 784                  }
 785                  if (this.checkForTrailingSpace) {
 786                      var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
 787                      if (trailingSpace) {
 788                          this.isTrailingSpace = true;
 789                          this.character = trailingSpace;
 790                          this.characterType = COLLAPSIBLE_SPACE;
 791                      }
 792                      this.checkForTrailingSpace = false;
 793                  }
 794                  if (this.checkForLeadingSpace) {
 795                      var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
 796                      if (leadingSpace) {
 797                          this.isLeadingSpace = true;
 798                          this.character = leadingSpace;
 799                          this.characterType = COLLAPSIBLE_SPACE;
 800                      }
 801                      this.checkForLeadingSpace = false;
 802                  }
 803              },
 804  
 805              getPrecedingUncollapsedPosition: function(characterOptions) {
 806                  var pos = this, character;
 807                  while ( (pos = pos.previousVisible()) ) {
 808                      character = pos.getCharacter(characterOptions);
 809                      if (character !== "") {
 810                          return pos;
 811                      }
 812                  }
 813  
 814                  return null;
 815              },
 816  
 817              getCharacter: function(characterOptions) {
 818                  this.resolveLeadingAndTrailingSpaces();
 819  
 820                  var thisChar = this.character, returnChar;
 821  
 822                  // Check if character is ignored
 823                  var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
 824                  var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
 825  
 826                  // Check if this position's  character is invariant (i.e. not dependent on character options) and return it
 827                  // if so
 828                  if (this.isCharInvariant) {
 829                      returnChar = isIgnoredCharacter ? "" : thisChar;
 830                      return returnChar;
 831                  }
 832  
 833                  var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
 834                  var cachedChar = this.cache.get(cacheKey);
 835                  if (cachedChar !== null) {
 836                      return cachedChar;
 837                  }
 838  
 839                  // We need to actually get the character now
 840                  var character = "";
 841                  var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
 842  
 843                  var nextPos, previousPos;
 844                  var gotPreviousPos = false;
 845                  var pos = this;
 846  
 847                  function getPreviousPos() {
 848                      if (!gotPreviousPos) {
 849                          previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
 850                          gotPreviousPos = true;
 851                      }
 852                      return previousPos;
 853                  }
 854  
 855                  // Disallow a collapsible space that is followed by a line break or is the last character
 856                  if (collapsible) {
 857                      // Allow a trailing space that we've previously determined should be included
 858                      if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
 859                          character = "\n";
 860                      }
 861                      // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
 862                      // or follows a collapsible included space
 863                      else if (thisChar == " " &&
 864                              (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
 865                      }
 866                      // Allow a leading line break unless it follows a line break
 867                      else if (thisChar == "\n" && this.isLeadingSpace) {
 868                          if (getPreviousPos() && previousPos.character != "\n") {
 869                              character = "\n";
 870                          } else {
 871                          }
 872                      } else {
 873                          nextPos = this.nextUncollapsed();
 874                          if (nextPos) {
 875                              if (nextPos.isBr) {
 876                                  this.type = TRAILING_SPACE_BEFORE_BR;
 877                              } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
 878                                  this.type = TRAILING_SPACE_IN_BLOCK;
 879                              } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
 880                                  this.type = TRAILING_SPACE_BEFORE_BLOCK;
 881                              }
 882  
 883                              if (nextPos.character == "\n") {
 884                                  if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
 885                                  } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
 886                                  } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
 887                                  } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
 888                                  } else if (thisChar == "\n") {
 889                                      if (nextPos.isTrailingSpace) {
 890                                          if (this.isTrailingSpace) {
 891                                          } else if (this.isBr) {
 892                                              nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
 893  
 894                                              if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
 895                                                  nextPos.character = "";
 896                                              } else {
 897                                                  nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
 898                                              }
 899                                          }
 900                                      } else {
 901                                          character = "\n";
 902                                      }
 903                                  } else if (thisChar == " ") {
 904                                      character = " ";
 905                                  } else {
 906                                  }
 907                              } else {
 908                                  character = thisChar;
 909                              }
 910                          } else {
 911                          }
 912                      }
 913                  }
 914  
 915                  if (ignoredChars.indexOf(character) > -1) {
 916                      character = "";
 917                  }
 918  
 919  
 920                  this.cache.set(cacheKey, character);
 921  
 922                  return character;
 923              },
 924  
 925              equals: function(pos) {
 926                  return !!pos && this.node === pos.node && this.offset === pos.offset;
 927              },
 928  
 929              inspect: inspectPosition,
 930  
 931              toString: function() {
 932                  return this.character;
 933              }
 934          };
 935  
 936          Position.prototype = positionProto;
 937  
 938          extend(positionProto, {
 939              next: createCachingGetter("nextPos", function(pos) {
 940                  var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
 941                  if (!node) {
 942                      return null;
 943                  }
 944                  var nextNode, nextOffset, child;
 945                  if (offset == nodeWrapper.getLength()) {
 946                      // Move onto the next node
 947                      nextNode = node.parentNode;
 948                      nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
 949                  } else {
 950                      if (nodeWrapper.isCharacterDataNode()) {
 951                          nextNode = node;
 952                          nextOffset = offset + 1;
 953                      } else {
 954                          child = node.childNodes[offset];
 955                          // Go into the children next, if children there are
 956                          if (session.getNodeWrapper(child).containsPositions()) {
 957                              nextNode = child;
 958                              nextOffset = 0;
 959                          } else {
 960                              nextNode = node;
 961                              nextOffset = offset + 1;
 962                          }
 963                      }
 964                  }
 965  
 966                  return nextNode ? session.getPosition(nextNode, nextOffset) : null;
 967              }),
 968  
 969              previous: createCachingGetter("previous", function(pos) {
 970                  var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
 971                  var previousNode, previousOffset, child;
 972                  if (offset == 0) {
 973                      previousNode = node.parentNode;
 974                      previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
 975                  } else {
 976                      if (nodeWrapper.isCharacterDataNode()) {
 977                          previousNode = node;
 978                          previousOffset = offset - 1;
 979                      } else {
 980                          child = node.childNodes[offset - 1];
 981                          // Go into the children next, if children there are
 982                          if (session.getNodeWrapper(child).containsPositions()) {
 983                              previousNode = child;
 984                              previousOffset = dom.getNodeLength(child);
 985                          } else {
 986                              previousNode = node;
 987                              previousOffset = offset - 1;
 988                          }
 989                      }
 990                  }
 991                  return previousNode ? session.getPosition(previousNode, previousOffset) : null;
 992              }),
 993  
 994              /*
 995               Next and previous position moving functions that filter out
 996  
 997               - Hidden (CSS visibility/display) elements
 998               - Script and style elements
 999               */
1000              nextVisible: createCachingGetter("nextVisible", function(pos) {
1001                  var next = pos.next();
1002                  if (!next) {
1003                      return null;
1004                  }
1005                  var nodeWrapper = next.nodeWrapper, node = next.node;
1006                  var newPos = next;
1007                  if (nodeWrapper.isCollapsed()) {
1008                      // We're skipping this node and all its descendants
1009                      newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
1010                  }
1011                  return newPos;
1012              }),
1013  
1014              nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
1015                  var nextPos = pos;
1016                  while ( (nextPos = nextPos.nextVisible()) ) {
1017                      nextPos.resolveLeadingAndTrailingSpaces();
1018                      if (nextPos.character !== "") {
1019                          return nextPos;
1020                      }
1021                  }
1022                  return null;
1023              }),
1024  
1025              previousVisible: createCachingGetter("previousVisible", function(pos) {
1026                  var previous = pos.previous();
1027                  if (!previous) {
1028                      return null;
1029                  }
1030                  var nodeWrapper = previous.nodeWrapper, node = previous.node;
1031                  var newPos = previous;
1032                  if (nodeWrapper.isCollapsed()) {
1033                      // We're skipping this node and all its descendants
1034                      newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
1035                  }
1036                  return newPos;
1037              })
1038          });
1039  
1040          /*----------------------------------------------------------------------------------------------------------------*/
1041  
1042          var currentSession = null;
1043  
1044          var Session = (function() {
1045              function createWrapperCache(nodeProperty) {
1046                  var cache = new Cache();
1047  
1048                  return {
1049                      get: function(node) {
1050                          var wrappersByProperty = cache.get(node[nodeProperty]);
1051                          if (wrappersByProperty) {
1052                              for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
1053                                  if (wrapper.node === node) {
1054                                      return wrapper;
1055                                  }
1056                              }
1057                          }
1058                          return null;
1059                      },
1060  
1061                      set: function(nodeWrapper) {
1062                          var property = nodeWrapper.node[nodeProperty];
1063                          var wrappersByProperty = cache.get(property) || cache.set(property, []);
1064                          wrappersByProperty.push(nodeWrapper);
1065                      }
1066                  };
1067              }
1068  
1069              var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
1070  
1071              function Session() {
1072                  this.initCaches();
1073              }
1074  
1075              Session.prototype = {
1076                  initCaches: function() {
1077                      this.elementCache = uniqueIDSupported ? (function() {
1078                          var elementsCache = new Cache();
1079  
1080                          return {
1081                              get: function(el) {
1082                                  return elementsCache.get(el.uniqueID);
1083                              },
1084  
1085                              set: function(elWrapper) {
1086                                  elementsCache.set(elWrapper.node.uniqueID, elWrapper);
1087                              }
1088                          };
1089                      })() : createWrapperCache("tagName");
1090  
1091                      // Store text nodes keyed by data, although we may need to truncate this
1092                      this.textNodeCache = createWrapperCache("data");
1093                      this.otherNodeCache = createWrapperCache("nodeName");
1094                  },
1095  
1096                  getNodeWrapper: function(node) {
1097                      var wrapperCache;
1098                      switch (node.nodeType) {
1099                          case 1:
1100                              wrapperCache = this.elementCache;
1101                              break;
1102                          case 3:
1103                              wrapperCache = this.textNodeCache;
1104                              break;
1105                          default:
1106                              wrapperCache = this.otherNodeCache;
1107                              break;
1108                      }
1109  
1110                      var wrapper = wrapperCache.get(node);
1111                      if (!wrapper) {
1112                          wrapper = new NodeWrapper(node, this);
1113                          wrapperCache.set(wrapper);
1114                      }
1115                      return wrapper;
1116                  },
1117  
1118                  getPosition: function(node, offset) {
1119                      return this.getNodeWrapper(node).getPosition(offset);
1120                  },
1121  
1122                  getRangeBoundaryPosition: function(range, isStart) {
1123                      var prefix = isStart ? "start" : "end";
1124                      return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
1125                  },
1126  
1127                  detach: function() {
1128                      this.elementCache = this.textNodeCache = this.otherNodeCache = null;
1129                  }
1130              };
1131  
1132              return Session;
1133          })();
1134  
1135          /*----------------------------------------------------------------------------------------------------------------*/
1136  
1137          function startSession() {
1138              endSession();
1139              return (currentSession = new Session());
1140          }
1141  
1142          function getSession() {
1143              return currentSession || startSession();
1144          }
1145  
1146          function endSession() {
1147              if (currentSession) {
1148                  currentSession.detach();
1149              }
1150              currentSession = null;
1151          }
1152  
1153          /*----------------------------------------------------------------------------------------------------------------*/
1154  
1155          // Extensions to the rangy.dom utility object
1156  
1157          extend(dom, {
1158              nextNode: nextNode,
1159              previousNode: previousNode
1160          });
1161  
1162          /*----------------------------------------------------------------------------------------------------------------*/
1163  
1164          function createCharacterIterator(startPos, backward, endPos, characterOptions) {
1165  
1166              // Adjust the end position to ensure that it is actually reached
1167              if (endPos) {
1168                  if (backward) {
1169                      if (isCollapsedNode(endPos.node)) {
1170                          endPos = startPos.previousVisible();
1171                      }
1172                  } else {
1173                      if (isCollapsedNode(endPos.node)) {
1174                          endPos = endPos.nextVisible();
1175                      }
1176                  }
1177              }
1178  
1179              var pos = startPos, finished = false;
1180  
1181              function next() {
1182                  var charPos = null;
1183                  if (backward) {
1184                      charPos = pos;
1185                      if (!finished) {
1186                          pos = pos.previousVisible();
1187                          finished = !pos || (endPos && pos.equals(endPos));
1188                      }
1189                  } else {
1190                      if (!finished) {
1191                          charPos = pos = pos.nextVisible();
1192                          finished = !pos || (endPos && pos.equals(endPos));
1193                      }
1194                  }
1195                  if (finished) {
1196                      pos = null;
1197                  }
1198                  return charPos;
1199              }
1200  
1201              var previousTextPos, returnPreviousTextPos = false;
1202  
1203              return {
1204                  next: function() {
1205                      if (returnPreviousTextPos) {
1206                          returnPreviousTextPos = false;
1207                          return previousTextPos;
1208                      } else {
1209                          var pos, character;
1210                          while ( (pos = next()) ) {
1211                              character = pos.getCharacter(characterOptions);
1212                              if (character) {
1213                                  previousTextPos = pos;
1214                                  return pos;
1215                              }
1216                          }
1217                          return null;
1218                      }
1219                  },
1220  
1221                  rewind: function() {
1222                      if (previousTextPos) {
1223                          returnPreviousTextPos = true;
1224                      } else {
1225                          throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
1226                      }
1227                  },
1228  
1229                  dispose: function() {
1230                      startPos = endPos = null;
1231                  }
1232              };
1233          }
1234  
1235          var arrayIndexOf = Array.prototype.indexOf ?
1236              function(arr, val) {
1237                  return arr.indexOf(val);
1238              } :
1239              function(arr, val) {
1240                  for (var i = 0, len = arr.length; i < len; ++i) {
1241                      if (arr[i] === val) {
1242                          return i;
1243                      }
1244                  }
1245                  return -1;
1246              };
1247  
1248          // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
1249          // is called and there is no more tokenized text
1250          function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
1251              var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
1252              var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
1253              var tokenizer = wordOptions.tokenizer;
1254  
1255              // Consumes a word and the whitespace beyond it
1256              function consumeWord(forward) {
1257                  var pos, textChar;
1258                  var newChars = [], it = forward ? forwardIterator : backwardIterator;
1259  
1260                  var passedWordBoundary = false, insideWord = false;
1261  
1262                  while ( (pos = it.next()) ) {
1263                      textChar = pos.character;
1264  
1265  
1266                      if (allWhiteSpaceRegex.test(textChar)) {
1267                          if (insideWord) {
1268                              insideWord = false;
1269                              passedWordBoundary = true;
1270                          }
1271                      } else {
1272                          if (passedWordBoundary) {
1273                              it.rewind();
1274                              break;
1275                          } else {
1276                              insideWord = true;
1277                          }
1278                      }
1279                      newChars.push(pos);
1280                  }
1281  
1282  
1283                  return newChars;
1284              }
1285  
1286              // Get initial word surrounding initial position and tokenize it
1287              var forwardChars = consumeWord(true);
1288              var backwardChars = consumeWord(false).reverse();
1289              var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
1290  
1291              // Create initial token buffers
1292              var forwardTokensBuffer = forwardChars.length ?
1293                  tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
1294  
1295              var backwardTokensBuffer = backwardChars.length ?
1296                  tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
1297  
1298              function inspectBuffer(buffer) {
1299                  var textPositions = ["[" + buffer.length + "]"];
1300                  for (var i = 0; i < buffer.length; ++i) {
1301                      textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
1302                  }
1303                  return textPositions;
1304              }
1305  
1306  
1307              return {
1308                  nextEndToken: function() {
1309                      var lastToken, forwardChars;
1310  
1311                      // If we're down to the last token, consume character chunks until we have a word or run out of
1312                      // characters to consume
1313                      while ( forwardTokensBuffer.length == 1 &&
1314                          !(lastToken = forwardTokensBuffer[0]).isWord &&
1315                          (forwardChars = consumeWord(true)).length > 0) {
1316  
1317                          // Merge trailing non-word into next word and tokenize
1318                          forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
1319                      }
1320  
1321                      return forwardTokensBuffer.shift();
1322                  },
1323  
1324                  previousStartToken: function() {
1325                      var lastToken, backwardChars;
1326  
1327                      // If we're down to the last token, consume character chunks until we have a word or run out of
1328                      // characters to consume
1329                      while ( backwardTokensBuffer.length == 1 &&
1330                          !(lastToken = backwardTokensBuffer[0]).isWord &&
1331                          (backwardChars = consumeWord(false)).length > 0) {
1332  
1333                          // Merge leading non-word into next word and tokenize
1334                          backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
1335                      }
1336  
1337                      return backwardTokensBuffer.pop();
1338                  },
1339  
1340                  dispose: function() {
1341                      forwardIterator.dispose();
1342                      backwardIterator.dispose();
1343                      forwardTokensBuffer = backwardTokensBuffer = null;
1344                  }
1345              };
1346          }
1347  
1348          function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
1349              var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
1350              if (count !== 0) {
1351                  var backward = (count < 0);
1352  
1353                  switch (unit) {
1354                      case CHARACTER:
1355                          charIterator = createCharacterIterator(pos, backward, null, characterOptions);
1356                          while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
1357                              ++unitsMoved;
1358                              newPos = currentPos;
1359                          }
1360                          nextPos = currentPos;
1361                          charIterator.dispose();
1362                          break;
1363                      case WORD:
1364                          var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
1365                          var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
1366  
1367                          while ( (token = next()) && unitsMoved < absCount ) {
1368                              if (token.isWord) {
1369                                  ++unitsMoved;
1370                                  newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
1371                              }
1372                          }
1373                          break;
1374                      default:
1375                          throw new Error("movePositionBy: unit '" + unit + "' not implemented");
1376                  }
1377  
1378                  // Perform any necessary position tweaks
1379                  if (backward) {
1380                      newPos = newPos.previousVisible();
1381                      unitsMoved = -unitsMoved;
1382                  } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
1383                      // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
1384                      // before a block element (for example, the line break between "1" and "2" in the following HTML:
1385                      // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
1386                      // corresponds with a different selection position in most browsers from the one we want (i.e. at the
1387                      // start of the contents of the block element). We get round this by advancing the position returned to
1388                      // the last possible equivalent visible position.
1389                      if (unit == WORD) {
1390                          charIterator = createCharacterIterator(pos, false, null, characterOptions);
1391                          nextPos = charIterator.next();
1392                          charIterator.dispose();
1393                      }
1394                      if (nextPos) {
1395                          newPos = nextPos.previousVisible();
1396                      }
1397                  }
1398              }
1399  
1400  
1401              return {
1402                  position: newPos,
1403                  unitsMoved: unitsMoved
1404              };
1405          }
1406  
1407          function createRangeCharacterIterator(session, range, characterOptions, backward) {
1408              var rangeStart = session.getRangeBoundaryPosition(range, true);
1409              var rangeEnd = session.getRangeBoundaryPosition(range, false);
1410              var itStart = backward ? rangeEnd : rangeStart;
1411              var itEnd = backward ? rangeStart : rangeEnd;
1412  
1413              return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
1414          }
1415  
1416          function getRangeCharacters(session, range, characterOptions) {
1417  
1418              var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
1419              while ( (pos = it.next()) ) {
1420                  chars.push(pos);
1421              }
1422  
1423              it.dispose();
1424              return chars;
1425          }
1426  
1427          function isWholeWord(startPos, endPos, wordOptions) {
1428              var range = api.createRange(startPos.node);
1429              range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
1430              return !range.expand("word", { wordOptions: wordOptions });
1431          }
1432  
1433          function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
1434              var backward = isDirectionBackward(findOptions.direction);
1435              var it = createCharacterIterator(
1436                  initialPos,
1437                  backward,
1438                  initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
1439                  findOptions.characterOptions
1440              );
1441              var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
1442              var result, insideRegexMatch;
1443              var returnValue = null;
1444  
1445              function handleMatch(startIndex, endIndex) {
1446                  var startPos = chars[startIndex].previousVisible();
1447                  var endPos = chars[endIndex - 1];
1448                  var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
1449  
1450                  return {
1451                      startPos: startPos,
1452                      endPos: endPos,
1453                      valid: valid
1454                  };
1455              }
1456  
1457              while ( (pos = it.next()) ) {
1458                  currentChar = pos.character;
1459                  if (!isRegex && !findOptions.caseSensitive) {
1460                      currentChar = currentChar.toLowerCase();
1461                  }
1462  
1463                  if (backward) {
1464                      chars.unshift(pos);
1465                      text = currentChar + text;
1466                  } else {
1467                      chars.push(pos);
1468                      text += currentChar;
1469                  }
1470  
1471                  if (isRegex) {
1472                      result = searchTerm.exec(text);
1473                      if (result) {
1474                          matchStartIndex = result.index;
1475                          matchEndIndex = matchStartIndex + result[0].length;
1476                          if (insideRegexMatch) {
1477                              // Check whether the match is now over
1478                              if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
1479                                  returnValue = handleMatch(matchStartIndex, matchEndIndex);
1480                                  break;
1481                              }
1482                          } else {
1483                              insideRegexMatch = true;
1484                          }
1485                      }
1486                  } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
1487                      returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
1488                      break;
1489                  }
1490              }
1491  
1492              // Check whether regex match extends to the end of the range
1493              if (insideRegexMatch) {
1494                  returnValue = handleMatch(matchStartIndex, matchEndIndex);
1495              }
1496              it.dispose();
1497  
1498              return returnValue;
1499          }
1500  
1501          function createEntryPointFunction(func) {
1502              return function() {
1503                  var sessionRunning = !!currentSession;
1504                  var session = getSession();
1505                  var args = [session].concat( util.toArray(arguments) );
1506                  var returnValue = func.apply(this, args);
1507                  if (!sessionRunning) {
1508                      endSession();
1509                  }
1510                  return returnValue;
1511              };
1512          }
1513  
1514          /*----------------------------------------------------------------------------------------------------------------*/
1515  
1516          // Extensions to the Rangy Range object
1517  
1518          function createRangeBoundaryMover(isStart, collapse) {
1519              /*
1520               Unit can be "character" or "word"
1521               Options:
1522  
1523               - includeTrailingSpace
1524               - wordRegex
1525               - tokenizer
1526               - collapseSpaceBeforeLineBreak
1527               */
1528              return createEntryPointFunction(
1529                  function(session, unit, count, moveOptions) {
1530                      if (typeof count == UNDEF) {
1531                          count = unit;
1532                          unit = CHARACTER;
1533                      }
1534                      moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
1535  
1536                      var boundaryIsStart = isStart;
1537                      if (collapse) {
1538                          boundaryIsStart = (count >= 0);
1539                          this.collapse(!boundaryIsStart);
1540                      }
1541                      var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
1542                      var newPos = moveResult.position;
1543                      this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
1544                      return moveResult.unitsMoved;
1545                  }
1546              );
1547          }
1548  
1549          function createRangeTrimmer(isStart) {
1550              return createEntryPointFunction(
1551                  function(session, characterOptions) {
1552                      characterOptions = createOptions(characterOptions, defaultCharacterOptions);
1553                      var pos;
1554                      var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
1555                      var trimCharCount = 0;
1556                      while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
1557                          ++trimCharCount;
1558                      }
1559                      it.dispose();
1560                      var trimmed = (trimCharCount > 0);
1561                      if (trimmed) {
1562                          this[isStart ? "moveStart" : "moveEnd"](
1563                              "character",
1564                              isStart ? trimCharCount : -trimCharCount,
1565                              { characterOptions: characterOptions }
1566                          );
1567                      }
1568                      return trimmed;
1569                  }
1570              );
1571          }
1572  
1573          extend(api.rangePrototype, {
1574              moveStart: createRangeBoundaryMover(true, false),
1575  
1576              moveEnd: createRangeBoundaryMover(false, false),
1577  
1578              move: createRangeBoundaryMover(true, true),
1579  
1580              trimStart: createRangeTrimmer(true),
1581  
1582              trimEnd: createRangeTrimmer(false),
1583  
1584              trim: createEntryPointFunction(
1585                  function(session, characterOptions) {
1586                      var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
1587                      return startTrimmed || endTrimmed;
1588                  }
1589              ),
1590  
1591              expand: createEntryPointFunction(
1592                  function(session, unit, expandOptions) {
1593                      var moved = false;
1594                      expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
1595                      var characterOptions = expandOptions.characterOptions;
1596                      if (!unit) {
1597                          unit = CHARACTER;
1598                      }
1599                      if (unit == WORD) {
1600                          var wordOptions = expandOptions.wordOptions;
1601                          var startPos = session.getRangeBoundaryPosition(this, true);
1602                          var endPos = session.getRangeBoundaryPosition(this, false);
1603  
1604                          var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
1605                          var startToken = startTokenizedTextProvider.nextEndToken();
1606                          var newStartPos = startToken.chars[0].previousVisible();
1607                          var endToken, newEndPos;
1608  
1609                          if (this.collapsed) {
1610                              endToken = startToken;
1611                          } else {
1612                              var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
1613                              endToken = endTokenizedTextProvider.previousStartToken();
1614                          }
1615                          newEndPos = endToken.chars[endToken.chars.length - 1];
1616  
1617                          if (!newStartPos.equals(startPos)) {
1618                              this.setStart(newStartPos.node, newStartPos.offset);
1619                              moved = true;
1620                          }
1621                          if (newEndPos && !newEndPos.equals(endPos)) {
1622                              this.setEnd(newEndPos.node, newEndPos.offset);
1623                              moved = true;
1624                          }
1625  
1626                          if (expandOptions.trim) {
1627                              if (expandOptions.trimStart) {
1628                                  moved = this.trimStart(characterOptions) || moved;
1629                              }
1630                              if (expandOptions.trimEnd) {
1631                                  moved = this.trimEnd(characterOptions) || moved;
1632                              }
1633                          }
1634  
1635                          return moved;
1636                      } else {
1637                          return this.moveEnd(CHARACTER, 1, expandOptions);
1638                      }
1639                  }
1640              ),
1641  
1642              text: createEntryPointFunction(
1643                  function(session, characterOptions) {
1644                      return this.collapsed ?
1645                          "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
1646                  }
1647              ),
1648  
1649              selectCharacters: createEntryPointFunction(
1650                  function(session, containerNode, startIndex, endIndex, characterOptions) {
1651                      var moveOptions = { characterOptions: characterOptions };
1652                      if (!containerNode) {
1653                          containerNode = getBody( this.getDocument() );
1654                      }
1655                      this.selectNodeContents(containerNode);
1656                      this.collapse(true);
1657                      this.moveStart("character", startIndex, moveOptions);
1658                      this.collapse(true);
1659                      this.moveEnd("character", endIndex - startIndex, moveOptions);
1660                  }
1661              ),
1662  
1663              // Character indexes are relative to the start of node
1664              toCharacterRange: createEntryPointFunction(
1665                  function(session, containerNode, characterOptions) {
1666                      if (!containerNode) {
1667                          containerNode = getBody( this.getDocument() );
1668                      }
1669                      var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
1670                      var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
1671                      var rangeBetween = this.cloneRange();
1672                      var startIndex, endIndex;
1673                      if (rangeStartsBeforeNode) {
1674                          rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
1675                          startIndex = -rangeBetween.text(characterOptions).length;
1676                      } else {
1677                          rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
1678                          startIndex = rangeBetween.text(characterOptions).length;
1679                      }
1680                      endIndex = startIndex + this.text(characterOptions).length;
1681  
1682                      return {
1683                          start: startIndex,
1684                          end: endIndex
1685                      };
1686                  }
1687              ),
1688  
1689              findText: createEntryPointFunction(
1690                  function(session, searchTermParam, findOptions) {
1691                      // Set up options
1692                      findOptions = createNestedOptions(findOptions, defaultFindOptions);
1693  
1694                      // Create word options if we're matching whole words only
1695                      if (findOptions.wholeWordsOnly) {
1696                          // We don't ever want trailing spaces for search results
1697                          findOptions.wordOptions.includeTrailingSpace = false;
1698                      }
1699  
1700                      var backward = isDirectionBackward(findOptions.direction);
1701  
1702                      // Create a range representing the search scope if none was provided
1703                      var searchScopeRange = findOptions.withinRange;
1704                      if (!searchScopeRange) {
1705                          searchScopeRange = api.createRange();
1706                          searchScopeRange.selectNodeContents(this.getDocument());
1707                      }
1708  
1709                      // Examine and prepare the search term
1710                      var searchTerm = searchTermParam, isRegex = false;
1711                      if (typeof searchTerm == "string") {
1712                          if (!findOptions.caseSensitive) {
1713                              searchTerm = searchTerm.toLowerCase();
1714                          }
1715                      } else {
1716                          isRegex = true;
1717                      }
1718  
1719                      var initialPos = session.getRangeBoundaryPosition(this, !backward);
1720  
1721                      // Adjust initial position if it lies outside the search scope
1722                      var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
1723  
1724                      if (comparison === -1) {
1725                          initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
1726                      } else if (comparison === 1) {
1727                          initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
1728                      }
1729  
1730                      var pos = initialPos;
1731                      var wrappedAround = false;
1732  
1733                      // Try to find a match and ignore invalid ones
1734                      var findResult;
1735                      while (true) {
1736                          findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
1737  
1738                          if (findResult) {
1739                              if (findResult.valid) {
1740                                  this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
1741                                  return true;
1742                              } else {
1743                                  // We've found a match that is not a whole word, so we carry on searching from the point immediately
1744                                  // after the match
1745                                  pos = backward ? findResult.startPos : findResult.endPos;
1746                              }
1747                          } else if (findOptions.wrap && !wrappedAround) {
1748                              // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
1749                              searchScopeRange = searchScopeRange.cloneRange();
1750                              pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
1751                              searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
1752                              wrappedAround = true;
1753                          } else {
1754                              // Nothing found and we can't wrap around, so we're done
1755                              return false;
1756                          }
1757                      }
1758                  }
1759              ),
1760  
1761              pasteHtml: function(html) {
1762                  this.deleteContents();
1763                  if (html) {
1764                      var frag = this.createContextualFragment(html);
1765                      var lastChild = frag.lastChild;
1766                      this.insertNode(frag);
1767                      this.collapseAfter(lastChild);
1768                  }
1769              }
1770          });
1771  
1772          /*----------------------------------------------------------------------------------------------------------------*/
1773  
1774          // Extensions to the Rangy Selection object
1775  
1776          function createSelectionTrimmer(methodName) {
1777              return createEntryPointFunction(
1778                  function(session, characterOptions) {
1779                      var trimmed = false;
1780                      this.changeEachRange(function(range) {
1781                          trimmed = range[methodName](characterOptions) || trimmed;
1782                      });
1783                      return trimmed;
1784                  }
1785              );
1786          }
1787  
1788          extend(api.selectionPrototype, {
1789              expand: createEntryPointFunction(
1790                  function(session, unit, expandOptions) {
1791                      this.changeEachRange(function(range) {
1792                          range.expand(unit, expandOptions);
1793                      });
1794                  }
1795              ),
1796  
1797              move: createEntryPointFunction(
1798                  function(session, unit, count, options) {
1799                      var unitsMoved = 0;
1800                      if (this.focusNode) {
1801                          this.collapse(this.focusNode, this.focusOffset);
1802                          var range = this.getRangeAt(0);
1803                          if (!options) {
1804                              options = {};
1805                          }
1806                          options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
1807                          unitsMoved = range.move(unit, count, options);
1808                          this.setSingleRange(range);
1809                      }
1810                      return unitsMoved;
1811                  }
1812              ),
1813  
1814              trimStart: createSelectionTrimmer("trimStart"),
1815              trimEnd: createSelectionTrimmer("trimEnd"),
1816              trim: createSelectionTrimmer("trim"),
1817  
1818              selectCharacters: createEntryPointFunction(
1819                  function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
1820                      var range = api.createRange(containerNode);
1821                      range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
1822                      this.setSingleRange(range, direction);
1823                  }
1824              ),
1825  
1826              saveCharacterRanges: createEntryPointFunction(
1827                  function(session, containerNode, characterOptions) {
1828                      var ranges = this.getAllRanges(), rangeCount = ranges.length;
1829                      var rangeInfos = [];
1830  
1831                      var backward = rangeCount == 1 && this.isBackward();
1832  
1833                      for (var i = 0, len = ranges.length; i < len; ++i) {
1834                          rangeInfos[i] = {
1835                              characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
1836                              backward: backward,
1837                              characterOptions: characterOptions
1838                          };
1839                      }
1840  
1841                      return rangeInfos;
1842                  }
1843              ),
1844  
1845              restoreCharacterRanges: createEntryPointFunction(
1846                  function(session, containerNode, saved) {
1847                      this.removeAllRanges();
1848                      for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
1849                          rangeInfo = saved[i];
1850                          characterRange = rangeInfo.characterRange;
1851                          range = api.createRange(containerNode);
1852                          range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
1853                          this.addRange(range, rangeInfo.backward);
1854                      }
1855                  }
1856              ),
1857  
1858              text: createEntryPointFunction(
1859                  function(session, characterOptions) {
1860                      var rangeTexts = [];
1861                      for (var i = 0, len = this.rangeCount; i < len; ++i) {
1862                          rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
1863                      }
1864                      return rangeTexts.join("");
1865                  }
1866              )
1867          });
1868  
1869          /*----------------------------------------------------------------------------------------------------------------*/
1870  
1871          // Extensions to the core rangy object
1872  
1873          api.innerText = function(el, characterOptions) {
1874              var range = api.createRange(el);
1875              range.selectNodeContents(el);
1876              var text = range.text(characterOptions);
1877              return text;
1878          };
1879  
1880          api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
1881              var session = getSession();
1882              iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
1883              var startPos = session.getPosition(startNode, startOffset);
1884              var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
1885              var backward = isDirectionBackward(iteratorOptions.direction);
1886  
1887              return {
1888                  next: function() {
1889                      return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
1890                  },
1891  
1892                  dispose: function() {
1893                      tokenizedTextProvider.dispose();
1894                      this.next = function() {};
1895                  }
1896              };
1897          };
1898  
1899          /*----------------------------------------------------------------------------------------------------------------*/
1900  
1901          api.noMutation = function(func) {
1902              var session = getSession();
1903              func(session);
1904              endSession();
1905          };
1906  
1907          api.noMutation.createEntryPointFunction = createEntryPointFunction;
1908  
1909          api.textRange = {
1910              isBlockNode: isBlockNode,
1911              isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
1912  
1913              createPosition: createEntryPointFunction(
1914                  function(session, node, offset) {
1915                      return session.getPosition(node, offset);
1916                  }
1917              )
1918          };
1919      });
1920      
1921      return rangy;
1922  }, this);


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