rangy-rails 1.3alpha.772.0

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