Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 2022, Tim Down
28
 * Licensed under the MIT license.
29
 * Version: 1.3.1
30
 * Build date: 17 August 2022
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);