rangy-rails 1.3alpha.804.0 → 1.3.1.pre.dev

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