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