Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/**
2
 * Rangy, a cross-browser JavaScript range and selection library
3
 * https://github.com/timdown/rangy
4
 *
5
 * Copyright 2022, Tim Down
6
 * Licensed under the MIT license.
7
 * Version: 1.3.1
8
 * Build date: 17 August 2022
9
 */
10
 
11
(function(factory, root) {
12
    // No AMD or CommonJS support so we place Rangy in (probably) the global variable
13
    root.rangy = factory();
14
})(function() {
15
 
16
    var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
17
 
18
    // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
19
    // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
20
    var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
21
        "commonAncestorContainer"];
22
 
23
    // Minimal set of methods required for DOM Level 2 Range compliance
24
    var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
25
        "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
26
        "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
27
 
28
    var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
29
 
30
    // Subset of TextRange's full set of methods that we're interested in
31
    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
32
        "setEndPoint", "getBoundingClientRect"];
33
 
34
    /*----------------------------------------------------------------------------------------------------------------*/
35
 
36
    // Trio of functions taken from Peter Michaux's article:
37
    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
38
    function isHostMethod(o, p) {
39
        var t = typeof o[p];
40
        return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
41
    }
42
 
43
    function isHostObject(o, p) {
44
        return !!(typeof o[p] == OBJECT && o[p]);
45
    }
46
 
47
    function isHostProperty(o, p) {
48
        return typeof o[p] != UNDEFINED;
49
    }
50
 
51
    // Creates a convenience function to save verbose repeated calls to tests functions
52
    function createMultiplePropertyTest(testFunc) {
53
        return function(o, props) {
54
            var i = props.length;
55
            while (i--) {
56
                if (!testFunc(o, props[i])) {
57
                    return false;
58
                }
59
            }
60
            return true;
61
        };
62
    }
63
 
64
    // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
65
    var areHostMethods = createMultiplePropertyTest(isHostMethod);
66
    var areHostObjects = createMultiplePropertyTest(isHostObject);
67
    var areHostProperties = createMultiplePropertyTest(isHostProperty);
68
 
69
    function isTextRange(range) {
70
        return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
71
    }
72
 
73
    function getBody(doc) {
74
        return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
75
    }
76
 
77
    var forEach = [].forEach ?
78
        function(arr, func) {
79
            arr.forEach(func);
80
        } :
81
        function(arr, func) {
82
            for (var i = 0, len = arr.length; i < len; ++i) {
83
                func(arr[i], i);
84
            }
85
        };
86
 
87
    var modules = {};
88
 
89
    var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
90
 
91
    var util = {
92
        isHostMethod: isHostMethod,
93
        isHostObject: isHostObject,
94
        isHostProperty: isHostProperty,
95
        areHostMethods: areHostMethods,
96
        areHostObjects: areHostObjects,
97
        areHostProperties: areHostProperties,
98
        isTextRange: isTextRange,
99
        getBody: getBody,
100
        forEach: forEach
101
    };
102
 
103
    var api = {
104
        version: "1.3.1",
105
        initialized: false,
106
        isBrowser: isBrowser,
107
        supported: true,
108
        util: util,
109
        features: {},
110
        modules: modules,
111
        config: {
112
            alertOnFail: false,
113
            alertOnWarn: false,
114
            preferTextRange: false,
115
            autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
116
        }
117
    };
118
 
119
    function consoleLog(msg) {
120
        if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
121
            console.log(msg);
122
        }
123
    }
124
 
125
    function alertOrLog(msg, shouldAlert) {
126
        if (isBrowser && shouldAlert) {
127
            alert(msg);
128
        } else  {
129
            consoleLog(msg);
130
        }
131
    }
132
 
133
    function fail(reason) {
134
        api.initialized = true;
135
        api.supported = false;
136
        alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
137
    }
138
 
139
    api.fail = fail;
140
 
141
    function warn(msg) {
142
        alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
143
    }
144
 
145
    api.warn = warn;
146
 
147
    // Add utility extend() method
148
    var extend;
149
    if ({}.hasOwnProperty) {
150
        util.extend = extend = function(obj, props, deep) {
151
            var o, p;
152
            for (var i in props) {
153
                if (props.hasOwnProperty(i)) {
154
                    o = obj[i];
155
                    p = props[i];
156
                    if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
157
                        extend(o, p, true);
158
                    }
159
                    obj[i] = p;
160
                }
161
            }
162
            // Special case for toString, which does not show up in for...in loops in IE <= 8
163
            if (props.hasOwnProperty("toString")) {
164
                obj.toString = props.toString;
165
            }
166
            return obj;
167
        };
168
 
169
        util.createOptions = function(optionsParam, defaults) {
170
            var options = {};
171
            extend(options, defaults);
172
            if (optionsParam) {
173
                extend(options, optionsParam);
174
            }
175
            return options;
176
        };
177
    } else {
178
        fail("hasOwnProperty not supported");
179
    }
180
 
181
    // Test whether we're in a browser and bail out if not
182
    if (!isBrowser) {
183
        fail("Rangy can only run in a browser");
184
    }
185
 
186
    // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
187
    (function() {
188
        var toArray;
189
 
190
        if (isBrowser) {
191
            var el = document.createElement("div");
192
            el.appendChild(document.createElement("span"));
193
            var slice = [].slice;
194
            try {
195
                if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
196
                    toArray = function(arrayLike) {
197
                        return slice.call(arrayLike, 0);
198
                    };
199
                }
200
            } catch (e) {}
201
        }
202
 
203
        if (!toArray) {
204
            toArray = function(arrayLike) {
205
                var arr = [];
206
                for (var i = 0, len = arrayLike.length; i < len; ++i) {
207
                    arr[i] = arrayLike[i];
208
                }
209
                return arr;
210
            };
211
        }
212
 
213
        util.toArray = toArray;
214
    })();
215
 
216
    // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
217
    // normalization of event properties because we don't need this.
218
    var addListener;
219
    if (isBrowser) {
220
        if (isHostMethod(document, "addEventListener")) {
221
            addListener = function(obj, eventType, listener) {
222
                obj.addEventListener(eventType, listener, false);
223
            };
224
        } else if (isHostMethod(document, "attachEvent")) {
225
            addListener = function(obj, eventType, listener) {
226
                obj.attachEvent("on" + eventType, listener);
227
            };
228
        } else {
229
            fail("Document does not have required addEventListener or attachEvent method");
230
        }
231
 
232
        util.addListener = addListener;
233
    }
234
 
235
    var initListeners = [];
236
 
237
    function getErrorDesc(ex) {
238
        return ex.message || ex.description || String(ex);
239
    }
240
 
241
    // Initialization
242
    function init() {
243
        if (!isBrowser || api.initialized) {
244
            return;
245
        }
246
        var testRange;
247
        var implementsDomRange = false, implementsTextRange = false;
248
 
249
        // First, perform basic feature tests
250
 
251
        if (isHostMethod(document, "createRange")) {
252
            testRange = document.createRange();
253
            if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
254
                implementsDomRange = true;
255
            }
256
        }
257
 
258
        var body = getBody(document);
259
        if (!body || body.nodeName.toLowerCase() != "body") {
260
            fail("No body element found");
261
            return;
262
        }
263
 
264
        if (body && isHostMethod(body, "createTextRange")) {
265
            testRange = body.createTextRange();
266
            if (isTextRange(testRange)) {
267
                implementsTextRange = true;
268
            }
269
        }
270
 
271
        if (!implementsDomRange && !implementsTextRange) {
272
            fail("Neither Range nor TextRange are available");
273
            return;
274
        }
275
 
276
        api.initialized = true;
277
        api.features = {
278
            implementsDomRange: implementsDomRange,
279
            implementsTextRange: implementsTextRange
280
        };
281
 
282
        // Initialize modules
283
        var module, errorMessage;
284
        for (var moduleName in modules) {
285
            if ( (module = modules[moduleName]) instanceof Module ) {
286
                module.init(module, api);
287
            }
288
        }
289
 
290
        // Call init listeners
291
        for (var i = 0, len = initListeners.length; i < len; ++i) {
292
            try {
293
                initListeners[i](api);
294
            } catch (ex) {
295
                errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
296
                consoleLog(errorMessage);
297
            }
298
        }
299
    }
300
 
301
    function deprecationNotice(deprecated, replacement, module) {
302
        if (module) {
303
            deprecated += " in module " + module.name;
304
        }
305
        api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
306
        replacement + " instead.");
307
    }
308
 
309
    function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
310
        owner[deprecated] = function() {
311
            deprecationNotice(deprecated, replacement, module);
312
            return owner[replacement].apply(owner, util.toArray(arguments));
313
        };
314
    }
315
 
316
    util.deprecationNotice = deprecationNotice;
317
    util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
318
 
319
    // Allow external scripts to initialize this library in case it's loaded after the document has loaded
320
    api.init = init;
321
 
322
    // Execute listener immediately if already initialized
323
    api.addInitListener = function(listener) {
324
        if (api.initialized) {
325
            listener(api);
326
        } else {
327
            initListeners.push(listener);
328
        }
329
    };
330
 
331
    var shimListeners = [];
332
 
333
    api.addShimListener = function(listener) {
334
        shimListeners.push(listener);
335
    };
336
 
337
    function shim(win) {
338
        win = win || window;
339
        init();
340
 
341
        // Notify listeners
342
        for (var i = 0, len = shimListeners.length; i < len; ++i) {
343
            shimListeners[i](win);
344
        }
345
    }
346
 
347
    if (isBrowser) {
348
        api.shim = api.createMissingNativeApi = shim;
349
        createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
350
    }
351
 
352
    function Module(name, dependencies, initializer) {
353
        this.name = name;
354
        this.dependencies = dependencies;
355
        this.initialized = false;
356
        this.supported = false;
357
        this.initializer = initializer;
358
    }
359
 
360
    Module.prototype = {
361
        init: function() {
362
            var requiredModuleNames = this.dependencies || [];
363
            for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
364
                moduleName = requiredModuleNames[i];
365
 
366
                requiredModule = modules[moduleName];
367
                if (!requiredModule || !(requiredModule instanceof Module)) {
368
                    throw new Error("required module '" + moduleName + "' not found");
369
                }
370
 
371
                requiredModule.init();
372
 
373
                if (!requiredModule.supported) {
374
                    throw new Error("required module '" + moduleName + "' not supported");
375
                }
376
            }
377
 
378
            // Now run initializer
379
            this.initializer(this);
380
        },
381
 
382
        fail: function(reason) {
383
            this.initialized = true;
384
            this.supported = false;
385
            throw new Error(reason);
386
        },
387
 
388
        warn: function(msg) {
389
            api.warn("Module " + this.name + ": " + msg);
390
        },
391
 
392
        deprecationNotice: function(deprecated, replacement) {
393
            api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
394
                replacement + " instead");
395
        },
396
 
397
        createError: function(msg) {
398
            return new Error("Error in Rangy " + this.name + " module: " + msg);
399
        }
400
    };
401
 
402
    function createModule(name, dependencies, initFunc) {
403
        var newModule = new Module(name, dependencies, function(module) {
404
            if (!module.initialized) {
405
                module.initialized = true;
406
                try {
407
                    initFunc(api, module);
408
                    module.supported = true;
409
                } catch (ex) {
410
                    var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
411
                    consoleLog(errorMessage);
412
                    if (ex.stack) {
413
                        consoleLog(ex.stack);
414
                    }
415
                }
416
            }
417
        });
418
        modules[name] = newModule;
419
        return newModule;
420
    }
421
 
422
    api.createModule = function(name) {
423
        // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
424
        var initFunc, dependencies;
425
        if (arguments.length == 2) {
426
            initFunc = arguments[1];
427
            dependencies = [];
428
        } else {
429
            initFunc = arguments[2];
430
            dependencies = arguments[1];
431
        }
432
 
433
        var module = createModule(name, dependencies, initFunc);
434
 
435
        // Initialize the module immediately if the core is already initialized
436
        if (api.initialized && api.supported) {
437
            module.init();
438
        }
439
    };
440
 
441
    api.createCoreModule = function(name, dependencies, initFunc) {
442
        createModule(name, dependencies, initFunc);
443
    };
444
 
445
    /*----------------------------------------------------------------------------------------------------------------*/
446
 
447
    // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
448
 
449
    function RangePrototype() {}
450
    api.RangePrototype = RangePrototype;
451
    api.rangePrototype = new RangePrototype();
452
 
453
    function SelectionPrototype() {}
454
    api.selectionPrototype = new SelectionPrototype();
455
 
456
    /*----------------------------------------------------------------------------------------------------------------*/
457
 
458
    // DOM utility methods used by Rangy
459
    api.createCoreModule("DomUtil", [], function(api, module) {
460
        var UNDEF = "undefined";
461
        var util = api.util;
462
        var getBody = util.getBody;
463
 
464
        // Perform feature tests
465
        if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
466
            module.fail("document missing a Node creation method");
467
        }
468
 
469
        if (!util.isHostMethod(document, "getElementsByTagName")) {
470
            module.fail("document missing getElementsByTagName method");
471
        }
472
 
473
        var el = document.createElement("div");
474
        if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
475
                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
476
            module.fail("Incomplete Element implementation");
477
        }
478
 
479
        // innerHTML is required for Range's createContextualFragment method
480
        if (!util.isHostProperty(el, "innerHTML")) {
481
            module.fail("Element is missing innerHTML property");
482
        }
483
 
484
        var textNode = document.createTextNode("test");
485
        if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
486
                !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
487
                !util.areHostProperties(textNode, ["data"]))) {
488
            module.fail("Incomplete Text Node implementation");
489
        }
490
 
491
        /*----------------------------------------------------------------------------------------------------------------*/
492
 
493
        // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
494
        // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
495
        // contains just the document as a single element and the value searched for is the document.
496
        var arrayContains = /*Array.prototype.indexOf ?
497
            function(arr, val) {
498
                return arr.indexOf(val) > -1;
499
            }:*/
500
 
501
            function(arr, val) {
502
                var i = arr.length;
503
                while (i--) {
504
                    if (arr[i] === val) {
505
                        return true;
506
                    }
507
                }
508
                return false;
509
            };
510
 
511
        // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
512
        function isHtmlNamespace(node) {
513
            var ns;
514
            return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
515
        }
516
 
517
        function parentElement(node) {
518
            var parent = node.parentNode;
519
            return (parent.nodeType == 1) ? parent : null;
520
        }
521
 
522
        function getNodeIndex(node) {
523
            var i = 0;
524
            while( (node = node.previousSibling) ) {
525
                ++i;
526
            }
527
            return i;
528
        }
529
 
530
        function getNodeLength(node) {
531
            switch (node.nodeType) {
532
                case 7:
533
                case 10:
534
                    return 0;
535
                case 3:
536
                case 8:
537
                    return node.length;
538
                default:
539
                    return node.childNodes.length;
540
            }
541
        }
542
 
543
        function getCommonAncestor(node1, node2) {
544
            var ancestors = [], n;
545
            for (n = node1; n; n = n.parentNode) {
546
                ancestors.push(n);
547
            }
548
 
549
            for (n = node2; n; n = n.parentNode) {
550
                if (arrayContains(ancestors, n)) {
551
                    return n;
552
                }
553
            }
554
 
555
            return null;
556
        }
557
 
558
        function isAncestorOf(ancestor, descendant, selfIsAncestor) {
559
            var n = selfIsAncestor ? descendant : descendant.parentNode;
560
            while (n) {
561
                if (n === ancestor) {
562
                    return true;
563
                } else {
564
                    n = n.parentNode;
565
                }
566
            }
567
            return false;
568
        }
569
 
570
        function isOrIsAncestorOf(ancestor, descendant) {
571
            return isAncestorOf(ancestor, descendant, true);
572
        }
573
 
574
        function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
575
            var p, n = selfIsAncestor ? node : node.parentNode;
576
            while (n) {
577
                p = n.parentNode;
578
                if (p === ancestor) {
579
                    return n;
580
                }
581
                n = p;
582
            }
583
            return null;
584
        }
585
 
586
        function isCharacterDataNode(node) {
587
            var t = node.nodeType;
588
            return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
589
        }
590
 
591
        function isTextOrCommentNode(node) {
592
            if (!node) {
593
                return false;
594
            }
595
            var t = node.nodeType;
596
            return t == 3 || t == 8 ; // Text or Comment
597
        }
598
 
599
        function insertAfter(node, precedingNode) {
600
            var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
601
            if (nextNode) {
602
                parent.insertBefore(node, nextNode);
603
            } else {
604
                parent.appendChild(node);
605
            }
606
            return node;
607
        }
608
 
609
        // Note that we cannot use splitText() because it is bugridden in IE 9.
610
        function splitDataNode(node, index, positionsToPreserve) {
611
            var newNode = node.cloneNode(false);
612
            newNode.deleteData(0, index);
613
            node.deleteData(index, node.length - index);
614
            insertAfter(newNode, node);
615
 
616
            // Preserve positions
617
            if (positionsToPreserve) {
618
                for (var i = 0, position; position = positionsToPreserve[i++]; ) {
619
                    // Handle case where position was inside the portion of node after the split point
620
                    if (position.node == node && position.offset > index) {
621
                        position.node = newNode;
622
                        position.offset -= index;
623
                    }
624
                    // Handle the case where the position is a node offset within node's parent
625
                    else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
626
                        ++position.offset;
627
                    }
628
                }
629
            }
630
            return newNode;
631
        }
632
 
633
        function getDocument(node) {
634
            if (node.nodeType == 9) {
635
                return node;
636
            } else if (typeof node.ownerDocument != UNDEF) {
637
                return node.ownerDocument;
638
            } else if (typeof node.document != UNDEF) {
639
                return node.document;
640
            } else if (node.parentNode) {
641
                return getDocument(node.parentNode);
642
            } else {
643
                throw module.createError("getDocument: no document found for node");
644
            }
645
        }
646
 
647
        function getWindow(node) {
648
            var doc = getDocument(node);
649
            if (typeof doc.defaultView != UNDEF) {
650
                return doc.defaultView;
651
            } else if (typeof doc.parentWindow != UNDEF) {
652
                return doc.parentWindow;
653
            } else {
654
                throw module.createError("Cannot get a window object for node");
655
            }
656
        }
657
 
658
        function getIframeDocument(iframeEl) {
659
            if (typeof iframeEl.contentDocument != UNDEF) {
660
                return iframeEl.contentDocument;
661
            } else if (typeof iframeEl.contentWindow != UNDEF) {
662
                return iframeEl.contentWindow.document;
663
            } else {
664
                throw module.createError("getIframeDocument: No Document object found for iframe element");
665
            }
666
        }
667
 
668
        function getIframeWindow(iframeEl) {
669
            if (typeof iframeEl.contentWindow != UNDEF) {
670
                return iframeEl.contentWindow;
671
            } else if (typeof iframeEl.contentDocument != UNDEF) {
672
                return iframeEl.contentDocument.defaultView;
673
            } else {
674
                throw module.createError("getIframeWindow: No Window object found for iframe element");
675
            }
676
        }
677
 
678
        // This looks bad. Is it worth it?
679
        function isWindow(obj) {
680
            return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
681
        }
682
 
683
        function getContentDocument(obj, module, methodName) {
684
            var doc;
685
 
686
            if (!obj) {
687
                doc = document;
688
            }
689
 
690
            // Test if a DOM node has been passed and obtain a document object for it if so
691
            else if (util.isHostProperty(obj, "nodeType")) {
692
                doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
693
                    getIframeDocument(obj) : getDocument(obj);
694
            }
695
 
696
            // Test if the doc parameter appears to be a Window object
697
            else if (isWindow(obj)) {
698
                doc = obj.document;
699
            }
700
 
701
            if (!doc) {
702
                throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
703
            }
704
 
705
            return doc;
706
        }
707
 
708
        function getRootContainer(node) {
709
            var parent;
710
            while ( (parent = node.parentNode) ) {
711
                node = parent;
712
            }
713
            return node;
714
        }
715
 
716
        function comparePoints(nodeA, offsetA, nodeB, offsetB) {
717
            // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
718
            var nodeC, root, childA, childB, n;
719
            if (nodeA == nodeB) {
720
                // Case 1: nodes are the same
721
                return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
722
            } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
723
                // Case 2: node C (container B or an ancestor) is a child node of A
724
                return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
725
            } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
726
                // Case 3: node C (container A or an ancestor) is a child node of B
727
                return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
728
            } else {
729
                root = getCommonAncestor(nodeA, nodeB);
730
                if (!root) {
731
                    throw new Error("comparePoints error: nodes have no common ancestor");
732
                }
733
 
734
                // Case 4: containers are siblings or descendants of siblings
735
                childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
736
                childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
737
 
738
                if (childA === childB) {
739
                    // This shouldn't be possible
740
                    throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
741
                } else {
742
                    n = root.firstChild;
743
                    while (n) {
744
                        if (n === childA) {
745
                            return -1;
746
                        } else if (n === childB) {
747
                            return 1;
748
                        }
749
                        n = n.nextSibling;
750
                    }
751
                }
752
            }
753
        }
754
 
755
        /*----------------------------------------------------------------------------------------------------------------*/
756
 
757
        // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
758
        var crashyTextNodes = false;
759
 
760
        function isBrokenNode(node) {
761
            var n;
762
            try {
763
                n = node.parentNode;
764
                return false;
765
            } catch (e) {
766
                return true;
767
            }
768
        }
769
 
770
        (function() {
771
            var el = document.createElement("b");
772
            el.innerHTML = "1";
773
            var textNode = el.firstChild;
774
            el.innerHTML = "<br />";
775
            crashyTextNodes = isBrokenNode(textNode);
776
 
777
            api.features.crashyTextNodes = crashyTextNodes;
778
        })();
779
 
780
        /*----------------------------------------------------------------------------------------------------------------*/
781
 
782
        function inspectNode(node) {
783
            if (!node) {
784
                return "[No node]";
785
            }
786
            if (crashyTextNodes && isBrokenNode(node)) {
787
                return "[Broken node]";
788
            }
789
            if (isCharacterDataNode(node)) {
790
                return '"' + node.data + '"';
791
            }
792
            if (node.nodeType == 1) {
793
                var idAttr = node.id ? ' id="' + node.id + '"' : "";
794
                return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
795
            }
796
            return node.nodeName;
797
        }
798
 
799
        function fragmentFromNodeChildren(node) {
800
            var fragment = getDocument(node).createDocumentFragment(), child;
801
            while ( (child = node.firstChild) ) {
802
                fragment.appendChild(child);
803
            }
804
            return fragment;
805
        }
806
 
807
        var getComputedStyleProperty;
808
        if (typeof window.getComputedStyle != UNDEF) {
809
            getComputedStyleProperty = function(el, propName) {
810
                return getWindow(el).getComputedStyle(el, null)[propName];
811
            };
812
        } else if (typeof document.documentElement.currentStyle != UNDEF) {
813
            getComputedStyleProperty = function(el, propName) {
814
                return el.currentStyle ? el.currentStyle[propName] : "";
815
            };
816
        } else {
817
            module.fail("No means of obtaining computed style properties found");
818
        }
819
 
820
        function createTestElement(doc, html, contentEditable) {
821
            var body = getBody(doc);
822
            var el = doc.createElement("div");
823
            el.contentEditable = "" + !!contentEditable;
824
            if (html) {
825
                el.innerHTML = html;
826
            }
827
 
828
            // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
829
            var bodyFirstChild = body.firstChild;
830
            if (bodyFirstChild) {
831
                body.insertBefore(el, bodyFirstChild);
832
            } else {
833
                body.appendChild(el);
834
            }
835
 
836
            return el;
837
        }
838
 
839
        function removeNode(node) {
840
            return node.parentNode.removeChild(node);
841
        }
842
 
843
        function NodeIterator(root) {
844
            this.root = root;
845
            this._next = root;
846
        }
847
 
848
        NodeIterator.prototype = {
849
            _current: null,
850
 
851
            hasNext: function() {
852
                return !!this._next;
853
            },
854
 
855
            next: function() {
856
                var n = this._current = this._next;
857
                var child, next;
858
                if (this._current) {
859
                    child = n.firstChild;
860
                    if (child) {
861
                        this._next = child;
862
                    } else {
863
                        next = null;
864
                        while ((n !== this.root) && !(next = n.nextSibling)) {
865
                            n = n.parentNode;
866
                        }
867
                        this._next = next;
868
                    }
869
                }
870
                return this._current;
871
            },
872
 
873
            detach: function() {
874
                this._current = this._next = this.root = null;
875
            }
876
        };
877
 
878
        function createIterator(root) {
879
            return new NodeIterator(root);
880
        }
881
 
882
        function DomPosition(node, offset) {
883
            this.node = node;
884
            this.offset = offset;
885
        }
886
 
887
        DomPosition.prototype = {
888
            equals: function(pos) {
889
                return !!pos && this.node === pos.node && this.offset == pos.offset;
890
            },
891
 
892
            inspect: function() {
893
                return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
894
            },
895
 
896
            toString: function() {
897
                return this.inspect();
898
            }
899
        };
900
 
901
        function DOMException(codeName) {
902
            this.code = this[codeName];
903
            this.codeName = codeName;
904
            this.message = "DOMException: " + this.codeName;
905
        }
906
 
907
        DOMException.prototype = {
908
            INDEX_SIZE_ERR: 1,
909
            HIERARCHY_REQUEST_ERR: 3,
910
            WRONG_DOCUMENT_ERR: 4,
911
            NO_MODIFICATION_ALLOWED_ERR: 7,
912
            NOT_FOUND_ERR: 8,
913
            NOT_SUPPORTED_ERR: 9,
914
            INVALID_STATE_ERR: 11,
915
            INVALID_NODE_TYPE_ERR: 24
916
        };
917
 
918
        DOMException.prototype.toString = function() {
919
            return this.message;
920
        };
921
 
922
        api.dom = {
923
            arrayContains: arrayContains,
924
            isHtmlNamespace: isHtmlNamespace,
925
            parentElement: parentElement,
926
            getNodeIndex: getNodeIndex,
927
            getNodeLength: getNodeLength,
928
            getCommonAncestor: getCommonAncestor,
929
            isAncestorOf: isAncestorOf,
930
            isOrIsAncestorOf: isOrIsAncestorOf,
931
            getClosestAncestorIn: getClosestAncestorIn,
932
            isCharacterDataNode: isCharacterDataNode,
933
            isTextOrCommentNode: isTextOrCommentNode,
934
            insertAfter: insertAfter,
935
            splitDataNode: splitDataNode,
936
            getDocument: getDocument,
937
            getWindow: getWindow,
938
            getIframeWindow: getIframeWindow,
939
            getIframeDocument: getIframeDocument,
940
            getBody: getBody,
941
            isWindow: isWindow,
942
            getContentDocument: getContentDocument,
943
            getRootContainer: getRootContainer,
944
            comparePoints: comparePoints,
945
            isBrokenNode: isBrokenNode,
946
            inspectNode: inspectNode,
947
            getComputedStyleProperty: getComputedStyleProperty,
948
            createTestElement: createTestElement,
949
            removeNode: removeNode,
950
            fragmentFromNodeChildren: fragmentFromNodeChildren,
951
            createIterator: createIterator,
952
            DomPosition: DomPosition
953
        };
954
 
955
        api.DOMException = DOMException;
956
    });
957
 
958
    /*----------------------------------------------------------------------------------------------------------------*/
959
 
960
    // Pure JavaScript implementation of DOM Range
961
    api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
962
        var dom = api.dom;
963
        var util = api.util;
964
        var DomPosition = dom.DomPosition;
965
        var DOMException = api.DOMException;
966
 
967
        var isCharacterDataNode = dom.isCharacterDataNode;
968
        var getNodeIndex = dom.getNodeIndex;
969
        var isOrIsAncestorOf = dom.isOrIsAncestorOf;
970
        var getDocument = dom.getDocument;
971
        var comparePoints = dom.comparePoints;
972
        var splitDataNode = dom.splitDataNode;
973
        var getClosestAncestorIn = dom.getClosestAncestorIn;
974
        var getNodeLength = dom.getNodeLength;
975
        var arrayContains = dom.arrayContains;
976
        var getRootContainer = dom.getRootContainer;
977
        var crashyTextNodes = api.features.crashyTextNodes;
978
 
979
        var removeNode = dom.removeNode;
980
 
981
        /*----------------------------------------------------------------------------------------------------------------*/
982
 
983
        // Utility functions
984
 
985
        function isNonTextPartiallySelected(node, range) {
986
            return (node.nodeType != 3) &&
987
                   (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
988
        }
989
 
990
        function getRangeDocument(range) {
991
            return range.document || getDocument(range.startContainer);
992
        }
993
 
994
        function getRangeRoot(range) {
995
            return getRootContainer(range.startContainer);
996
        }
997
 
998
        function getBoundaryBeforeNode(node) {
999
            return new DomPosition(node.parentNode, getNodeIndex(node));
1000
        }
1001
 
1002
        function getBoundaryAfterNode(node) {
1003
            return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1004
        }
1005
 
1006
        function insertNodeAtPosition(node, n, o) {
1007
            var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1008
            if (isCharacterDataNode(n)) {
1009
                if (o == n.length) {
1010
                    dom.insertAfter(node, n);
1011
                } else {
1012
                    n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1013
                }
1014
            } else if (o >= n.childNodes.length) {
1015
                n.appendChild(node);
1016
            } else {
1017
                n.insertBefore(node, n.childNodes[o]);
1018
            }
1019
            return firstNodeInserted;
1020
        }
1021
 
1022
        function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1023
            assertRangeValid(rangeA);
1024
            assertRangeValid(rangeB);
1025
 
1026
            if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1027
                throw new DOMException("WRONG_DOCUMENT_ERR");
1028
            }
1029
 
1030
            var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1031
                endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1032
 
1033
            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1034
        }
1035
 
1036
        function cloneSubtree(iterator) {
1037
            var partiallySelected;
1038
            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1039
                partiallySelected = iterator.isPartiallySelectedSubtree();
1040
                node = node.cloneNode(!partiallySelected);
1041
                if (partiallySelected) {
1042
                    subIterator = iterator.getSubtreeIterator();
1043
                    node.appendChild(cloneSubtree(subIterator));
1044
                    subIterator.detach();
1045
                }
1046
 
1047
                if (node.nodeType == 10) { // DocumentType
1048
                    throw new DOMException("HIERARCHY_REQUEST_ERR");
1049
                }
1050
                frag.appendChild(node);
1051
            }
1052
            return frag;
1053
        }
1054
 
1055
        function iterateSubtree(rangeIterator, func, iteratorState) {
1056
            var it, n;
1057
            iteratorState = iteratorState || { stop: false };
1058
            for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1059
                if (rangeIterator.isPartiallySelectedSubtree()) {
1060
                    if (func(node) === false) {
1061
                        iteratorState.stop = true;
1062
                        return;
1063
                    } else {
1064
                        // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1065
                        // the node selected by the Range.
1066
                        subRangeIterator = rangeIterator.getSubtreeIterator();
1067
                        iterateSubtree(subRangeIterator, func, iteratorState);
1068
                        subRangeIterator.detach();
1069
                        if (iteratorState.stop) {
1070
                            return;
1071
                        }
1072
                    }
1073
                } else {
1074
                    // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1075
                    // descendants
1076
                    it = dom.createIterator(node);
1077
                    while ( (n = it.next()) ) {
1078
                        if (func(n) === false) {
1079
                            iteratorState.stop = true;
1080
                            return;
1081
                        }
1082
                    }
1083
                }
1084
            }
1085
        }
1086
 
1087
        function deleteSubtree(iterator) {
1088
            var subIterator;
1089
            while (iterator.next()) {
1090
                if (iterator.isPartiallySelectedSubtree()) {
1091
                    subIterator = iterator.getSubtreeIterator();
1092
                    deleteSubtree(subIterator);
1093
                    subIterator.detach();
1094
                } else {
1095
                    iterator.remove();
1096
                }
1097
            }
1098
        }
1099
 
1100
        function extractSubtree(iterator) {
1101
            for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1102
 
1103
                if (iterator.isPartiallySelectedSubtree()) {
1104
                    node = node.cloneNode(false);
1105
                    subIterator = iterator.getSubtreeIterator();
1106
                    node.appendChild(extractSubtree(subIterator));
1107
                    subIterator.detach();
1108
                } else {
1109
                    iterator.remove();
1110
                }
1111
                if (node.nodeType == 10) { // DocumentType
1112
                    throw new DOMException("HIERARCHY_REQUEST_ERR");
1113
                }
1114
                frag.appendChild(node);
1115
            }
1116
            return frag;
1117
        }
1118
 
1119
        function getNodesInRange(range, nodeTypes, filter) {
1120
            var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1121
            var filterExists = !!filter;
1122
            if (filterNodeTypes) {
1123
                regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1124
            }
1125
 
1126
            var nodes = [];
1127
            iterateSubtree(new RangeIterator(range, false), function(node) {
1128
                if (filterNodeTypes && !regex.test(node.nodeType)) {
1129
                    return;
1130
                }
1131
                if (filterExists && !filter(node)) {
1132
                    return;
1133
                }
1134
                // Don't include a boundary container if it is a character data node and the range does not contain any
1135
                // of its character data. See issue 190.
1136
                var sc = range.startContainer;
1137
                if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1138
                    return;
1139
                }
1140
 
1141
                var ec = range.endContainer;
1142
                if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1143
                    return;
1144
                }
1145
 
1146
                nodes.push(node);
1147
            });
1148
            return nodes;
1149
        }
1150
 
1151
        function inspect(range) {
1152
            var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1153
            return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1154
                    dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1155
        }
1156
 
1157
        /*----------------------------------------------------------------------------------------------------------------*/
1158
 
1159
        // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1160
 
1161
        function RangeIterator(range, clonePartiallySelectedTextNodes) {
1162
            this.range = range;
1163
            this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1164
 
1165
 
1166
            if (!range.collapsed) {
1167
                this.sc = range.startContainer;
1168
                this.so = range.startOffset;
1169
                this.ec = range.endContainer;
1170
                this.eo = range.endOffset;
1171
                var root = range.commonAncestorContainer;
1172
 
1173
                if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1174
                    this.isSingleCharacterDataNode = true;
1175
                    this._first = this._last = this._next = this.sc;
1176
                } else {
1177
                    this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1178
                        this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1179
                    this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1180
                        this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1181
                }
1182
            }
1183
        }
1184
 
1185
        RangeIterator.prototype = {
1186
            _current: null,
1187
            _next: null,
1188
            _first: null,
1189
            _last: null,
1190
            isSingleCharacterDataNode: false,
1191
 
1192
            reset: function() {
1193
                this._current = null;
1194
                this._next = this._first;
1195
            },
1196
 
1197
            hasNext: function() {
1198
                return !!this._next;
1199
            },
1200
 
1201
            next: function() {
1202
                // Move to next node
1203
                var current = this._current = this._next;
1204
                if (current) {
1205
                    this._next = (current !== this._last) ? current.nextSibling : null;
1206
 
1207
                    // Check for partially selected text nodes
1208
                    if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1209
                        if (current === this.ec) {
1210
                            (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1211
                        }
1212
                        if (this._current === this.sc) {
1213
                            (current = current.cloneNode(true)).deleteData(0, this.so);
1214
                        }
1215
                    }
1216
                }
1217
 
1218
                return current;
1219
            },
1220
 
1221
            remove: function() {
1222
                var current = this._current, start, end;
1223
 
1224
                if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1225
                    start = (current === this.sc) ? this.so : 0;
1226
                    end = (current === this.ec) ? this.eo : current.length;
1227
                    if (start != end) {
1228
                        current.deleteData(start, end - start);
1229
                    }
1230
                } else {
1231
                    if (current.parentNode) {
1232
                        removeNode(current);
1233
                    } else {
1234
                    }
1235
                }
1236
            },
1237
 
1238
            // Checks if the current node is partially selected
1239
            isPartiallySelectedSubtree: function() {
1240
                var current = this._current;
1241
                return isNonTextPartiallySelected(current, this.range);
1242
            },
1243
 
1244
            getSubtreeIterator: function() {
1245
                var subRange;
1246
                if (this.isSingleCharacterDataNode) {
1247
                    subRange = this.range.cloneRange();
1248
                    subRange.collapse(false);
1249
                } else {
1250
                    subRange = new Range(getRangeDocument(this.range));
1251
                    var current = this._current;
1252
                    var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1253
 
1254
                    if (isOrIsAncestorOf(current, this.sc)) {
1255
                        startContainer = this.sc;
1256
                        startOffset = this.so;
1257
                    }
1258
                    if (isOrIsAncestorOf(current, this.ec)) {
1259
                        endContainer = this.ec;
1260
                        endOffset = this.eo;
1261
                    }
1262
 
1263
                    updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1264
                }
1265
                return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1266
            },
1267
 
1268
            detach: function() {
1269
                this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1270
            }
1271
        };
1272
 
1273
        /*----------------------------------------------------------------------------------------------------------------*/
1274
 
1275
        var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1276
        var rootContainerNodeTypes = [2, 9, 11];
1277
        var readonlyNodeTypes = [5, 6, 10, 12];
1278
        var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1279
        var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1280
 
1281
        function createAncestorFinder(nodeTypes) {
1282
            return function(node, selfIsAncestor) {
1283
                var t, n = selfIsAncestor ? node : node.parentNode;
1284
                while (n) {
1285
                    t = n.nodeType;
1286
                    if (arrayContains(nodeTypes, t)) {
1287
                        return n;
1288
                    }
1289
                    n = n.parentNode;
1290
                }
1291
                return null;
1292
            };
1293
        }
1294
 
1295
        var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1296
        var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1297
        var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1298
        var getElementAncestor = createAncestorFinder( [1] );
1299
 
1300
        function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1301
            if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1302
                throw new DOMException("INVALID_NODE_TYPE_ERR");
1303
            }
1304
        }
1305
 
1306
        function assertValidNodeType(node, invalidTypes) {
1307
            if (!arrayContains(invalidTypes, node.nodeType)) {
1308
                throw new DOMException("INVALID_NODE_TYPE_ERR");
1309
            }
1310
        }
1311
 
1312
        function assertValidOffset(node, offset) {
1313
            if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1314
                throw new DOMException("INDEX_SIZE_ERR");
1315
            }
1316
        }
1317
 
1318
        function assertSameDocumentOrFragment(node1, node2) {
1319
            if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1320
                throw new DOMException("WRONG_DOCUMENT_ERR");
1321
            }
1322
        }
1323
 
1324
        function assertNodeNotReadOnly(node) {
1325
            if (getReadonlyAncestor(node, true)) {
1326
                throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1327
            }
1328
        }
1329
 
1330
        function assertNode(node, codeName) {
1331
            if (!node) {
1332
                throw new DOMException(codeName);
1333
            }
1334
        }
1335
 
1336
        function isValidOffset(node, offset) {
1337
            return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1338
        }
1339
 
1340
        function isRangeValid(range) {
1341
            return (!!range.startContainer && !!range.endContainer &&
1342
                    !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1343
                    getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1344
                    isValidOffset(range.startContainer, range.startOffset) &&
1345
                    isValidOffset(range.endContainer, range.endOffset));
1346
        }
1347
 
1348
        function assertRangeValid(range) {
1349
            if (!isRangeValid(range)) {
1350
                throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1351
            }
1352
        }
1353
 
1354
        /*----------------------------------------------------------------------------------------------------------------*/
1355
 
1356
        // Test the browser's innerHTML support to decide how to implement createContextualFragment
1357
        var styleEl = document.createElement("style");
1358
        var htmlParsingConforms = false;
1359
        try {
1360
            styleEl.innerHTML = "<b>x</b>";
1361
            htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Pre-Blink Opera incorrectly creates an element node
1362
        } catch (e) {
1363
            // IE 6 and 7 throw
1364
        }
1365
 
1366
        api.features.htmlParsingConforms = htmlParsingConforms;
1367
 
1368
        var createContextualFragment = htmlParsingConforms ?
1369
 
1370
            // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1371
            // discussion and base code for this implementation at issue 67.
1372
            // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1373
            // Thanks to Aleks Williams.
1374
            function(fragmentStr) {
1375
                // "Let node the context object's start's node."
1376
                var node = this.startContainer;
1377
                var doc = getDocument(node);
1378
 
1379
                // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1380
                // exception and abort these steps."
1381
                if (!node) {
1382
                    throw new DOMException("INVALID_STATE_ERR");
1383
                }
1384
 
1385
                // "Let element be as follows, depending on node's interface:"
1386
                // Document, Document Fragment: null
1387
                var el = null;
1388
 
1389
                // "Element: node"
1390
                if (node.nodeType == 1) {
1391
                    el = node;
1392
 
1393
                // "Text, Comment: node's parentElement"
1394
                } else if (isCharacterDataNode(node)) {
1395
                    el = dom.parentElement(node);
1396
                }
1397
 
1398
                // "If either element is null or element's ownerDocument is an HTML document
1399
                // and element's local name is "html" and element's namespace is the HTML
1400
                // namespace"
1401
                if (el === null || (
1402
                    el.nodeName == "HTML" &&
1403
                    dom.isHtmlNamespace(getDocument(el).documentElement) &&
1404
                    dom.isHtmlNamespace(el)
1405
                )) {
1406
 
1407
                // "let element be a new Element with "body" as its local name and the HTML
1408
                // namespace as its namespace.""
1409
                    el = doc.createElement("body");
1410
                } else {
1411
                    el = el.cloneNode(false);
1412
                }
1413
 
1414
                // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1415
                // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1416
                // "In either case, the algorithm must be invoked with fragment as the input
1417
                // and element as the context element."
1418
                el.innerHTML = fragmentStr;
1419
 
1420
                // "If this raises an exception, then abort these steps. Otherwise, let new
1421
                // children be the nodes returned."
1422
 
1423
                // "Let fragment be a new DocumentFragment."
1424
                // "Append all new children to fragment."
1425
                // "Return fragment."
1426
                return dom.fragmentFromNodeChildren(el);
1427
            } :
1428
 
1429
            // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1430
            // previous versions of Rangy used (with the exception of using a body element rather than a div)
1431
            function(fragmentStr) {
1432
                var doc = getRangeDocument(this);
1433
                var el = doc.createElement("body");
1434
                el.innerHTML = fragmentStr;
1435
 
1436
                return dom.fragmentFromNodeChildren(el);
1437
            };
1438
 
1439
        function splitRangeBoundaries(range, positionsToPreserve) {
1440
            assertRangeValid(range);
1441
 
1442
            var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1443
            var startEndSame = (sc === ec);
1444
 
1445
            if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1446
                splitDataNode(ec, eo, positionsToPreserve);
1447
            }
1448
 
1449
            if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1450
                sc = splitDataNode(sc, so, positionsToPreserve);
1451
                if (startEndSame) {
1452
                    eo -= so;
1453
                    ec = sc;
1454
                } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1455
                    eo++;
1456
                }
1457
                so = 0;
1458
            }
1459
            range.setStartAndEnd(sc, so, ec, eo);
1460
        }
1461
 
1462
        function rangeToHtml(range) {
1463
            assertRangeValid(range);
1464
            var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1465
            container.appendChild( range.cloneContents() );
1466
            return container.innerHTML;
1467
        }
1468
 
1469
        /*----------------------------------------------------------------------------------------------------------------*/
1470
 
1471
        var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1472
            "commonAncestorContainer"];
1473
 
1474
        var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1475
        var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1476
 
1477
        util.extend(api.rangePrototype, {
1478
            compareBoundaryPoints: function(how, range) {
1479
                assertRangeValid(this);
1480
                assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1481
 
1482
                var nodeA, offsetA, nodeB, offsetB;
1483
                var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1484
                var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1485
                nodeA = this[prefixA + "Container"];
1486
                offsetA = this[prefixA + "Offset"];
1487
                nodeB = range[prefixB + "Container"];
1488
                offsetB = range[prefixB + "Offset"];
1489
                return comparePoints(nodeA, offsetA, nodeB, offsetB);
1490
            },
1491
 
1492
            insertNode: function(node) {
1493
                assertRangeValid(this);
1494
                assertValidNodeType(node, insertableNodeTypes);
1495
                assertNodeNotReadOnly(this.startContainer);
1496
 
1497
                if (isOrIsAncestorOf(node, this.startContainer)) {
1498
                    throw new DOMException("HIERARCHY_REQUEST_ERR");
1499
                }
1500
 
1501
                // No check for whether the container of the start of the Range is of a type that does not allow
1502
                // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1503
                // to add the node
1504
 
1505
                var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1506
                this.setStartBefore(firstNodeInserted);
1507
            },
1508
 
1509
            cloneContents: function() {
1510
                assertRangeValid(this);
1511
 
1512
                var clone, frag;
1513
                if (this.collapsed) {
1514
                    return getRangeDocument(this).createDocumentFragment();
1515
                } else {
1516
                    if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1517
                        clone = this.startContainer.cloneNode(true);
1518
                        clone.data = clone.data.slice(this.startOffset, this.endOffset);
1519
                        frag = getRangeDocument(this).createDocumentFragment();
1520
                        frag.appendChild(clone);
1521
                        return frag;
1522
                    } else {
1523
                        var iterator = new RangeIterator(this, true);
1524
                        clone = cloneSubtree(iterator);
1525
                        iterator.detach();
1526
                    }
1527
                    return clone;
1528
                }
1529
            },
1530
 
1531
            canSurroundContents: function() {
1532
                assertRangeValid(this);
1533
                assertNodeNotReadOnly(this.startContainer);
1534
                assertNodeNotReadOnly(this.endContainer);
1535
 
1536
                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1537
                // no non-text nodes.
1538
                var iterator = new RangeIterator(this, true);
1539
                var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1540
                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1541
                iterator.detach();
1542
                return !boundariesInvalid;
1543
            },
1544
 
1545
            surroundContents: function(node) {
1546
                assertValidNodeType(node, surroundNodeTypes);
1547
 
1548
                if (!this.canSurroundContents()) {
1549
                    throw new DOMException("INVALID_STATE_ERR");
1550
                }
1551
 
1552
                // Extract the contents
1553
                var content = this.extractContents();
1554
 
1555
                // Clear the children of the node
1556
                if (node.hasChildNodes()) {
1557
                    while (node.lastChild) {
1558
                        node.removeChild(node.lastChild);
1559
                    }
1560
                }
1561
 
1562
                // Insert the new node and add the extracted contents
1563
                insertNodeAtPosition(node, this.startContainer, this.startOffset);
1564
                node.appendChild(content);
1565
 
1566
                this.selectNode(node);
1567
            },
1568
 
1569
            cloneRange: function() {
1570
                assertRangeValid(this);
1571
                var range = new Range(getRangeDocument(this));
1572
                var i = rangeProperties.length, prop;
1573
                while (i--) {
1574
                    prop = rangeProperties[i];
1575
                    range[prop] = this[prop];
1576
                }
1577
                return range;
1578
            },
1579
 
1580
            toString: function() {
1581
                assertRangeValid(this);
1582
                var sc = this.startContainer;
1583
                if (sc === this.endContainer && isCharacterDataNode(sc)) {
1584
                    return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1585
                } else {
1586
                    var textParts = [], iterator = new RangeIterator(this, true);
1587
                    iterateSubtree(iterator, function(node) {
1588
                        // Accept only text or CDATA nodes, not comments
1589
                        if (node.nodeType == 3 || node.nodeType == 4) {
1590
                            textParts.push(node.data);
1591
                        }
1592
                    });
1593
                    iterator.detach();
1594
                    return textParts.join("");
1595
                }
1596
            },
1597
 
1598
            // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1599
            // been removed from Mozilla.
1600
 
1601
            compareNode: function(node) {
1602
                assertRangeValid(this);
1603
 
1604
                var parent = node.parentNode;
1605
                var nodeIndex = getNodeIndex(node);
1606
 
1607
                if (!parent) {
1608
                    throw new DOMException("NOT_FOUND_ERR");
1609
                }
1610
 
1611
                var startComparison = this.comparePoint(parent, nodeIndex),
1612
                    endComparison = this.comparePoint(parent, nodeIndex + 1);
1613
 
1614
                if (startComparison < 0) { // Node starts before
1615
                    return (endComparison > 0) ? n_b_a : n_b;
1616
                } else {
1617
                    return (endComparison > 0) ? n_a : n_i;
1618
                }
1619
            },
1620
 
1621
            comparePoint: function(node, offset) {
1622
                assertRangeValid(this);
1623
                assertNode(node, "HIERARCHY_REQUEST_ERR");
1624
                assertSameDocumentOrFragment(node, this.startContainer);
1625
 
1626
                if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1627
                    return -1;
1628
                } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1629
                    return 1;
1630
                }
1631
                return 0;
1632
            },
1633
 
1634
            createContextualFragment: createContextualFragment,
1635
 
1636
            toHtml: function() {
1637
                return rangeToHtml(this);
1638
            },
1639
 
1640
            // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1641
            // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1642
            intersectsNode: function(node, touchingIsIntersecting) {
1643
                assertRangeValid(this);
1644
                if (getRootContainer(node) != getRangeRoot(this)) {
1645
                    return false;
1646
                }
1647
 
1648
                var parent = node.parentNode, offset = getNodeIndex(node);
1649
                if (!parent) {
1650
                    return true;
1651
                }
1652
 
1653
                var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1654
                    endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1655
 
1656
                return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1657
            },
1658
 
1659
            isPointInRange: function(node, offset) {
1660
                assertRangeValid(this);
1661
                assertNode(node, "HIERARCHY_REQUEST_ERR");
1662
                assertSameDocumentOrFragment(node, this.startContainer);
1663
 
1664
                return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1665
                       (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1666
            },
1667
 
1668
            // The methods below are non-standard and invented by me.
1669
 
1670
            // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1671
            intersectsRange: function(range) {
1672
                return rangesIntersect(this, range, false);
1673
            },
1674
 
1675
            // Sharing a boundary start-to-end or end-to-start does count as intersection.
1676
            intersectsOrTouchesRange: function(range) {
1677
                return rangesIntersect(this, range, true);
1678
            },
1679
 
1680
            intersection: function(range) {
1681
                if (this.intersectsRange(range)) {
1682
                    var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1683
                        endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1684
 
1685
                    var intersectionRange = this.cloneRange();
1686
                    if (startComparison == -1) {
1687
                        intersectionRange.setStart(range.startContainer, range.startOffset);
1688
                    }
1689
                    if (endComparison == 1) {
1690
                        intersectionRange.setEnd(range.endContainer, range.endOffset);
1691
                    }
1692
                    return intersectionRange;
1693
                }
1694
                return null;
1695
            },
1696
 
1697
            union: function(range) {
1698
                if (this.intersectsOrTouchesRange(range)) {
1699
                    var unionRange = this.cloneRange();
1700
                    if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1701
                        unionRange.setStart(range.startContainer, range.startOffset);
1702
                    }
1703
                    if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1704
                        unionRange.setEnd(range.endContainer, range.endOffset);
1705
                    }
1706
                    return unionRange;
1707
                } else {
1708
                    throw new DOMException("Ranges do not intersect");
1709
                }
1710
            },
1711
 
1712
            containsNode: function(node, allowPartial) {
1713
                if (allowPartial) {
1714
                    return this.intersectsNode(node, false);
1715
                } else {
1716
                    return this.compareNode(node) == n_i;
1717
                }
1718
            },
1719
 
1720
            containsNodeContents: function(node) {
1721
                return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1722
            },
1723
 
1724
            containsRange: function(range) {
1725
                var intersection = this.intersection(range);
1726
                return intersection !== null && range.equals(intersection);
1727
            },
1728
 
1729
            containsNodeText: function(node) {
1730
                var nodeRange = this.cloneRange();
1731
                nodeRange.selectNode(node);
1732
                var textNodes = nodeRange.getNodes([3]);
1733
                if (textNodes.length > 0) {
1734
                    nodeRange.setStart(textNodes[0], 0);
1735
                    var lastTextNode = textNodes.pop();
1736
                    nodeRange.setEnd(lastTextNode, lastTextNode.length);
1737
                    return this.containsRange(nodeRange);
1738
                } else {
1739
                    return this.containsNodeContents(node);
1740
                }
1741
            },
1742
 
1743
            getNodes: function(nodeTypes, filter) {
1744
                assertRangeValid(this);
1745
                return getNodesInRange(this, nodeTypes, filter);
1746
            },
1747
 
1748
            getDocument: function() {
1749
                return getRangeDocument(this);
1750
            },
1751
 
1752
            collapseBefore: function(node) {
1753
                this.setEndBefore(node);
1754
                this.collapse(false);
1755
            },
1756
 
1757
            collapseAfter: function(node) {
1758
                this.setStartAfter(node);
1759
                this.collapse(true);
1760
            },
1761
 
1762
            getBookmark: function(containerNode) {
1763
                var doc = getRangeDocument(this);
1764
                var preSelectionRange = api.createRange(doc);
1765
                containerNode = containerNode || dom.getBody(doc);
1766
                preSelectionRange.selectNodeContents(containerNode);
1767
                var range = this.intersection(preSelectionRange);
1768
                var start = 0, end = 0;
1769
                if (range) {
1770
                    preSelectionRange.setEnd(range.startContainer, range.startOffset);
1771
                    start = preSelectionRange.toString().length;
1772
                    end = start + range.toString().length;
1773
                }
1774
 
1775
                return {
1776
                    start: start,
1777
                    end: end,
1778
                    containerNode: containerNode
1779
                };
1780
            },
1781
 
1782
            moveToBookmark: function(bookmark) {
1783
                var containerNode = bookmark.containerNode;
1784
                var charIndex = 0;
1785
                this.setStart(containerNode, 0);
1786
                this.collapse(true);
1787
                var nodeStack = [containerNode], node, foundStart = false, stop = false;
1788
                var nextCharIndex, i, childNodes;
1789
 
1790
                while (!stop && (node = nodeStack.pop())) {
1791
                    if (node.nodeType == 3) {
1792
                        nextCharIndex = charIndex + node.length;
1793
                        if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1794
                            this.setStart(node, bookmark.start - charIndex);
1795
                            foundStart = true;
1796
                        }
1797
                        if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1798
                            this.setEnd(node, bookmark.end - charIndex);
1799
                            stop = true;
1800
                        }
1801
                        charIndex = nextCharIndex;
1802
                    } else {
1803
                        childNodes = node.childNodes;
1804
                        i = childNodes.length;
1805
                        while (i--) {
1806
                            nodeStack.push(childNodes[i]);
1807
                        }
1808
                    }
1809
                }
1810
            },
1811
 
1812
            getName: function() {
1813
                return "DomRange";
1814
            },
1815
 
1816
            equals: function(range) {
1817
                return Range.rangesEqual(this, range);
1818
            },
1819
 
1820
            isValid: function() {
1821
                return isRangeValid(this);
1822
            },
1823
 
1824
            inspect: function() {
1825
                return inspect(this);
1826
            },
1827
 
1828
            detach: function() {
1829
                // In DOM4, detach() is now a no-op.
1830
            }
1831
        });
1832
 
1833
        function copyComparisonConstantsToObject(obj) {
1834
            obj.START_TO_START = s2s;
1835
            obj.START_TO_END = s2e;
1836
            obj.END_TO_END = e2e;
1837
            obj.END_TO_START = e2s;
1838
 
1839
            obj.NODE_BEFORE = n_b;
1840
            obj.NODE_AFTER = n_a;
1841
            obj.NODE_BEFORE_AND_AFTER = n_b_a;
1842
            obj.NODE_INSIDE = n_i;
1843
        }
1844
 
1845
        function copyComparisonConstants(constructor) {
1846
            copyComparisonConstantsToObject(constructor);
1847
            copyComparisonConstantsToObject(constructor.prototype);
1848
        }
1849
 
1850
        function createRangeContentRemover(remover, boundaryUpdater) {
1851
            return function() {
1852
                assertRangeValid(this);
1853
 
1854
                var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1855
 
1856
                var iterator = new RangeIterator(this, true);
1857
 
1858
                // Work out where to position the range after content removal
1859
                var node, boundary;
1860
                if (sc !== root) {
1861
                    node = getClosestAncestorIn(sc, root, true);
1862
                    boundary = getBoundaryAfterNode(node);
1863
                    sc = boundary.node;
1864
                    so = boundary.offset;
1865
                }
1866
 
1867
                // Check none of the range is read-only
1868
                iterateSubtree(iterator, assertNodeNotReadOnly);
1869
 
1870
                iterator.reset();
1871
 
1872
                // Remove the content
1873
                var returnValue = remover(iterator);
1874
                iterator.detach();
1875
 
1876
                // Move to the new position
1877
                boundaryUpdater(this, sc, so, sc, so);
1878
 
1879
                return returnValue;
1880
            };
1881
        }
1882
 
1883
        function createPrototypeRange(constructor, boundaryUpdater) {
1884
            function createBeforeAfterNodeSetter(isBefore, isStart) {
1885
                return function(node) {
1886
                    assertValidNodeType(node, beforeAfterNodeTypes);
1887
                    assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1888
 
1889
                    var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1890
                    (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1891
                };
1892
            }
1893
 
1894
            function setRangeStart(range, node, offset) {
1895
                var ec = range.endContainer, eo = range.endOffset;
1896
                if (node !== range.startContainer || offset !== range.startOffset) {
1897
                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
1898
                    // is after the current end. In either case, collapse the range to the new position
1899
                    if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1900
                        ec = node;
1901
                        eo = offset;
1902
                    }
1903
                    boundaryUpdater(range, node, offset, ec, eo);
1904
                }
1905
            }
1906
 
1907
            function setRangeEnd(range, node, offset) {
1908
                var sc = range.startContainer, so = range.startOffset;
1909
                if (node !== range.endContainer || offset !== range.endOffset) {
1910
                    // Check the root containers of the range and the new boundary, and also check whether the new boundary
1911
                    // is after the current end. In either case, collapse the range to the new position
1912
                    if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1913
                        sc = node;
1914
                        so = offset;
1915
                    }
1916
                    boundaryUpdater(range, sc, so, node, offset);
1917
                }
1918
            }
1919
 
1920
            // Set up inheritance
1921
            var F = function() {};
1922
            F.prototype = api.rangePrototype;
1923
            constructor.prototype = new F();
1924
 
1925
            util.extend(constructor.prototype, {
1926
                setStart: function(node, offset) {
1927
                    assertNoDocTypeNotationEntityAncestor(node, true);
1928
                    assertValidOffset(node, offset);
1929
 
1930
                    setRangeStart(this, node, offset);
1931
                },
1932
 
1933
                setEnd: function(node, offset) {
1934
                    assertNoDocTypeNotationEntityAncestor(node, true);
1935
                    assertValidOffset(node, offset);
1936
 
1937
                    setRangeEnd(this, node, offset);
1938
                },
1939
 
1940
                /**
1941
                 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1942
                 * - Two parameters (node, offset) creates a collapsed range at that position
1943
                 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1944
                 *   startOffset and ending at endOffset
1945
                 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1946
                 *   startNode and ending at endOffset in endNode
1947
                 */
1948
                setStartAndEnd: function() {
1949
                    var args = arguments;
1950
                    var sc = args[0], so = args[1], ec = sc, eo = so;
1951
 
1952
                    switch (args.length) {
1953
                        case 3:
1954
                            eo = args[2];
1955
                            break;
1956
                        case 4:
1957
                            ec = args[2];
1958
                            eo = args[3];
1959
                            break;
1960
                    }
1961
 
1962
                    assertNoDocTypeNotationEntityAncestor(sc, true);
1963
                    assertValidOffset(sc, so);
1964
 
1965
                    assertNoDocTypeNotationEntityAncestor(ec, true);
1966
                    assertValidOffset(ec, eo);
1967
 
1968
                    boundaryUpdater(this, sc, so, ec, eo);
1969
                },
1970
 
1971
                setBoundary: function(node, offset, isStart) {
1972
                    this["set" + (isStart ? "Start" : "End")](node, offset);
1973
                },
1974
 
1975
                setStartBefore: createBeforeAfterNodeSetter(true, true),
1976
                setStartAfter: createBeforeAfterNodeSetter(false, true),
1977
                setEndBefore: createBeforeAfterNodeSetter(true, false),
1978
                setEndAfter: createBeforeAfterNodeSetter(false, false),
1979
 
1980
                collapse: function(isStart) {
1981
                    assertRangeValid(this);
1982
                    if (isStart) {
1983
                        boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1984
                    } else {
1985
                        boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1986
                    }
1987
                },
1988
 
1989
                selectNodeContents: function(node) {
1990
                    assertNoDocTypeNotationEntityAncestor(node, true);
1991
 
1992
                    boundaryUpdater(this, node, 0, node, getNodeLength(node));
1993
                },
1994
 
1995
                selectNode: function(node) {
1996
                    assertNoDocTypeNotationEntityAncestor(node, false);
1997
                    assertValidNodeType(node, beforeAfterNodeTypes);
1998
 
1999
                    var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2000
                    boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2001
                },
2002
 
2003
                extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2004
 
2005
                deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2006
 
2007
                canSurroundContents: function() {
2008
                    assertRangeValid(this);
2009
                    assertNodeNotReadOnly(this.startContainer);
2010
                    assertNodeNotReadOnly(this.endContainer);
2011
 
2012
                    // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2013
                    // no non-text nodes.
2014
                    var iterator = new RangeIterator(this, true);
2015
                    var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2016
                            (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2017
                    iterator.detach();
2018
                    return !boundariesInvalid;
2019
                },
2020
 
2021
                splitBoundaries: function() {
2022
                    splitRangeBoundaries(this);
2023
                },
2024
 
2025
                splitBoundariesPreservingPositions: function(positionsToPreserve) {
2026
                    splitRangeBoundaries(this, positionsToPreserve);
2027
                },
2028
 
2029
                normalizeBoundaries: function() {
2030
                    assertRangeValid(this);
2031
 
2032
                    var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2033
 
2034
                    var mergeForward = function(node) {
2035
                        var sibling = node.nextSibling;
2036
                        if (sibling && sibling.nodeType == node.nodeType) {
2037
                            ec = node;
2038
                            eo = node.length;
2039
                            node.appendData(sibling.data);
2040
                            removeNode(sibling);
2041
                        }
2042
                    };
2043
 
2044
                    var mergeBackward = function(node) {
2045
                        var sibling = node.previousSibling;
2046
                        if (sibling && sibling.nodeType == node.nodeType) {
2047
                            sc = node;
2048
                            var nodeLength = node.length;
2049
                            so = sibling.length;
2050
                            node.insertData(0, sibling.data);
2051
                            removeNode(sibling);
2052
                            if (sc == ec) {
2053
                                eo += so;
2054
                                ec = sc;
2055
                            } else if (ec == node.parentNode) {
2056
                                var nodeIndex = getNodeIndex(node);
2057
                                if (eo == nodeIndex) {
2058
                                    ec = node;
2059
                                    eo = nodeLength;
2060
                                } else if (eo > nodeIndex) {
2061
                                    eo--;
2062
                                }
2063
                            }
2064
                        }
2065
                    };
2066
 
2067
                    var normalizeStart = true;
2068
                    var sibling;
2069
 
2070
                    if (isCharacterDataNode(ec)) {
2071
                        if (eo == ec.length) {
2072
                            mergeForward(ec);
2073
                        } else if (eo == 0) {
2074
                            sibling = ec.previousSibling;
2075
                            if (sibling && sibling.nodeType == ec.nodeType) {
2076
                                eo = sibling.length;
2077
                                if (sc == ec) {
2078
                                    normalizeStart = false;
2079
                                }
2080
                                sibling.appendData(ec.data);
2081
                                removeNode(ec);
2082
                                ec = sibling;
2083
                            }
2084
                        }
2085
                    } else {
2086
                        if (eo > 0) {
2087
                            var endNode = ec.childNodes[eo - 1];
2088
                            if (endNode && isCharacterDataNode(endNode)) {
2089
                                mergeForward(endNode);
2090
                            }
2091
                        }
2092
                        normalizeStart = !this.collapsed;
2093
                    }
2094
 
2095
                    if (normalizeStart) {
2096
                        if (isCharacterDataNode(sc)) {
2097
                            if (so == 0) {
2098
                                mergeBackward(sc);
2099
                            } else if (so == sc.length) {
2100
                                sibling = sc.nextSibling;
2101
                                if (sibling && sibling.nodeType == sc.nodeType) {
2102
                                    if (ec == sibling) {
2103
                                        ec = sc;
2104
                                        eo += sc.length;
2105
                                    }
2106
                                    sc.appendData(sibling.data);
2107
                                    removeNode(sibling);
2108
                                }
2109
                            }
2110
                        } else {
2111
                            if (so < sc.childNodes.length) {
2112
                                var startNode = sc.childNodes[so];
2113
                                if (startNode && isCharacterDataNode(startNode)) {
2114
                                    mergeBackward(startNode);
2115
                                }
2116
                            }
2117
                        }
2118
                    } else {
2119
                        sc = ec;
2120
                        so = eo;
2121
                    }
2122
 
2123
                    boundaryUpdater(this, sc, so, ec, eo);
2124
                },
2125
 
2126
                collapseToPoint: function(node, offset) {
2127
                    assertNoDocTypeNotationEntityAncestor(node, true);
2128
                    assertValidOffset(node, offset);
2129
                    this.setStartAndEnd(node, offset);
2130
                },
2131
 
2132
                parentElement: function() {
2133
                    assertRangeValid(this);
2134
                    var parentNode = this.commonAncestorContainer;
2135
                    return parentNode ? getElementAncestor(this.commonAncestorContainer, true) : null;
2136
                }
2137
            });
2138
 
2139
            copyComparisonConstants(constructor);
2140
        }
2141
 
2142
        /*----------------------------------------------------------------------------------------------------------------*/
2143
 
2144
        // Updates commonAncestorContainer and collapsed after boundary change
2145
        function updateCollapsedAndCommonAncestor(range) {
2146
            range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2147
            range.commonAncestorContainer = range.collapsed ?
2148
                range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2149
        }
2150
 
2151
        function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2152
            range.startContainer = startContainer;
2153
            range.startOffset = startOffset;
2154
            range.endContainer = endContainer;
2155
            range.endOffset = endOffset;
2156
            range.document = dom.getDocument(startContainer);
2157
            updateCollapsedAndCommonAncestor(range);
2158
        }
2159
 
2160
        function Range(doc) {
2161
            updateBoundaries(this, doc, 0, doc, 0);
2162
        }
2163
 
2164
        createPrototypeRange(Range, updateBoundaries);
2165
 
2166
        util.extend(Range, {
2167
            rangeProperties: rangeProperties,
2168
            RangeIterator: RangeIterator,
2169
            copyComparisonConstants: copyComparisonConstants,
2170
            createPrototypeRange: createPrototypeRange,
2171
            inspect: inspect,
2172
            toHtml: rangeToHtml,
2173
            getRangeDocument: getRangeDocument,
2174
            rangesEqual: function(r1, r2) {
2175
                return r1.startContainer === r2.startContainer &&
2176
                    r1.startOffset === r2.startOffset &&
2177
                    r1.endContainer === r2.endContainer &&
2178
                    r1.endOffset === r2.endOffset;
2179
            }
2180
        });
2181
 
2182
        api.DomRange = Range;
2183
    });
2184
 
2185
    /*----------------------------------------------------------------------------------------------------------------*/
2186
 
2187
    // Wrappers for the browser's native DOM Range and/or TextRange implementation
2188
    api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2189
        var WrappedRange, WrappedTextRange;
2190
        var dom = api.dom;
2191
        var util = api.util;
2192
        var DomPosition = dom.DomPosition;
2193
        var DomRange = api.DomRange;
2194
        var getBody = dom.getBody;
2195
        var getContentDocument = dom.getContentDocument;
2196
        var isCharacterDataNode = dom.isCharacterDataNode;
2197
 
2198
 
2199
        /*----------------------------------------------------------------------------------------------------------------*/
2200
 
2201
        if (api.features.implementsDomRange) {
2202
            // This is a wrapper around the browser's native DOM Range. It has two aims:
2203
            // - Provide workarounds for specific browser bugs
2204
            // - provide convenient extensions, which are inherited from Rangy's DomRange
2205
 
2206
            (function() {
2207
                var rangeProto;
2208
                var rangeProperties = DomRange.rangeProperties;
2209
 
2210
                function updateRangeProperties(range) {
2211
                    var i = rangeProperties.length, prop;
2212
                    while (i--) {
2213
                        prop = rangeProperties[i];
2214
                        range[prop] = range.nativeRange[prop];
2215
                    }
2216
                    // Fix for broken collapsed property in IE 9.
2217
                    range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2218
                }
2219
 
2220
                function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2221
                    var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2222
                    var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2223
                    var nativeRangeDifferent = !range.equals(range.nativeRange);
2224
 
2225
                    // Always set both boundaries for the benefit of IE9 (see issue 35)
2226
                    if (startMoved || endMoved || nativeRangeDifferent) {
2227
                        range.setEnd(endContainer, endOffset);
2228
                        range.setStart(startContainer, startOffset);
2229
                    }
2230
                }
2231
 
2232
                var createBeforeAfterNodeSetter;
2233
 
2234
                WrappedRange = function(range) {
2235
                    if (!range) {
2236
                        throw module.createError("WrappedRange: Range must be specified");
2237
                    }
2238
                    this.nativeRange = range;
2239
                    updateRangeProperties(this);
2240
                };
2241
 
2242
                DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2243
 
2244
                rangeProto = WrappedRange.prototype;
2245
 
2246
                rangeProto.selectNode = function(node) {
2247
                    this.nativeRange.selectNode(node);
2248
                    updateRangeProperties(this);
2249
                };
2250
 
2251
                rangeProto.cloneContents = function() {
2252
                    return this.nativeRange.cloneContents();
2253
                };
2254
 
2255
                // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2256
                // insertNode() is never delegated to the native range.
2257
 
2258
                rangeProto.surroundContents = function(node) {
2259
                    this.nativeRange.surroundContents(node);
2260
                    updateRangeProperties(this);
2261
                };
2262
 
2263
                rangeProto.collapse = function(isStart) {
2264
                    this.nativeRange.collapse(isStart);
2265
                    updateRangeProperties(this);
2266
                };
2267
 
2268
                rangeProto.cloneRange = function() {
2269
                    return new WrappedRange(this.nativeRange.cloneRange());
2270
                };
2271
 
2272
                rangeProto.refresh = function() {
2273
                    updateRangeProperties(this);
2274
                };
2275
 
2276
                rangeProto.toString = function() {
2277
                    return this.nativeRange.toString();
2278
                };
2279
 
2280
                // Create test range and node for feature detection
2281
 
2282
                var testTextNode = document.createTextNode("test");
2283
                getBody(document).appendChild(testTextNode);
2284
                var range = document.createRange();
2285
 
2286
                /*--------------------------------------------------------------------------------------------------------*/
2287
 
2288
                // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2289
                // correct for it
2290
 
2291
                range.setStart(testTextNode, 0);
2292
                range.setEnd(testTextNode, 0);
2293
 
2294
                try {
2295
                    range.setStart(testTextNode, 1);
2296
 
2297
                    rangeProto.setStart = function(node, offset) {
2298
                        this.nativeRange.setStart(node, offset);
2299
                        updateRangeProperties(this);
2300
                    };
2301
 
2302
                    rangeProto.setEnd = function(node, offset) {
2303
                        this.nativeRange.setEnd(node, offset);
2304
                        updateRangeProperties(this);
2305
                    };
2306
 
2307
                    createBeforeAfterNodeSetter = function(name) {
2308
                        return function(node) {
2309
                            this.nativeRange[name](node);
2310
                            updateRangeProperties(this);
2311
                        };
2312
                    };
2313
 
2314
                } catch(ex) {
2315
 
2316
                    rangeProto.setStart = function(node, offset) {
2317
                        try {
2318
                            this.nativeRange.setStart(node, offset);
2319
                        } catch (ex) {
2320
                            this.nativeRange.setEnd(node, offset);
2321
                            this.nativeRange.setStart(node, offset);
2322
                        }
2323
                        updateRangeProperties(this);
2324
                    };
2325
 
2326
                    rangeProto.setEnd = function(node, offset) {
2327
                        try {
2328
                            this.nativeRange.setEnd(node, offset);
2329
                        } catch (ex) {
2330
                            this.nativeRange.setStart(node, offset);
2331
                            this.nativeRange.setEnd(node, offset);
2332
                        }
2333
                        updateRangeProperties(this);
2334
                    };
2335
 
2336
                    createBeforeAfterNodeSetter = function(name, oppositeName) {
2337
                        return function(node) {
2338
                            try {
2339
                                this.nativeRange[name](node);
2340
                            } catch (ex) {
2341
                                this.nativeRange[oppositeName](node);
2342
                                this.nativeRange[name](node);
2343
                            }
2344
                            updateRangeProperties(this);
2345
                        };
2346
                    };
2347
                }
2348
 
2349
                rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2350
                rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2351
                rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2352
                rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2353
 
2354
                /*--------------------------------------------------------------------------------------------------------*/
2355
 
2356
                // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2357
                // whether the native implementation can be trusted
2358
                rangeProto.selectNodeContents = function(node) {
2359
                    this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2360
                };
2361
 
2362
                /*--------------------------------------------------------------------------------------------------------*/
2363
 
2364
                // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2365
                // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2366
 
2367
                range.selectNodeContents(testTextNode);
2368
                range.setEnd(testTextNode, 3);
2369
 
2370
                var range2 = document.createRange();
2371
                range2.selectNodeContents(testTextNode);
2372
                range2.setEnd(testTextNode, 4);
2373
                range2.setStart(testTextNode, 2);
2374
 
2375
                if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2376
                        range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2377
                    // This is the wrong way round, so correct for it
2378
 
2379
                    rangeProto.compareBoundaryPoints = function(type, range) {
2380
                        range = range.nativeRange || range;
2381
                        if (type == range.START_TO_END) {
2382
                            type = range.END_TO_START;
2383
                        } else if (type == range.END_TO_START) {
2384
                            type = range.START_TO_END;
2385
                        }
2386
                        return this.nativeRange.compareBoundaryPoints(type, range);
2387
                    };
2388
                } else {
2389
                    rangeProto.compareBoundaryPoints = function(type, range) {
2390
                        return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2391
                    };
2392
                }
2393
 
2394
                /*--------------------------------------------------------------------------------------------------------*/
2395
 
2396
                // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2397
 
2398
                var el = document.createElement("div");
2399
                el.innerHTML = "123";
2400
                var textNode = el.firstChild;
2401
                var body = getBody(document);
2402
                body.appendChild(el);
2403
 
2404
                range.setStart(textNode, 1);
2405
                range.setEnd(textNode, 2);
2406
                range.deleteContents();
2407
 
2408
                if (textNode.data == "13") {
2409
                    // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2410
                    // extractContents()
2411
                    rangeProto.deleteContents = function() {
2412
                        this.nativeRange.deleteContents();
2413
                        updateRangeProperties(this);
2414
                    };
2415
 
2416
                    rangeProto.extractContents = function() {
2417
                        var frag = this.nativeRange.extractContents();
2418
                        updateRangeProperties(this);
2419
                        return frag;
2420
                    };
2421
                } else {
2422
                }
2423
 
2424
                body.removeChild(el);
2425
                body = null;
2426
 
2427
                /*--------------------------------------------------------------------------------------------------------*/
2428
 
2429
                // Test for existence of createContextualFragment and delegate to it if it exists
2430
                if (util.isHostMethod(range, "createContextualFragment")) {
2431
                    rangeProto.createContextualFragment = function(fragmentStr) {
2432
                        return this.nativeRange.createContextualFragment(fragmentStr);
2433
                    };
2434
                }
2435
 
2436
                /*--------------------------------------------------------------------------------------------------------*/
2437
 
2438
                // Clean up
2439
                getBody(document).removeChild(testTextNode);
2440
 
2441
                rangeProto.getName = function() {
2442
                    return "WrappedRange";
2443
                };
2444
 
2445
                api.WrappedRange = WrappedRange;
2446
 
2447
                api.createNativeRange = function(doc) {
2448
                    doc = getContentDocument(doc, module, "createNativeRange");
2449
                    return doc.createRange();
2450
                };
2451
            })();
2452
        }
2453
 
2454
        if (api.features.implementsTextRange) {
2455
            /*
2456
            This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2457
            method. For example, in the following (where pipes denote the selection boundaries):
2458
 
2459
            <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2460
 
2461
            var range = document.selection.createRange();
2462
            alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2463
 
2464
            This method returns the common ancestor node of the following:
2465
            - the parentElement() of the textRange
2466
            - the parentElement() of the textRange after calling collapse(true)
2467
            - the parentElement() of the textRange after calling collapse(false)
2468
            */
2469
            var getTextRangeContainerElement = function(textRange) {
2470
                var parentEl = textRange.parentElement();
2471
                var range = textRange.duplicate();
2472
                range.collapse(true);
2473
                var startEl = range.parentElement();
2474
                range = textRange.duplicate();
2475
                range.collapse(false);
2476
                var endEl = range.parentElement();
2477
                var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2478
 
2479
                return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2480
            };
2481
 
2482
            var textRangeIsCollapsed = function(textRange) {
2483
                return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2484
            };
2485
 
2486
            // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2487
            // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2488
            // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2489
            // bugs, handling for inputs and images, plus optimizations.
2490
            var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2491
                var workingRange = textRange.duplicate();
2492
                workingRange.collapse(isStart);
2493
                var containerElement = workingRange.parentElement();
2494
 
2495
                // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2496
                // check for that
2497
                if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2498
                    containerElement = wholeRangeContainerElement;
2499
                }
2500
 
2501
 
2502
                // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2503
                // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2504
                if (!containerElement.canHaveHTML) {
2505
                    var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2506
                    return {
2507
                        boundaryPosition: pos,
2508
                        nodeInfo: {
2509
                            nodeIndex: pos.offset,
2510
                            containerElement: pos.node
2511
                        }
2512
                    };
2513
                }
2514
 
2515
                var workingNode = dom.getDocument(containerElement).createElement("span");
2516
 
2517
                // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2518
                // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2519
                if (workingNode.parentNode) {
2520
                    dom.removeNode(workingNode);
2521
                }
2522
 
2523
                var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2524
                var previousNode, nextNode, boundaryPosition, boundaryNode;
2525
                var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2526
                var childNodeCount = containerElement.childNodes.length;
2527
                var end = childNodeCount;
2528
 
2529
                // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2530
                // after the range boundary.
2531
                var nodeIndex = end;
2532
 
2533
                while (true) {
2534
                    if (nodeIndex == childNodeCount) {
2535
                        containerElement.appendChild(workingNode);
2536
                    } else {
2537
                        containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2538
                    }
2539
                    workingRange.moveToElementText(workingNode);
2540
                    comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2541
                    if (comparison == 0 || start == end) {
2542
                        break;
2543
                    } else if (comparison == -1) {
2544
                        if (end == start + 1) {
2545
                            // We know the endth child node is after the range boundary, so we must be done.
2546
                            break;
2547
                        } else {
2548
                            start = nodeIndex;
2549
                        }
2550
                    } else {
2551
                        end = (end == start + 1) ? start : nodeIndex;
2552
                    }
2553
                    nodeIndex = Math.floor((start + end) / 2);
2554
                    containerElement.removeChild(workingNode);
2555
                }
2556
 
2557
 
2558
                // We've now reached or gone past the boundary of the text range we're interested in
2559
                // so have identified the node we want
2560
                boundaryNode = workingNode.nextSibling;
2561
 
2562
                if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2563
                    // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2564
                    // the node containing the text range's boundary, so we move the end of the working range to the
2565
                    // boundary point and measure the length of its text to get the boundary's offset within the node.
2566
                    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2567
 
2568
                    var offset;
2569
 
2570
                    if (/[\r\n]/.test(boundaryNode.data)) {
2571
                        /*
2572
                        For the particular case of a boundary within a text node containing rendered line breaks (within a
2573
                        <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2574
                        IE. The facts:
2575
 
2576
                        - Each line break is represented as \r in the text node's data/nodeValue properties
2577
                        - Each line break is represented as \r\n in the TextRange's 'text' property
2578
                        - The 'text' property of the TextRange does not contain trailing line breaks
2579
 
2580
                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
2581
                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2582
                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
2583
                        to use this to store the characters moved when moving both the start and end of the range to the
2584
                        start of the document body and subtracting the start offset from the end offset (the
2585
                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2586
                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2587
                        the end of the document) has the same problem.
2588
 
2589
                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
2590
                        end boundary one character at a time and incrementing a counter with the value returned by the
2591
                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2592
                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2593
                        by the location of the range within the document).
2594
 
2595
                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
2596
                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2597
                        be longer than the text of the TextRange, so the start of the range is moved that length initially
2598
                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2599
                        property. This has good performance in most situations compared to the previous two methods.
2600
                        */
2601
                        var tempRange = workingRange.duplicate();
2602
                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2603
 
2604
                        offset = tempRange.moveStart("character", rangeLength);
2605
                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2606
                            offset++;
2607
                            tempRange.moveStart("character", 1);
2608
                        }
2609
                    } else {
2610
                        offset = workingRange.text.length;
2611
                    }
2612
                    boundaryPosition = new DomPosition(boundaryNode, offset);
2613
                } else {
2614
 
2615
                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2616
                    // a position within that, and likewise for a start boundary preceding a character data node
2617
                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2618
                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2619
                    if (nextNode && isCharacterDataNode(nextNode)) {
2620
                        boundaryPosition = new DomPosition(nextNode, 0);
2621
                    } else if (previousNode && isCharacterDataNode(previousNode)) {
2622
                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2623
                    } else {
2624
                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2625
                    }
2626
                }
2627
 
2628
                // Clean up
2629
                dom.removeNode(workingNode);
2630
 
2631
                return {
2632
                    boundaryPosition: boundaryPosition,
2633
                    nodeInfo: {
2634
                        nodeIndex: nodeIndex,
2635
                        containerElement: containerElement
2636
                    }
2637
                };
2638
            };
2639
 
2640
            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2641
            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2642
            // (http://code.google.com/p/ierange/)
2643
            var createBoundaryTextRange = function(boundaryPosition, isStart) {
2644
                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2645
                var doc = dom.getDocument(boundaryPosition.node);
2646
                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2647
                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2648
 
2649
                if (nodeIsDataNode) {
2650
                    boundaryNode = boundaryPosition.node;
2651
                    boundaryParent = boundaryNode.parentNode;
2652
                } else {
2653
                    childNodes = boundaryPosition.node.childNodes;
2654
                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2655
                    boundaryParent = boundaryPosition.node;
2656
                }
2657
 
2658
                // Position the range immediately before the node containing the boundary
2659
                workingNode = doc.createElement("span");
2660
 
2661
                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2662
                // the element rather than immediately before or after it
2663
                workingNode.innerHTML = "&#feff;";
2664
 
2665
                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2666
                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2667
                if (boundaryNode) {
2668
                    boundaryParent.insertBefore(workingNode, boundaryNode);
2669
                } else {
2670
                    boundaryParent.appendChild(workingNode);
2671
                }
2672
 
2673
                workingRange.moveToElementText(workingNode);
2674
                workingRange.collapse(!isStart);
2675
 
2676
                // Clean up
2677
                boundaryParent.removeChild(workingNode);
2678
 
2679
                // Move the working range to the text offset, if required
2680
                if (nodeIsDataNode) {
2681
                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2682
                }
2683
 
2684
                return workingRange;
2685
            };
2686
 
2687
            /*------------------------------------------------------------------------------------------------------------*/
2688
 
2689
            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2690
            // prototype
2691
 
2692
            WrappedTextRange = function(textRange) {
2693
                this.textRange = textRange;
2694
                this.refresh();
2695
            };
2696
 
2697
            WrappedTextRange.prototype = new DomRange(document);
2698
 
2699
            WrappedTextRange.prototype.refresh = function() {
2700
                var start, end, startBoundary;
2701
 
2702
                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2703
                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2704
 
2705
                if (textRangeIsCollapsed(this.textRange)) {
2706
                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2707
                        true).boundaryPosition;
2708
                } else {
2709
                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2710
                    start = startBoundary.boundaryPosition;
2711
 
2712
                    // An optimization used here is that if the start and end boundaries have the same parent element, the
2713
                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2714
                    // the start boundary
2715
                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2716
                        startBoundary.nodeInfo).boundaryPosition;
2717
                }
2718
 
2719
                this.setStart(start.node, start.offset);
2720
                this.setEnd(end.node, end.offset);
2721
            };
2722
 
2723
            WrappedTextRange.prototype.getName = function() {
2724
                return "WrappedTextRange";
2725
            };
2726
 
2727
            DomRange.copyComparisonConstants(WrappedTextRange);
2728
 
2729
            var rangeToTextRange = function(range) {
2730
                if (range.collapsed) {
2731
                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2732
                } else {
2733
                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2734
                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2735
                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2736
                    textRange.setEndPoint("StartToStart", startRange);
2737
                    textRange.setEndPoint("EndToEnd", endRange);
2738
                    return textRange;
2739
                }
2740
            };
2741
 
2742
            WrappedTextRange.rangeToTextRange = rangeToTextRange;
2743
 
2744
            WrappedTextRange.prototype.toTextRange = function() {
2745
                return rangeToTextRange(this);
2746
            };
2747
 
2748
            api.WrappedTextRange = WrappedTextRange;
2749
 
2750
            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2751
            // implementation to use by default.
2752
            if (!api.features.implementsDomRange || api.config.preferTextRange) {
2753
                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2754
                var globalObj = (function(f) { return f("return this;")(); })(Function);
2755
                if (typeof globalObj.Range == "undefined") {
2756
                    globalObj.Range = WrappedTextRange;
2757
                }
2758
 
2759
                api.createNativeRange = function(doc) {
2760
                    doc = getContentDocument(doc, module, "createNativeRange");
2761
                    return getBody(doc).createTextRange();
2762
                };
2763
 
2764
                api.WrappedRange = WrappedTextRange;
2765
            }
2766
        }
2767
 
2768
        api.createRange = function(doc) {
2769
            doc = getContentDocument(doc, module, "createRange");
2770
            return new api.WrappedRange(api.createNativeRange(doc));
2771
        };
2772
 
2773
        api.createRangyRange = function(doc) {
2774
            doc = getContentDocument(doc, module, "createRangyRange");
2775
            return new DomRange(doc);
2776
        };
2777
 
2778
        util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
2779
        util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
2780
 
2781
        api.addShimListener(function(win) {
2782
            var doc = win.document;
2783
            if (typeof doc.createRange == "undefined") {
2784
                doc.createRange = function() {
2785
                    return api.createRange(doc);
2786
                };
2787
            }
2788
            doc = win = null;
2789
        });
2790
    });
2791
 
2792
    /*----------------------------------------------------------------------------------------------------------------*/
2793
 
2794
    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2795
    // in the W3C Selection API spec (https://www.w3.org/TR/selection-api)
2796
    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2797
        api.config.checkSelectionRanges = true;
2798
 
2799
        var BOOLEAN = "boolean";
2800
        var NUMBER = "number";
2801
        var dom = api.dom;
2802
        var util = api.util;
2803
        var isHostMethod = util.isHostMethod;
2804
        var DomRange = api.DomRange;
2805
        var WrappedRange = api.WrappedRange;
2806
        var DOMException = api.DOMException;
2807
        var DomPosition = dom.DomPosition;
2808
        var getNativeSelection;
2809
        var selectionIsCollapsed;
2810
        var features = api.features;
2811
        var CONTROL = "Control";
2812
        var getDocument = dom.getDocument;
2813
        var getBody = dom.getBody;
2814
        var rangesEqual = DomRange.rangesEqual;
2815
 
2816
 
2817
        // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
2818
        // "forward" or "forwards") or a Boolean (true for backwards).
2819
        function isDirectionBackward(dir) {
2820
            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2821
        }
2822
 
2823
        function getWindow(win, methodName) {
2824
            if (!win) {
2825
                return window;
2826
            } else if (dom.isWindow(win)) {
2827
                return win;
2828
            } else if (win instanceof WrappedSelection) {
2829
                return win.win;
2830
            } else {
2831
                var doc = dom.getContentDocument(win, module, methodName);
2832
                return dom.getWindow(doc);
2833
            }
2834
        }
2835
 
2836
        function getWinSelection(winParam) {
2837
            return getWindow(winParam, "getWinSelection").getSelection();
2838
        }
2839
 
2840
        function getDocSelection(winParam) {
2841
            return getWindow(winParam, "getDocSelection").document.selection;
2842
        }
2843
 
2844
        function winSelectionIsBackward(sel) {
2845
            var backward = false;
2846
            if (sel.anchorNode) {
2847
                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2848
            }
2849
            return backward;
2850
        }
2851
 
2852
        // Test for the Range/TextRange and Selection features required
2853
        // Test for ability to retrieve selection
2854
        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2855
            implementsDocSelection = util.isHostObject(document, "selection");
2856
 
2857
        features.implementsWinGetSelection = implementsWinGetSelection;
2858
        features.implementsDocSelection = implementsDocSelection;
2859
 
2860
        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2861
 
2862
        if (useDocumentSelection) {
2863
            getNativeSelection = getDocSelection;
2864
            api.isSelectionValid = function(winParam) {
2865
                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2866
 
2867
                // Check whether the selection TextRange is actually contained within the correct document
2868
                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2869
            };
2870
        } else if (implementsWinGetSelection) {
2871
            getNativeSelection = getWinSelection;
2872
            api.isSelectionValid = function() {
2873
                return true;
2874
            };
2875
        } else {
2876
            module.fail("Neither document.selection or window.getSelection() detected.");
2877
            return false;
2878
        }
2879
 
2880
        api.getNativeSelection = getNativeSelection;
2881
 
2882
        var testSelection = getNativeSelection();
2883
 
2884
        // In Firefox, the selection is null in an iframe with display: none. See issue #138.
2885
        if (!testSelection) {
2886
            module.fail("Native selection was null (possibly issue 138?)");
2887
            return false;
2888
        }
2889
 
2890
        var testRange = api.createNativeRange(document);
2891
        var body = getBody(document);
2892
 
2893
        // Obtaining a range from a selection
2894
        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2895
            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2896
 
2897
        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2898
 
2899
        // Test for existence of native selection extend() method
2900
        var selectionHasExtend = isHostMethod(testSelection, "extend");
2901
        features.selectionHasExtend = selectionHasExtend;
2902
 
2903
        // Test for existence of native selection setBaseAndExtent() method
2904
        var selectionHasSetBaseAndExtent = isHostMethod(testSelection, "setBaseAndExtent");
2905
        features.selectionHasSetBaseAndExtent = selectionHasSetBaseAndExtent;
2906
 
2907
        // Test if rangeCount exists
2908
        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2909
        features.selectionHasRangeCount = selectionHasRangeCount;
2910
 
2911
        var selectionSupportsMultipleRanges = false;
2912
        var collapsedNonEditableSelectionsSupported = true;
2913
 
2914
        var addRangeBackwardToNative = selectionHasExtend ?
2915
            function(nativeSelection, range) {
2916
                var doc = DomRange.getRangeDocument(range);
2917
                var endRange = api.createRange(doc);
2918
                endRange.collapseToPoint(range.endContainer, range.endOffset);
2919
                nativeSelection.addRange(getNativeRange(endRange));
2920
                nativeSelection.extend(range.startContainer, range.startOffset);
2921
            } : null;
2922
 
2923
        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2924
                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2925
 
2926
            (function() {
2927
                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2928
                // performed on the current document's selection. See issue 109.
2929
 
2930
                // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
2931
                // will result in the selection direction being reversed if the original selection was backwards and the
2932
                // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
2933
                var sel = window.getSelection();
2934
                if (sel) {
2935
                    // Store the current selection
2936
                    var originalSelectionRangeCount = sel.rangeCount;
2937
                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2938
                    var originalSelectionRanges = [];
2939
                    var originalSelectionBackward = winSelectionIsBackward(sel);
2940
                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
2941
                        originalSelectionRanges[i] = sel.getRangeAt(i);
2942
                    }
2943
 
2944
                    // Create some test elements
2945
                    var testEl = dom.createTestElement(document, "", false);
2946
                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2947
 
2948
                    // Test whether the native selection will allow a collapsed selection within a non-editable element
2949
                    var r1 = document.createRange();
2950
 
2951
                    r1.setStart(textNode, 1);
2952
                    r1.collapse(true);
2953
                    sel.removeAllRanges();
2954
                    sel.addRange(r1);
2955
                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2956
                    sel.removeAllRanges();
2957
 
2958
                    // Test whether the native selection is capable of supporting multiple ranges.
2959
                    if (!selectionHasMultipleRanges) {
2960
                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2961
                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2962
                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
2963
                        // sniff. I'm not happy about it. See
2964
                        // https://code.google.com/p/chromium/issues/detail?id=399791
2965
                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2966
                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2967
                            selectionSupportsMultipleRanges = false;
2968
                        } else {
2969
                            var r2 = r1.cloneRange();
2970
                            r1.setStart(textNode, 0);
2971
                            r2.setEnd(textNode, 3);
2972
                            r2.setStart(textNode, 2);
2973
                            sel.addRange(r1);
2974
                            sel.addRange(r2);
2975
                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2976
                        }
2977
                    }
2978
 
2979
                    // Clean up
2980
                    dom.removeNode(testEl);
2981
                    sel.removeAllRanges();
2982
 
2983
                    for (i = 0; i < originalSelectionRangeCount; ++i) {
2984
                        if (i == 0 && originalSelectionBackward) {
2985
                            if (addRangeBackwardToNative) {
2986
                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2987
                            } else {
2988
                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2989
                                sel.addRange(originalSelectionRanges[i]);
2990
                            }
2991
                        } else {
2992
                            sel.addRange(originalSelectionRanges[i]);
2993
                        }
2994
                    }
2995
                }
2996
            })();
2997
        }
2998
 
2999
        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
3000
        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
3001
 
3002
        // ControlRanges
3003
        var implementsControlRange = false, testControlRange;
3004
 
3005
        if (body && isHostMethod(body, "createControlRange")) {
3006
            testControlRange = body.createControlRange();
3007
            if (util.areHostProperties(testControlRange, ["item", "add"])) {
3008
                implementsControlRange = true;
3009
            }
3010
        }
3011
        features.implementsControlRange = implementsControlRange;
3012
 
3013
        // Selection collapsedness
3014
        if (selectionHasAnchorAndFocus) {
3015
            selectionIsCollapsed = function(sel) {
3016
                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3017
            };
3018
        } else {
3019
            selectionIsCollapsed = function(sel) {
3020
                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3021
            };
3022
        }
3023
 
3024
        function updateAnchorAndFocusFromRange(sel, range, backward) {
3025
            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3026
            sel.anchorNode = range[anchorPrefix + "Container"];
3027
            sel.anchorOffset = range[anchorPrefix + "Offset"];
3028
            sel.focusNode = range[focusPrefix + "Container"];
3029
            sel.focusOffset = range[focusPrefix + "Offset"];
3030
        }
3031
 
3032
        function updateAnchorAndFocusFromNativeSelection(sel) {
3033
            var nativeSel = sel.nativeSelection;
3034
            sel.anchorNode = nativeSel.anchorNode;
3035
            sel.anchorOffset = nativeSel.anchorOffset;
3036
            sel.focusNode = nativeSel.focusNode;
3037
            sel.focusOffset = nativeSel.focusOffset;
3038
        }
3039
 
3040
        function updateEmptySelection(sel) {
3041
            sel.anchorNode = sel.focusNode = null;
3042
            sel.anchorOffset = sel.focusOffset = 0;
3043
            sel.rangeCount = 0;
3044
            sel.isCollapsed = true;
3045
            sel._ranges.length = 0;
3046
            updateType(sel);
3047
        }
3048
 
3049
        function updateType(sel) {
3050
            sel.type = (sel.rangeCount == 0) ? "None" : (selectionIsCollapsed(sel) ? "Caret" : "Range");
3051
        }
3052
 
3053
        function getNativeRange(range) {
3054
            var nativeRange;
3055
            if (range instanceof DomRange) {
3056
                nativeRange = api.createNativeRange(range.getDocument());
3057
                nativeRange.setEnd(range.endContainer, range.endOffset);
3058
                nativeRange.setStart(range.startContainer, range.startOffset);
3059
            } else if (range instanceof WrappedRange) {
3060
                nativeRange = range.nativeRange;
3061
            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3062
                nativeRange = range;
3063
            }
3064
            return nativeRange;
3065
        }
3066
 
3067
        function rangeContainsSingleElement(rangeNodes) {
3068
            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3069
                return false;
3070
            }
3071
            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3072
                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3073
                    return false;
3074
                }
3075
            }
3076
            return true;
3077
        }
3078
 
3079
        function getSingleElementFromRange(range) {
3080
            var nodes = range.getNodes();
3081
            if (!rangeContainsSingleElement(nodes)) {
3082
                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3083
            }
3084
            return nodes[0];
3085
        }
3086
 
3087
        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3088
        function isTextRange(range) {
3089
            return !!range && typeof range.text != "undefined";
3090
        }
3091
 
3092
        function updateFromTextRange(sel, range) {
3093
            // Create a Range from the selected TextRange
3094
            var wrappedRange = new WrappedRange(range);
3095
            sel._ranges = [wrappedRange];
3096
 
3097
            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3098
            sel.rangeCount = 1;
3099
            sel.isCollapsed = wrappedRange.collapsed;
3100
            updateType(sel);
3101
        }
3102
 
3103
        function updateControlSelection(sel) {
3104
            // Update the wrapped selection based on what's now in the native selection
3105
            sel._ranges.length = 0;
3106
            if (sel.docSelection.type == "None") {
3107
                updateEmptySelection(sel);
3108
            } else {
3109
                var controlRange = sel.docSelection.createRange();
3110
                if (isTextRange(controlRange)) {
3111
                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
3112
                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3113
                    // ControlRange have been removed from the ControlRange and removed from the document.
3114
                    updateFromTextRange(sel, controlRange);
3115
                } else {
3116
                    sel.rangeCount = controlRange.length;
3117
                    var range, doc = getDocument(controlRange.item(0));
3118
                    for (var i = 0; i < sel.rangeCount; ++i) {
3119
                        range = api.createRange(doc);
3120
                        range.selectNode(controlRange.item(i));
3121
                        sel._ranges.push(range);
3122
                    }
3123
                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3124
                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3125
                    updateType(sel);
3126
                }
3127
            }
3128
        }
3129
 
3130
        function addRangeToControlSelection(sel, range) {
3131
            var controlRange = sel.docSelection.createRange();
3132
            var rangeElement = getSingleElementFromRange(range);
3133
 
3134
            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3135
            // contained by the supplied range
3136
            var doc = getDocument(controlRange.item(0));
3137
            var newControlRange = getBody(doc).createControlRange();
3138
            for (var i = 0, len = controlRange.length; i < len; ++i) {
3139
                newControlRange.add(controlRange.item(i));
3140
            }
3141
            try {
3142
                newControlRange.add(rangeElement);
3143
            } catch (ex) {
3144
                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3145
            }
3146
            newControlRange.select();
3147
 
3148
            // Update the wrapped selection based on what's now in the native selection
3149
            updateControlSelection(sel);
3150
        }
3151
 
3152
        var getSelectionRangeAt;
3153
 
3154
        if (isHostMethod(testSelection, "getRangeAt")) {
3155
            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3156
            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3157
            // lesson to us all, especially me.
3158
            getSelectionRangeAt = function(sel, index) {
3159
                try {
3160
                    return sel.getRangeAt(index);
3161
                } catch (ex) {
3162
                    return null;
3163
                }
3164
            };
3165
        } else if (selectionHasAnchorAndFocus) {
3166
            getSelectionRangeAt = function(sel) {
3167
                var doc = getDocument(sel.anchorNode);
3168
                var range = api.createRange(doc);
3169
                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3170
 
3171
                // Handle the case when the selection was selected backwards (from the end to the start in the
3172
                // document)
3173
                if (range.collapsed !== this.isCollapsed) {
3174
                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3175
                }
3176
 
3177
                return range;
3178
            };
3179
        }
3180
 
3181
        function WrappedSelection(selection, docSelection, win) {
3182
            this.nativeSelection = selection;
3183
            this.docSelection = docSelection;
3184
            this._ranges = [];
3185
            this.win = win;
3186
            this.refresh();
3187
        }
3188
 
3189
        WrappedSelection.prototype = api.selectionPrototype;
3190
 
3191
        function deleteProperties(sel) {
3192
            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3193
            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3194
            sel.detached = true;
3195
            updateType(sel);
3196
        }
3197
 
3198
        var cachedRangySelections = [];
3199
 
3200
        function actOnCachedSelection(win, action) {
3201
            var i = cachedRangySelections.length, cached, sel;
3202
            while (i--) {
3203
                cached = cachedRangySelections[i];
3204
                sel = cached.selection;
3205
                if (action == "deleteAll") {
3206
                    deleteProperties(sel);
3207
                } else if (cached.win == win) {
3208
                    if (action == "delete") {
3209
                        cachedRangySelections.splice(i, 1);
3210
                        return true;
3211
                    } else {
3212
                        return sel;
3213
                    }
3214
                }
3215
            }
3216
            if (action == "deleteAll") {
3217
                cachedRangySelections.length = 0;
3218
            }
3219
            return null;
3220
        }
3221
 
3222
        var getSelection = function(win) {
3223
            // Check if the parameter is a Rangy Selection object
3224
            if (win && win instanceof WrappedSelection) {
3225
                win.refresh();
3226
                return win;
3227
            }
3228
 
3229
            win = getWindow(win, "getNativeSelection");
3230
 
3231
            var sel = actOnCachedSelection(win);
3232
            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3233
            if (sel) {
3234
                sel.nativeSelection = nativeSel;
3235
                sel.docSelection = docSel;
3236
                sel.refresh();
3237
            } else {
3238
                sel = new WrappedSelection(nativeSel, docSel, win);
3239
                cachedRangySelections.push( { win: win, selection: sel } );
3240
            }
3241
            return sel;
3242
        };
3243
 
3244
        api.getSelection = getSelection;
3245
 
3246
        util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3247
 
3248
        var selProto = WrappedSelection.prototype;
3249
 
3250
        function createControlSelection(sel, ranges) {
3251
            // Ensure that the selection becomes of type "Control"
3252
            var doc = getDocument(ranges[0].startContainer);
3253
            var controlRange = getBody(doc).createControlRange();
3254
            for (var i = 0, el, len = ranges.length; i < len; ++i) {
3255
                el = getSingleElementFromRange(ranges[i]);
3256
                try {
3257
                    controlRange.add(el);
3258
                } catch (ex) {
3259
                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3260
                }
3261
            }
3262
            controlRange.select();
3263
 
3264
            // Update the wrapped selection based on what's now in the native selection
3265
            updateControlSelection(sel);
3266
        }
3267
 
3268
        // Selecting a range
3269
        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3270
            selProto.removeAllRanges = function() {
3271
                this.nativeSelection.removeAllRanges();
3272
                updateEmptySelection(this);
3273
            };
3274
 
3275
            var addRangeBackward = function(sel, range) {
3276
                addRangeBackwardToNative(sel.nativeSelection, range);
3277
                sel.refresh();
3278
            };
3279
 
3280
            if (selectionHasRangeCount) {
3281
                selProto.addRange = function(range, direction) {
3282
                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3283
                        addRangeToControlSelection(this, range);
3284
                    } else {
3285
                        if (isDirectionBackward(direction) && selectionHasExtend) {
3286
                            addRangeBackward(this, range);
3287
                        } else {
3288
                            var previousRangeCount;
3289
                            if (selectionSupportsMultipleRanges) {
3290
                                previousRangeCount = this.rangeCount;
3291
                            } else {
3292
                                this.removeAllRanges();
3293
                                previousRangeCount = 0;
3294
                            }
3295
                            // Clone the native range so that changing the selected range does not affect the selection.
3296
                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3297
                            // issue 80.
3298
                            var clonedNativeRange = getNativeRange(range).cloneRange();
3299
                            try {
3300
                                this.nativeSelection.addRange(clonedNativeRange);
3301
                            } catch (ex) {
3302
                            }
3303
 
3304
                            // Check whether adding the range was successful
3305
                            this.rangeCount = this.nativeSelection.rangeCount;
3306
 
3307
                            if (this.rangeCount == previousRangeCount + 1) {
3308
                                // The range was added successfully
3309
 
3310
                                // Check whether the range that we added to the selection is reflected in the last range extracted from
3311
                                // the selection
3312
                                if (api.config.checkSelectionRanges) {
3313
                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3314
                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
3315
                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
3316
                                        range = new WrappedRange(nativeRange);
3317
                                    }
3318
                                }
3319
                                this._ranges[this.rangeCount - 1] = range;
3320
                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3321
                                this.isCollapsed = selectionIsCollapsed(this);
3322
                                updateType(this);
3323
                            } else {
3324
                                // The range was not added successfully. The simplest thing is to refresh
3325
                                this.refresh();
3326
                            }
3327
                        }
3328
                    }
3329
                };
3330
            } else {
3331
                selProto.addRange = function(range, direction) {
3332
                    if (isDirectionBackward(direction) && selectionHasExtend) {
3333
                        addRangeBackward(this, range);
3334
                    } else {
3335
                        this.nativeSelection.addRange(getNativeRange(range));
3336
                        this.refresh();
3337
                    }
3338
                };
3339
            }
3340
 
3341
            selProto.setRanges = function(ranges) {
3342
                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3343
                    createControlSelection(this, ranges);
3344
                } else {
3345
                    this.removeAllRanges();
3346
                    for (var i = 0, len = ranges.length; i < len; ++i) {
3347
                        this.addRange(ranges[i]);
3348
                    }
3349
                }
3350
            };
3351
        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3352
                   implementsControlRange && useDocumentSelection) {
3353
 
3354
            selProto.removeAllRanges = function() {
3355
                // Added try/catch as fix for issue #21
3356
                try {
3357
                    this.docSelection.empty();
3358
 
3359
                    // Check for empty() not working (issue #24)
3360
                    if (this.docSelection.type != "None") {
3361
                        // Work around failure to empty a control selection by instead selecting a TextRange and then
3362
                        // calling empty()
3363
                        var doc;
3364
                        if (this.anchorNode) {
3365
                            doc = getDocument(this.anchorNode);
3366
                        } else if (this.docSelection.type == CONTROL) {
3367
                            var controlRange = this.docSelection.createRange();
3368
                            if (controlRange.length) {
3369
                                doc = getDocument( controlRange.item(0) );
3370
                            }
3371
                        }
3372
                        if (doc) {
3373
                            var textRange = getBody(doc).createTextRange();
3374
                            textRange.select();
3375
                            this.docSelection.empty();
3376
                        }
3377
                    }
3378
                } catch(ex) {}
3379
                updateEmptySelection(this);
3380
            };
3381
 
3382
            selProto.addRange = function(range) {
3383
                if (this.docSelection.type == CONTROL) {
3384
                    addRangeToControlSelection(this, range);
3385
                } else {
3386
                    api.WrappedTextRange.rangeToTextRange(range).select();
3387
                    this._ranges[0] = range;
3388
                    this.rangeCount = 1;
3389
                    this.isCollapsed = this._ranges[0].collapsed;
3390
                    updateAnchorAndFocusFromRange(this, range, false);
3391
                    updateType(this);
3392
                }
3393
            };
3394
 
3395
            selProto.setRanges = function(ranges) {
3396
                this.removeAllRanges();
3397
                var rangeCount = ranges.length;
3398
                if (rangeCount > 1) {
3399
                    createControlSelection(this, ranges);
3400
                } else if (rangeCount) {
3401
                    this.addRange(ranges[0]);
3402
                }
3403
            };
3404
        } else {
3405
            module.fail("No means of selecting a Range or TextRange was found");
3406
            return false;
3407
        }
3408
 
3409
        selProto.getRangeAt = function(index) {
3410
            if (index < 0 || index >= this.rangeCount) {
3411
                throw new DOMException("INDEX_SIZE_ERR");
3412
            } else {
3413
                // Clone the range to preserve selection-range independence. See issue 80.
3414
                return this._ranges[index].cloneRange();
3415
            }
3416
        };
3417
 
3418
        var refreshSelection;
3419
 
3420
        if (useDocumentSelection) {
3421
            refreshSelection = function(sel) {
3422
                var range;
3423
                if (api.isSelectionValid(sel.win)) {
3424
                    range = sel.docSelection.createRange();
3425
                } else {
3426
                    range = getBody(sel.win.document).createTextRange();
3427
                    range.collapse(true);
3428
                }
3429
 
3430
                if (sel.docSelection.type == CONTROL) {
3431
                    updateControlSelection(sel);
3432
                } else if (isTextRange(range)) {
3433
                    updateFromTextRange(sel, range);
3434
                } else {
3435
                    updateEmptySelection(sel);
3436
                }
3437
            };
3438
        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3439
            refreshSelection = function(sel) {
3440
                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3441
                    updateControlSelection(sel);
3442
                } else {
3443
                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3444
                    if (sel.rangeCount) {
3445
                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3446
                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3447
                        }
3448
                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3449
                        sel.isCollapsed = selectionIsCollapsed(sel);
3450
                        updateType(sel);
3451
                    } else {
3452
                        updateEmptySelection(sel);
3453
                    }
3454
                }
3455
            };
3456
        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3457
            refreshSelection = function(sel) {
3458
                var range, nativeSel = sel.nativeSelection;
3459
                if (nativeSel.anchorNode) {
3460
                    range = getSelectionRangeAt(nativeSel, 0);
3461
                    sel._ranges = [range];
3462
                    sel.rangeCount = 1;
3463
                    updateAnchorAndFocusFromNativeSelection(sel);
3464
                    sel.isCollapsed = selectionIsCollapsed(sel);
3465
                    updateType(sel);
3466
                } else {
3467
                    updateEmptySelection(sel);
3468
                }
3469
            };
3470
        } else {
3471
            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3472
            return false;
3473
        }
3474
 
3475
        selProto.refresh = function(checkForChanges) {
3476
            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3477
            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3478
 
3479
            refreshSelection(this);
3480
            if (checkForChanges) {
3481
                // Check the range count first
3482
                var i = oldRanges.length;
3483
                if (i != this._ranges.length) {
3484
                    return true;
3485
                }
3486
 
3487
                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3488
                // ranges after this
3489
                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3490
                    return true;
3491
                }
3492
 
3493
                // Finally, compare each range in turn
3494
                while (i--) {
3495
                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3496
                        return true;
3497
                    }
3498
                }
3499
                return false;
3500
            }
3501
        };
3502
 
3503
        // Removal of a single range
3504
        var removeRangeManually = function(sel, range) {
3505
            var ranges = sel.getAllRanges();
3506
            sel.removeAllRanges();
3507
            for (var i = 0, len = ranges.length; i < len; ++i) {
3508
                if (!rangesEqual(range, ranges[i])) {
3509
                    sel.addRange(ranges[i]);
3510
                }
3511
            }
3512
            if (!sel.rangeCount) {
3513
                updateEmptySelection(sel);
3514
            }
3515
        };
3516
 
3517
        if (implementsControlRange && implementsDocSelection) {
3518
            selProto.removeRange = function(range) {
3519
                if (this.docSelection.type == CONTROL) {
3520
                    var controlRange = this.docSelection.createRange();
3521
                    var rangeElement = getSingleElementFromRange(range);
3522
 
3523
                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3524
                    // element contained by the supplied range
3525
                    var doc = getDocument(controlRange.item(0));
3526
                    var newControlRange = getBody(doc).createControlRange();
3527
                    var el, removed = false;
3528
                    for (var i = 0, len = controlRange.length; i < len; ++i) {
3529
                        el = controlRange.item(i);
3530
                        if (el !== rangeElement || removed) {
3531
                            newControlRange.add(controlRange.item(i));
3532
                        } else {
3533
                            removed = true;
3534
                        }
3535
                    }
3536
                    newControlRange.select();
3537
 
3538
                    // Update the wrapped selection based on what's now in the native selection
3539
                    updateControlSelection(this);
3540
                } else {
3541
                    removeRangeManually(this, range);
3542
                }
3543
            };
3544
        } else {
3545
            selProto.removeRange = function(range) {
3546
                removeRangeManually(this, range);
3547
            };
3548
        }
3549
 
3550
        // Detecting if a selection is backward
3551
        var selectionIsBackward;
3552
        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3553
            selectionIsBackward = winSelectionIsBackward;
3554
 
3555
            selProto.isBackward = function() {
3556
                return selectionIsBackward(this);
3557
            };
3558
        } else {
3559
            selectionIsBackward = selProto.isBackward = function() {
3560
                return false;
3561
            };
3562
        }
3563
 
3564
        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3565
        selProto.isBackwards = selProto.isBackward;
3566
 
3567
        // Selection stringifier
3568
        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3569
        // The current spec does not yet define this method.
3570
        selProto.toString = function() {
3571
            var rangeTexts = [];
3572
            for (var i = 0, len = this.rangeCount; i < len; ++i) {
3573
                rangeTexts[i] = "" + this._ranges[i];
3574
            }
3575
            return rangeTexts.join("");
3576
        };
3577
 
3578
        function assertNodeInSameDocument(sel, node) {
3579
            if (sel.win.document != getDocument(node)) {
3580
                throw new DOMException("WRONG_DOCUMENT_ERR");
3581
            }
3582
        }
3583
 
3584
        function assertValidOffset(node, offset) {
3585
            if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
3586
                throw new DOMException("INDEX_SIZE_ERR");
3587
            }
3588
        }
3589
 
3590
        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3591
        selProto.collapse = function(node, offset) {
3592
            assertNodeInSameDocument(this, node);
3593
            var range = api.createRange(node);
3594
            range.collapseToPoint(node, offset);
3595
            this.setSingleRange(range);
3596
            this.isCollapsed = true;
3597
        };
3598
 
3599
        selProto.collapseToStart = function() {
3600
            if (this.rangeCount) {
3601
                var range = this._ranges[0];
3602
                this.collapse(range.startContainer, range.startOffset);
3603
            } else {
3604
                throw new DOMException("INVALID_STATE_ERR");
3605
            }
3606
        };
3607
 
3608
        selProto.collapseToEnd = function() {
3609
            if (this.rangeCount) {
3610
                var range = this._ranges[this.rangeCount - 1];
3611
                this.collapse(range.endContainer, range.endOffset);
3612
            } else {
3613
                throw new DOMException("INVALID_STATE_ERR");
3614
            }
3615
        };
3616
 
3617
        // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
3618
        // specified so the native implementation is never used by Rangy.
3619
        selProto.selectAllChildren = function(node) {
3620
            assertNodeInSameDocument(this, node);
3621
            var range = api.createRange(node);
3622
            range.selectNodeContents(node);
3623
            this.setSingleRange(range);
3624
        };
3625
 
3626
        if (selectionHasSetBaseAndExtent) {
3627
            selProto.setBaseAndExtent = function(anchorNode, anchorOffset, focusNode, focusOffset) {
3628
                this.nativeSelection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
3629
                this.refresh();
3630
            };
3631
        } else if (selectionHasExtend) {
3632
            selProto.setBaseAndExtent = function(anchorNode, anchorOffset, focusNode, focusOffset) {
3633
                assertValidOffset(anchorNode, anchorOffset);
3634
                assertValidOffset(focusNode, focusOffset);
3635
                assertNodeInSameDocument(this, anchorNode);
3636
                assertNodeInSameDocument(this, focusNode);
3637
                var range = api.createRange(node);
3638
                var isBackwards = (dom.comparePoints(anchorNode, anchorOffset, focusNode, focusOffset) == -1);
3639
                if (isBackwards) {
3640
                    range.setStartAndEnd(focusNode, focusOffset, anchorNode, anchorOffset);
3641
                } else {
3642
                    range.setStartAndEnd(anchorNode, anchorOffset, focusNode, focusOffset);
3643
                }
3644
                this.setSingleRange(range, isBackwards);
3645
            };
3646
        }
3647
 
3648
        selProto.deleteFromDocument = function() {
3649
            // Sepcial behaviour required for IE's control selections
3650
            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3651
                var controlRange = this.docSelection.createRange();
3652
                var element;
3653
                while (controlRange.length) {
3654
                    element = controlRange.item(0);
3655
                    controlRange.remove(element);
3656
                    dom.removeNode(element);
3657
                }
3658
                this.refresh();
3659
            } else if (this.rangeCount) {
3660
                var ranges = this.getAllRanges();
3661
                if (ranges.length) {
3662
                    this.removeAllRanges();
3663
                    for (var i = 0, len = ranges.length; i < len; ++i) {
3664
                        ranges[i].deleteContents();
3665
                    }
3666
                    // The spec says nothing about what the selection should contain after calling deleteContents on each
3667
                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
3668
                    this.addRange(ranges[len - 1]);
3669
                }
3670
            }
3671
        };
3672
 
3673
        // The following are non-standard extensions
3674
        selProto.eachRange = function(func, returnValue) {
3675
            for (var i = 0, len = this._ranges.length; i < len; ++i) {
3676
                if ( func( this.getRangeAt(i) ) ) {
3677
                    return returnValue;
3678
                }
3679
            }
3680
        };
3681
 
3682
        selProto.getAllRanges = function() {
3683
            var ranges = [];
3684
            this.eachRange(function(range) {
3685
                ranges.push(range);
3686
            });
3687
            return ranges;
3688
        };
3689
 
3690
        selProto.setSingleRange = function(range, direction) {
3691
            this.removeAllRanges();
3692
            this.addRange(range, direction);
3693
        };
3694
 
3695
        selProto.callMethodOnEachRange = function(methodName, params) {
3696
            var results = [];
3697
            this.eachRange( function(range) {
3698
                results.push( range[methodName].apply(range, params || []) );
3699
            } );
3700
            return results;
3701
        };
3702
 
3703
        function createStartOrEndSetter(isStart) {
3704
            return function(node, offset) {
3705
                var range;
3706
                if (this.rangeCount) {
3707
                    range = this.getRangeAt(0);
3708
                    range["set" + (isStart ? "Start" : "End")](node, offset);
3709
                } else {
3710
                    range = api.createRange(this.win.document);
3711
                    range.setStartAndEnd(node, offset);
3712
                }
3713
                this.setSingleRange(range, this.isBackward());
3714
            };
3715
        }
3716
 
3717
        selProto.setStart = createStartOrEndSetter(true);
3718
        selProto.setEnd = createStartOrEndSetter(false);
3719
 
3720
        // Add select() method to Range prototype. Any existing selection will be removed.
3721
        api.rangePrototype.select = function(direction) {
3722
            getSelection( this.getDocument() ).setSingleRange(this, direction);
3723
        };
3724
 
3725
        selProto.changeEachRange = function(func) {
3726
            var ranges = [];
3727
            var backward = this.isBackward();
3728
 
3729
            this.eachRange(function(range) {
3730
                func(range);
3731
                ranges.push(range);
3732
            });
3733
 
3734
            this.removeAllRanges();
3735
            if (backward && ranges.length == 1) {
3736
                this.addRange(ranges[0], "backward");
3737
            } else {
3738
                this.setRanges(ranges);
3739
            }
3740
        };
3741
 
3742
        selProto.containsNode = function(node, allowPartial) {
3743
            return this.eachRange( function(range) {
3744
                return range.containsNode(node, allowPartial);
3745
            }, true ) || false;
3746
        };
3747
 
3748
        selProto.getBookmark = function(containerNode) {
3749
            return {
3750
                backward: this.isBackward(),
3751
                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3752
            };
3753
        };
3754
 
3755
        selProto.moveToBookmark = function(bookmark) {
3756
            var selRanges = [];
3757
            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3758
                range = api.createRange(this.win);
3759
                range.moveToBookmark(rangeBookmark);
3760
                selRanges.push(range);
3761
            }
3762
            if (bookmark.backward) {
3763
                this.setSingleRange(selRanges[0], "backward");
3764
            } else {
3765
                this.setRanges(selRanges);
3766
            }
3767
        };
3768
 
3769
        selProto.saveRanges = function() {
3770
            return {
3771
                backward: this.isBackward(),
3772
                ranges: this.callMethodOnEachRange("cloneRange")
3773
            };
3774
        };
3775
 
3776
        selProto.restoreRanges = function(selRanges) {
3777
            this.removeAllRanges();
3778
            for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
3779
                this.addRange(range, (selRanges.backward && i == 0));
3780
            }
3781
        };
3782
 
3783
        selProto.toHtml = function() {
3784
            var rangeHtmls = [];
3785
            this.eachRange(function(range) {
3786
                rangeHtmls.push( DomRange.toHtml(range) );
3787
            });
3788
            return rangeHtmls.join("");
3789
        };
3790
 
3791
        if (features.implementsTextRange) {
3792
            selProto.getNativeTextRange = function() {
3793
                var sel, textRange;
3794
                if ( (sel = this.docSelection) ) {
3795
                    var range = sel.createRange();
3796
                    if (isTextRange(range)) {
3797
                        return range;
3798
                    } else {
3799
                        throw module.createError("getNativeTextRange: selection is a control selection");
3800
                    }
3801
                } else if (this.rangeCount > 0) {
3802
                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3803
                } else {
3804
                    throw module.createError("getNativeTextRange: selection contains no range");
3805
                }
3806
            };
3807
        }
3808
 
3809
        function inspect(sel) {
3810
            var rangeInspects = [];
3811
            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3812
            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3813
            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3814
 
3815
            if (typeof sel.rangeCount != "undefined") {
3816
                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3817
                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3818
                }
3819
            }
3820
            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3821
                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3822
        }
3823
 
3824
        selProto.getName = function() {
3825
            return "WrappedSelection";
3826
        };
3827
 
3828
        selProto.inspect = function() {
3829
            return inspect(this);
3830
        };
3831
 
3832
        selProto.detach = function() {
3833
            actOnCachedSelection(this.win, "delete");
3834
            deleteProperties(this);
3835
        };
3836
 
3837
        WrappedSelection.detachAll = function() {
3838
            actOnCachedSelection(null, "deleteAll");
3839
        };
3840
 
3841
        WrappedSelection.inspect = inspect;
3842
        WrappedSelection.isDirectionBackward = isDirectionBackward;
3843
 
3844
        api.Selection = WrappedSelection;
3845
 
3846
        api.selectionPrototype = selProto;
3847
 
3848
        api.addShimListener(function(win) {
3849
            if (typeof win.getSelection == "undefined") {
3850
                win.getSelection = function() {
3851
                    return getSelection(win);
3852
                };
3853
            }
3854
            win = null;
3855
        });
3856
    });
3857
 
3858
 
3859
    /*----------------------------------------------------------------------------------------------------------------*/
3860
 
3861
    // Wait for document to load before initializing
3862
    var docReady = false;
3863
 
3864
    var loadHandler = function(e) {
3865
        if (!docReady) {
3866
            docReady = true;
3867
            if (!api.initialized && api.config.autoInitialize) {
3868
                init();
3869
            }
3870
        }
3871
    };
3872
 
3873
    if (isBrowser) {
3874
        // Test whether the document has already been loaded and initialize immediately if so
3875
        if (document.readyState == "complete") {
3876
            loadHandler();
3877
        } else {
3878
            if (isHostMethod(document, "addEventListener")) {
3879
                document.addEventListener("DOMContentLoaded", loadHandler, false);
3880
            }
3881
 
3882
            // Add a fallback in case the DOMContentLoaded event isn't supported
3883
            addListener(window, "load", loadHandler);
3884
        }
3885
    }
3886
 
3887
    return api;
3888
}, this);
3889
/**
3890
 * Selection save and restore module for Rangy.
3891
 * Saves and restores user selections using marker invisible elements in the DOM.
3892
 *
3893
 * Part of Rangy, a cross-browser JavaScript range and selection library
3894
 * https://github.com/timdown/rangy
3895
 *
3896
 * Depends on Rangy core.
3897
 *
3898
 * Copyright 2022, Tim Down
3899
 * Licensed under the MIT license.
3900
 * Version: 1.3.1
3901
 * Build date: 17 August 2022
3902
 */
3903
(function(factory, root) {
3904
    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
3905
    factory(root.rangy);
3906
})(function(rangy) {
3907
    rangy.createModule("SaveRestore", ["WrappedSelection"], function(api, module) {
3908
        var dom = api.dom;
3909
        var removeNode = dom.removeNode;
3910
        var isDirectionBackward = api.Selection.isDirectionBackward;
3911
        var markerTextChar = "\ufeff";
3912
 
3913
        function gEBI(id, doc) {
3914
            return (doc || document).getElementById(id);
3915
        }
3916
 
3917
        function insertRangeBoundaryMarker(range, atStart) {
3918
            var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
3919
            var markerEl;
3920
            var doc = dom.getDocument(range.startContainer);
3921
 
3922
            // Clone the Range and collapse to the appropriate boundary point
3923
            var boundaryRange = range.cloneRange();
3924
            boundaryRange.collapse(atStart);
3925
 
3926
            // Create the marker element containing a single invisible character using DOM methods and insert it
3927
            markerEl = doc.createElement("span");
3928
            markerEl.id = markerId;
3929
            markerEl.style.lineHeight = "0";
3930
            markerEl.style.display = "none";
3931
            markerEl.className = "rangySelectionBoundary";
3932
            markerEl.appendChild(doc.createTextNode(markerTextChar));
3933
 
3934
            boundaryRange.insertNode(markerEl);
3935
            return markerEl;
3936
        }
3937
 
3938
        function setRangeBoundary(doc, range, markerId, atStart) {
3939
            var markerEl = gEBI(markerId, doc);
3940
            if (markerEl) {
3941
                range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
3942
                removeNode(markerEl);
3943
            } else {
3944
                module.warn("Marker element has been removed. Cannot restore selection.");
3945
            }
3946
        }
3947
 
3948
        function compareRanges(r1, r2) {
3949
            return r2.compareBoundaryPoints(r1.START_TO_START, r1);
3950
        }
3951
 
3952
        function saveRange(range, direction) {
3953
            var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
3954
            var backward = isDirectionBackward(direction);
3955
 
3956
            if (range.collapsed) {
3957
                endEl = insertRangeBoundaryMarker(range, false);
3958
                return {
3959
                    document: doc,
3960
                    markerId: endEl.id,
3961
                    collapsed: true
3962
                };
3963
            } else {
3964
                endEl = insertRangeBoundaryMarker(range, false);
3965
                startEl = insertRangeBoundaryMarker(range, true);
3966
 
3967
                return {
3968
                    document: doc,
3969
                    startMarkerId: startEl.id,
3970
                    endMarkerId: endEl.id,
3971
                    collapsed: false,
3972
                    backward: backward,
3973
                    toString: function() {
3974
                        return "original text: '" + text + "', new text: '" + range.toString() + "'";
3975
                    }
3976
                };
3977
            }
3978
        }
3979
 
3980
        function restoreRange(rangeInfo, normalize) {
3981
            var doc = rangeInfo.document;
3982
            if (typeof normalize == "undefined") {
3983
                normalize = true;
3984
            }
3985
            var range = api.createRange(doc);
3986
            if (rangeInfo.collapsed) {
3987
                var markerEl = gEBI(rangeInfo.markerId, doc);
3988
                if (markerEl) {
3989
                    markerEl.style.display = "inline";
3990
                    var previousNode = markerEl.previousSibling;
3991
 
3992
                    // Workaround for issue 17
3993
                    if (previousNode && previousNode.nodeType == 3) {
3994
                        removeNode(markerEl);
3995
                        range.collapseToPoint(previousNode, previousNode.length);
3996
                    } else {
3997
                        range.collapseBefore(markerEl);
3998
                        removeNode(markerEl);
3999
                    }
4000
                } else {
4001
                    module.warn("Marker element has been removed. Cannot restore selection.");
4002
                }
4003
            } else {
4004
                setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
4005
                setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
4006
            }
4007
 
4008
            if (normalize) {
4009
                range.normalizeBoundaries();
4010
            }
4011
 
4012
            return range;
4013
        }
4014
 
4015
        function saveRanges(ranges, direction) {
4016
            var rangeInfos = [], range, doc;
4017
            var backward = isDirectionBackward(direction);
4018
 
4019
            // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
4020
            ranges = ranges.slice(0);
4021
            ranges.sort(compareRanges);
4022
 
4023
            for (var i = 0, len = ranges.length; i < len; ++i) {
4024
                rangeInfos[i] = saveRange(ranges[i], backward);
4025
            }
4026
 
4027
            // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
4028
            // between its markers
4029
            for (i = len - 1; i >= 0; --i) {
4030
                range = ranges[i];
4031
                doc = api.DomRange.getRangeDocument(range);
4032
                if (range.collapsed) {
4033
                    range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
4034
                } else {
4035
                    range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
4036
                    range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
4037
                }
4038
            }
4039
 
4040
            return rangeInfos;
4041
        }
4042
 
4043
        function saveSelection(win) {
4044
            if (!api.isSelectionValid(win)) {
4045
                module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
4046
                return null;
4047
            }
4048
            var sel = api.getSelection(win);
4049
            var ranges = sel.getAllRanges();
4050
            var backward = (ranges.length == 1 && sel.isBackward());
4051
 
4052
            var rangeInfos = saveRanges(ranges, backward);
4053
 
4054
            // Ensure current selection is unaffected
4055
            if (backward) {
4056
                sel.setSingleRange(ranges[0], backward);
4057
            } else {
4058
                sel.setRanges(ranges);
4059
            }
4060
 
4061
            return {
4062
                win: win,
4063
                rangeInfos: rangeInfos,
4064
                restored: false
4065
            };
4066
        }
4067
 
4068
        function restoreRanges(rangeInfos) {
4069
            var ranges = [];
4070
 
4071
            // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
4072
            // normalization affecting previously restored ranges.
4073
            var rangeCount = rangeInfos.length;
4074
 
4075
            for (var i = rangeCount - 1; i >= 0; i--) {
4076
                ranges[i] = restoreRange(rangeInfos[i], true);
4077
            }
4078
 
4079
            return ranges;
4080
        }
4081
 
4082
        function restoreSelection(savedSelection, preserveDirection) {
4083
            if (!savedSelection.restored) {
4084
                var rangeInfos = savedSelection.rangeInfos;
4085
                var sel = api.getSelection(savedSelection.win);
4086
                var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
4087
 
4088
                if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
4089
                    sel.removeAllRanges();
4090
                    sel.addRange(ranges[0], true);
4091
                } else {
4092
                    sel.setRanges(ranges);
4093
                }
4094
 
4095
                savedSelection.restored = true;
4096
            }
4097
        }
4098
 
4099
        function removeMarkerElement(doc, markerId) {
4100
            var markerEl = gEBI(markerId, doc);
4101
            if (markerEl) {
4102
                removeNode(markerEl);
4103
            }
4104
        }
4105
 
4106
        function removeMarkers(savedSelection) {
4107
            var rangeInfos = savedSelection.rangeInfos;
4108
            for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
4109
                rangeInfo = rangeInfos[i];
4110
                if (rangeInfo.collapsed) {
4111
                    removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
4112
                } else {
4113
                    removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
4114
                    removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
4115
                }
4116
            }
4117
        }
4118
 
4119
        api.util.extend(api, {
4120
            saveRange: saveRange,
4121
            restoreRange: restoreRange,
4122
            saveRanges: saveRanges,
4123
            restoreRanges: restoreRanges,
4124
            saveSelection: saveSelection,
4125
            restoreSelection: restoreSelection,
4126
            removeMarkerElement: removeMarkerElement,
4127
            removeMarkers: removeMarkers
4128
        });
4129
    });
4130
 
4131
    return rangy;
4132
}, this);
4133
/**
4134
 * Serializer module for Rangy.
4135
 * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
4136
 * cookie or local storage and restore it on the user's next visit to the same page.
4137
 *
4138
 * Part of Rangy, a cross-browser JavaScript range and selection library
4139
 * https://github.com/timdown/rangy
4140
 *
4141
 * Depends on Rangy core.
4142
 *
4143
 * Copyright 2022, Tim Down
4144
 * Licensed under the MIT license.
4145
 * Version: 1.3.1
4146
 * Build date: 17 August 2022
4147
 */
4148
(function(factory, root) {
4149
    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
4150
    factory(root.rangy);
4151
})(function(rangy) {
4152
    rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
4153
        var UNDEF = "undefined";
4154
        var util = api.util;
4155
 
4156
        // encodeURIComponent and decodeURIComponent are required for cookie handling
4157
        if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
4158
            module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
4159
        }
4160
 
4161
        // Checksum for checking whether range can be serialized
4162
        var crc32 = (function() {
4163
            function utf8encode(str) {
4164
                var utf8CharCodes = [];
4165
 
4166
                for (var i = 0, len = str.length, c; i < len; ++i) {
4167
                    c = str.charCodeAt(i);
4168
                    if (c < 128) {
4169
                        utf8CharCodes.push(c);
4170
                    } else if (c < 2048) {
4171
                        utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
4172
                    } else {
4173
                        utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
4174
                    }
4175
                }
4176
                return utf8CharCodes;
4177
            }
4178
 
4179
            var cachedCrcTable = null;
4180
 
4181
            function buildCRCTable() {
4182
                var table = [];
4183
                for (var i = 0, j, crc; i < 256; ++i) {
4184
                    crc = i;
4185
                    j = 8;
4186
                    while (j--) {
4187
                        if ((crc & 1) == 1) {
4188
                            crc = (crc >>> 1) ^ 0xEDB88320;
4189
                        } else {
4190
                            crc >>>= 1;
4191
                        }
4192
                    }
4193
                    table[i] = crc >>> 0;
4194
                }
4195
                return table;
4196
            }
4197
 
4198
            function getCrcTable() {
4199
                if (!cachedCrcTable) {
4200
                    cachedCrcTable = buildCRCTable();
4201
                }
4202
                return cachedCrcTable;
4203
            }
4204
 
4205
            return function(str) {
4206
                var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
4207
                for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
4208
                    y = (crc ^ utf8CharCodes[i]) & 0xFF;
4209
                    crc = (crc >>> 8) ^ crcTable[y];
4210
                }
4211
                return (crc ^ -1) >>> 0;
4212
            };
4213
        })();
4214
 
4215
        var dom = api.dom;
4216
 
4217
        function escapeTextForHtml(str) {
4218
            return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
4219
        }
4220
 
4221
        function nodeToInfoString(node, infoParts) {
4222
            infoParts = infoParts || [];
4223
            var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
4224
            var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
4225
            var start = "", end = "";
4226
            switch (nodeType) {
4227
                case 3: // Text node
4228
                    start = escapeTextForHtml(node.nodeValue);
4229
                    break;
4230
                case 8: // Comment
4231
                    start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
4232
                    break;
4233
                default:
4234
                    start = "<" + nodeInfo + ">";
4235
                    end = "</>";
4236
                    break;
4237
            }
4238
            if (start) {
4239
                infoParts.push(start);
4240
            }
4241
            for (var i = 0; i < childCount; ++i) {
4242
                nodeToInfoString(children[i], infoParts);
4243
            }
4244
            if (end) {
4245
                infoParts.push(end);
4246
            }
4247
            return infoParts;
4248
        }
4249
 
4250
        // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
4251
        // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
4252
        // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
4253
        // innerHTML whenever the user changes an input within the element.
4254
        function getElementChecksum(el) {
4255
            var info = nodeToInfoString(el).join("");
4256
            return crc32(info).toString(16);
4257
        }
4258
 
4259
        function serializePosition(node, offset, rootNode) {
4260
            var pathParts = [], n = node;
4261
            rootNode = rootNode || dom.getDocument(node).documentElement;
4262
            while (n && n != rootNode) {
4263
                pathParts.push(dom.getNodeIndex(n, true));
4264
                n = n.parentNode;
4265
            }
4266
            return pathParts.join("/") + ":" + offset;
4267
        }
4268
 
4269
        function deserializePosition(serialized, rootNode, doc) {
4270
            if (!rootNode) {
4271
                rootNode = (doc || document).documentElement;
4272
            }
4273
            var parts = serialized.split(":");
4274
            var node = rootNode;
4275
            var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
4276
 
4277
            while (i--) {
4278
                nodeIndex = parseInt(nodeIndices[i], 10);
4279
                if (nodeIndex < node.childNodes.length) {
4280
                    node = node.childNodes[nodeIndex];
4281
                } else {
4282
                    throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
4283
                            " has no child with index " + nodeIndex + ", " + i);
4284
                }
4285
            }
4286
 
4287
            return new dom.DomPosition(node, parseInt(parts[1], 10));
4288
        }
4289
 
4290
        function serializeRange(range, omitChecksum, rootNode) {
4291
            rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
4292
            if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
4293
                throw module.createError("serializeRange(): range " + range.inspect() +
4294
                    " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
4295
            }
4296
            var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
4297
                serializePosition(range.endContainer, range.endOffset, rootNode);
4298
            if (!omitChecksum) {
4299
                serialized += "{" + getElementChecksum(rootNode) + "}";
4300
            }
4301
            return serialized;
4302
        }
4303
 
4304
        var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
4305
 
4306
        function deserializeRange(serialized, rootNode, doc) {
4307
            if (rootNode) {
4308
                doc = doc || dom.getDocument(rootNode);
4309
            } else {
4310
                doc = doc || document;
4311
                rootNode = doc.documentElement;
4312
            }
4313
            var result = deserializeRegex.exec(serialized);
4314
            var checksum = result[4];
4315
            if (checksum) {
4316
                var rootNodeChecksum = getElementChecksum(rootNode);
4317
                if (checksum !== rootNodeChecksum) {
4318
                    throw module.createError("deserializeRange(): checksums of serialized range root node (" + checksum +
4319
                        ") and target root node (" + rootNodeChecksum + ") do not match");
4320
                }
4321
            }
4322
            var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
4323
            var range = api.createRange(doc);
4324
            range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
4325
            return range;
4326
        }
4327
 
4328
        function canDeserializeRange(serialized, rootNode, doc) {
4329
            if (!rootNode) {
4330
                rootNode = (doc || document).documentElement;
4331
            }
4332
            var result = deserializeRegex.exec(serialized);
4333
            var checksum = result[3];
4334
            return !checksum || checksum === getElementChecksum(rootNode);
4335
        }
4336
 
4337
        function serializeSelection(selection, omitChecksum, rootNode) {
4338
            selection = api.getSelection(selection);
4339
            var ranges = selection.getAllRanges(), serializedRanges = [];
4340
            for (var i = 0, len = ranges.length; i < len; ++i) {
4341
                serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
4342
            }
4343
            return serializedRanges.join("|");
4344
        }
4345
 
4346
        function deserializeSelection(serialized, rootNode, win) {
4347
            if (rootNode) {
4348
                win = win || dom.getWindow(rootNode);
4349
            } else {
4350
                win = win || window;
4351
                rootNode = win.document.documentElement;
4352
            }
4353
            var serializedRanges = serialized.split("|");
4354
            var sel = api.getSelection(win);
4355
            var ranges = [];
4356
 
4357
            for (var i = 0, len = serializedRanges.length; i < len; ++i) {
4358
                ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
4359
            }
4360
            sel.setRanges(ranges);
4361
 
4362
            return sel;
4363
        }
4364
 
4365
        function canDeserializeSelection(serialized, rootNode, win) {
4366
            var doc;
4367
            if (rootNode) {
4368
                doc = win ? win.document : dom.getDocument(rootNode);
4369
            } else {
4370
                win = win || window;
4371
                rootNode = win.document.documentElement;
4372
            }
4373
            var serializedRanges = serialized.split("|");
4374
 
4375
            for (var i = 0, len = serializedRanges.length; i < len; ++i) {
4376
                if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
4377
                    return false;
4378
                }
4379
            }
4380
 
4381
            return true;
4382
        }
4383
 
4384
        var cookieName = "rangySerializedSelection";
4385
 
4386
        function getSerializedSelectionFromCookie(cookie) {
4387
            var parts = cookie.split(/[;,]/);
4388
            for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
4389
                nameVal = parts[i].split("=");
4390
                if (nameVal[0].replace(/^\s+/, "") == cookieName) {
4391
                    val = nameVal[1];
4392
                    if (val) {
4393
                        return decodeURIComponent(val.replace(/\s+$/, ""));
4394
                    }
4395
                }
4396
            }
4397
            return null;
4398
        }
4399
 
4400
        function restoreSelectionFromCookie(win) {
4401
            win = win || window;
4402
            var serialized = getSerializedSelectionFromCookie(win.document.cookie);
4403
            if (serialized) {
4404
                deserializeSelection(serialized, win.doc);
4405
            }
4406
        }
4407
 
4408
        function saveSelectionCookie(win, props) {
4409
            win = win || window;
4410
            props = (typeof props == "object") ? props : {};
4411
            var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
4412
            var path = props.path ? ";path=" + props.path : "";
4413
            var domain = props.domain ? ";domain=" + props.domain : "";
4414
            var secure = props.secure ? ";secure" : "";
4415
            var serialized = serializeSelection(api.getSelection(win));
4416
            win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
4417
        }
4418
 
4419
        util.extend(api, {
4420
            serializePosition: serializePosition,
4421
            deserializePosition: deserializePosition,
4422
            serializeRange: serializeRange,
4423
            deserializeRange: deserializeRange,
4424
            canDeserializeRange: canDeserializeRange,
4425
            serializeSelection: serializeSelection,
4426
            deserializeSelection: deserializeSelection,
4427
            canDeserializeSelection: canDeserializeSelection,
4428
            restoreSelectionFromCookie: restoreSelectionFromCookie,
4429
            saveSelectionCookie: saveSelectionCookie,
4430
            getElementChecksum: getElementChecksum,
4431
            nodeToInfoString: nodeToInfoString
4432
        });
4433
 
4434
        util.crc32 = crc32;
4435
    });
4436
 
4437
    return rangy;
4438
}, this);
4439
/**
4440
 * Class Applier module for Rangy.
4441
 * Adds, removes and toggles classes on Ranges and Selections
4442
 *
4443
 * Part of Rangy, a cross-browser JavaScript range and selection library
4444
 * https://github.com/timdown/rangy
4445
 *
4446
 * Depends on Rangy core.
4447
 *
4448
 * Copyright 2022, Tim Down
4449
 * Licensed under the MIT license.
4450
 * Version: 1.3.1
4451
 * Build date: 17 August 2022
4452
 */
4453
(function(factory, root) {
4454
    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
4455
    factory(root.rangy);
4456
})(function(rangy) {
4457
    rangy.createModule("ClassApplier", ["WrappedSelection"], function(api, module) {
4458
        var dom = api.dom;
4459
        var DomPosition = dom.DomPosition;
4460
        var contains = dom.arrayContains;
4461
        var util = api.util;
4462
        var forEach = util.forEach;
4463
 
4464
 
4465
        var defaultTagName = "span";
4466
        var createElementNSSupported = util.isHostMethod(document, "createElementNS");
4467
 
4468
        function each(obj, func) {
4469
            for (var i in obj) {
4470
                if (obj.hasOwnProperty(i)) {
4471
                    if (func(i, obj[i]) === false) {
4472
                        return false;
4473
                    }
4474
                }
4475
            }
4476
            return true;
4477
        }
4478
 
4479
        function trim(str) {
4480
            return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
4481
        }
4482
 
4483
        function classNameContainsClass(fullClassName, className) {
4484
            return !!fullClassName && new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
4485
        }
4486
 
4487
        // Inefficient, inelegant nonsense for IE's svg element, which has no classList and non-HTML className implementation
4488
        function hasClass(el, className) {
4489
            if (typeof el.classList == "object") {
4490
                return el.classList.contains(className);
4491
            } else {
4492
                var classNameSupported = (typeof el.className == "string");
4493
                var elClass = classNameSupported ? el.className : el.getAttribute("class");
4494
                return classNameContainsClass(elClass, className);
4495
            }
4496
        }
4497
 
4498
        function addClass(el, className) {
4499
            if (typeof el.classList == "object") {
4500
                el.classList.add(className);
4501
            } else {
4502
                var classNameSupported = (typeof el.className == "string");
4503
                var elClass = classNameSupported ? el.className : el.getAttribute("class");
4504
                if (elClass) {
4505
                    if (!classNameContainsClass(elClass, className)) {
4506
                        elClass += " " + className;
4507
                    }
4508
                } else {
4509
                    elClass = className;
4510
                }
4511
                if (classNameSupported) {
4512
                    el.className = elClass;
4513
                } else {
4514
                    el.setAttribute("class", elClass);
4515
                }
4516
            }
4517
        }
4518
 
4519
        var removeClass = (function() {
4520
            function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
4521
                return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
4522
            }
4523
 
4524
            return function(el, className) {
4525
                if (typeof el.classList == "object") {
4526
                    el.classList.remove(className);
4527
                } else {
4528
                    var classNameSupported = (typeof el.className == "string");
4529
                    var elClass = classNameSupported ? el.className : el.getAttribute("class");
4530
                    elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
4531
                    if (classNameSupported) {
4532
                        el.className = elClass;
4533
                    } else {
4534
                        el.setAttribute("class", elClass);
4535
                    }
4536
                }
4537
            };
4538
        })();
4539
 
4540
        function getClass(el) {
4541
            var classNameSupported = (typeof el.className == "string");
4542
            return classNameSupported ? el.className : el.getAttribute("class");
4543
        }
4544
 
4545
        function sortClassName(className) {
4546
            return className && className.split(/\s+/).sort().join(" ");
4547
        }
4548
 
4549
        function getSortedClassName(el) {
4550
            return sortClassName( getClass(el) );
4551
        }
4552
 
4553
        function haveSameClasses(el1, el2) {
4554
            return getSortedClassName(el1) == getSortedClassName(el2);
4555
        }
4556
 
4557
        function hasAllClasses(el, className) {
4558
            var classes = className.split(/\s+/);
4559
            for (var i = 0, len = classes.length; i < len; ++i) {
4560
                if (!hasClass(el, trim(classes[i]))) {
4561
                    return false;
4562
                }
4563
            }
4564
            return true;
4565
        }
4566
 
4567
        function canTextBeStyled(textNode) {
4568
            var parent = textNode.parentNode;
4569
            return (parent && parent.nodeType == 1 && !/^(textarea|style|script|select|iframe)$/i.test(parent.nodeName));
4570
        }
4571
 
4572
        function movePosition(position, oldParent, oldIndex, newParent, newIndex) {
4573
            var posNode = position.node, posOffset = position.offset;
4574
            var newNode = posNode, newOffset = posOffset;
4575
 
4576
            if (posNode == newParent && posOffset > newIndex) {
4577
                ++newOffset;
4578
            }
4579
 
4580
            if (posNode == oldParent && (posOffset == oldIndex  || posOffset == oldIndex + 1)) {
4581
                newNode = newParent;
4582
                newOffset += newIndex - oldIndex;
4583
            }
4584
 
4585
            if (posNode == oldParent && posOffset > oldIndex + 1) {
4586
                --newOffset;
4587
            }
4588
 
4589
            position.node = newNode;
4590
            position.offset = newOffset;
4591
        }
4592
 
4593
        function movePositionWhenRemovingNode(position, parentNode, index) {
4594
            if (position.node == parentNode && position.offset > index) {
4595
                --position.offset;
4596
            }
4597
        }
4598
 
4599
        function movePreservingPositions(node, newParent, newIndex, positionsToPreserve) {
4600
            // For convenience, allow newIndex to be -1 to mean "insert at the end".
4601
            if (newIndex == -1) {
4602
                newIndex = newParent.childNodes.length;
4603
            }
4604
 
4605
            var oldParent = node.parentNode;
4606
            var oldIndex = dom.getNodeIndex(node);
4607
 
4608
            forEach(positionsToPreserve, function(position) {
4609
                movePosition(position, oldParent, oldIndex, newParent, newIndex);
4610
            });
4611
 
4612
            // Now actually move the node.
4613
            if (newParent.childNodes.length == newIndex) {
4614
                newParent.appendChild(node);
4615
            } else {
4616
                newParent.insertBefore(node, newParent.childNodes[newIndex]);
4617
            }
4618
        }
4619
 
4620
        function removePreservingPositions(node, positionsToPreserve) {
4621
 
4622
            var oldParent = node.parentNode;
4623
            var oldIndex = dom.getNodeIndex(node);
4624
 
4625
            forEach(positionsToPreserve, function(position) {
4626
                movePositionWhenRemovingNode(position, oldParent, oldIndex);
4627
            });
4628
 
4629
            dom.removeNode(node);
4630
        }
4631
 
4632
        function moveChildrenPreservingPositions(node, newParent, newIndex, removeNode, positionsToPreserve) {
4633
            var child, children = [];
4634
            while ( (child = node.firstChild) ) {
4635
                movePreservingPositions(child, newParent, newIndex++, positionsToPreserve);
4636
                children.push(child);
4637
            }
4638
            if (removeNode) {
4639
                removePreservingPositions(node, positionsToPreserve);
4640
            }
4641
            return children;
4642
        }
4643
 
4644
        function replaceWithOwnChildrenPreservingPositions(element, positionsToPreserve) {
4645
            return moveChildrenPreservingPositions(element, element.parentNode, dom.getNodeIndex(element), true, positionsToPreserve);
4646
        }
4647
 
4648
        function rangeSelectsAnyText(range, textNode) {
4649
            var textNodeRange = range.cloneRange();
4650
            textNodeRange.selectNodeContents(textNode);
4651
 
4652
            var intersectionRange = textNodeRange.intersection(range);
4653
            var text = intersectionRange ? intersectionRange.toString() : "";
4654
 
4655
            return text != "";
4656
        }
4657
 
4658
        function getEffectiveTextNodes(range) {
4659
            var nodes = range.getNodes([3]);
4660
 
4661
            // Optimization as per issue 145
4662
 
4663
            // Remove non-intersecting text nodes from the start of the range
4664
            var start = 0, node;
4665
            while ( (node = nodes[start]) && !rangeSelectsAnyText(range, node) ) {
4666
                ++start;
4667
            }
4668
 
4669
            // Remove non-intersecting text nodes from the start of the range
4670
            var end = nodes.length - 1;
4671
            while ( (node = nodes[end]) && !rangeSelectsAnyText(range, node) ) {
4672
                --end;
4673
            }
4674
 
4675
            return nodes.slice(start, end + 1);
4676
        }
4677
 
4678
        function elementsHaveSameNonClassAttributes(el1, el2) {
4679
            if (el1.attributes.length != el2.attributes.length) return false;
4680
            for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
4681
                attr1 = el1.attributes[i];
4682
                name = attr1.name;
4683
                if (name != "class") {
4684
                    attr2 = el2.attributes.getNamedItem(name);
4685
                    if ( (attr1 === null) != (attr2 === null) ) return false;
4686
                    if (attr1.specified != attr2.specified) return false;
4687
                    if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
4688
                }
4689
            }
4690
            return true;
4691
        }
4692
 
4693
        function elementHasNonClassAttributes(el, exceptions) {
4694
            for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
4695
                attrName = el.attributes[i].name;
4696
                if ( !(exceptions && contains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
4697
                    return true;
4698
                }
4699
            }
4700
            return false;
4701
        }
4702
 
4703
        var getComputedStyleProperty = dom.getComputedStyleProperty;
4704
        var isEditableElement = (function() {
4705
            var testEl = document.createElement("div");
4706
            return typeof testEl.isContentEditable == "boolean" ?
4707
                function (node) {
4708
                    return node && node.nodeType == 1 && node.isContentEditable;
4709
                } :
4710
                function (node) {
4711
                    if (!node || node.nodeType != 1 || node.contentEditable == "false") {
4712
                        return false;
4713
                    }
4714
                    return node.contentEditable == "true" || isEditableElement(node.parentNode);
4715
                };
4716
        })();
4717
 
4718
        function isEditingHost(node) {
4719
            var parent;
4720
            return node && node.nodeType == 1 &&
4721
                (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") ||
4722
                (isEditableElement(node) && !isEditableElement(node.parentNode)));
4723
        }
4724
 
4725
        function isEditable(node) {
4726
            return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
4727
        }
4728
 
4729
        var inlineDisplayRegex = /^inline(-block|-table)?$/i;
4730
 
4731
        function isNonInlineElement(node) {
4732
            return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
4733
        }
4734
 
4735
        // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
4736
        var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
4737
 
4738
        function isUnrenderedWhiteSpaceNode(node) {
4739
            if (node.data.length == 0) {
4740
                return true;
4741
            }
4742
            if (htmlNonWhiteSpaceRegex.test(node.data)) {
4743
                return false;
4744
            }
4745
            var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
4746
            switch (cssWhiteSpace) {
4747
                case "pre":
4748
                case "pre-wrap":
4749
                case "-moz-pre-wrap":
4750
                    return false;
4751
                case "pre-line":
4752
                    if (/[\r\n]/.test(node.data)) {
4753
                        return false;
4754
                    }
4755
            }
4756
 
4757
            // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
4758
            // non-inline element, it will not be rendered. This seems to be a good enough definition.
4759
            return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
4760
        }
4761
 
4762
        function getRangeBoundaries(ranges) {
4763
            var positions = [], i, range;
4764
            for (i = 0; range = ranges[i++]; ) {
4765
                positions.push(
4766
                    new DomPosition(range.startContainer, range.startOffset),
4767
                    new DomPosition(range.endContainer, range.endOffset)
4768
                );
4769
            }
4770
            return positions;
4771
        }
4772
 
4773
        function updateRangesFromBoundaries(ranges, positions) {
4774
            for (var i = 0, range, start, end, len = ranges.length; i < len; ++i) {
4775
                range = ranges[i];
4776
                start = positions[i * 2];
4777
                end = positions[i * 2 + 1];
4778
                range.setStartAndEnd(start.node, start.offset, end.node, end.offset);
4779
            }
4780
        }
4781
 
4782
        function isSplitPoint(node, offset) {
4783
            if (dom.isCharacterDataNode(node)) {
4784
                if (offset == 0) {
4785
                    return !!node.previousSibling;
4786
                } else if (offset == node.length) {
4787
                    return !!node.nextSibling;
4788
                } else {
4789
                    return true;
4790
                }
4791
            }
4792
 
4793
            return offset > 0 && offset < node.childNodes.length;
4794
        }
4795
 
4796
        function splitNodeAt(node, descendantNode, descendantOffset, positionsToPreserve) {
4797
            var newNode, parentNode;
4798
            var splitAtStart = (descendantOffset == 0);
4799
 
4800
            if (dom.isAncestorOf(descendantNode, node)) {
4801
                return node;
4802
            }
4803
 
4804
            if (dom.isCharacterDataNode(descendantNode)) {
4805
                var descendantIndex = dom.getNodeIndex(descendantNode);
4806
                if (descendantOffset == 0) {
4807
                    descendantOffset = descendantIndex;
4808
                } else if (descendantOffset == descendantNode.length) {
4809
                    descendantOffset = descendantIndex + 1;
4810
                } else {
4811
                    throw module.createError("splitNodeAt() should not be called with offset in the middle of a data node (" +
4812
                        descendantOffset + " in " + descendantNode.data);
4813
                }
4814
                descendantNode = descendantNode.parentNode;
4815
            }
4816
 
4817
            if (isSplitPoint(descendantNode, descendantOffset)) {
4818
                // descendantNode is now guaranteed not to be a text or other character node
4819
                newNode = descendantNode.cloneNode(false);
4820
                parentNode = descendantNode.parentNode;
4821
                if (newNode.id) {
4822
                    newNode.removeAttribute("id");
4823
                }
4824
                var child, newChildIndex = 0;
4825
 
4826
                while ( (child = descendantNode.childNodes[descendantOffset]) ) {
4827
                    movePreservingPositions(child, newNode, newChildIndex++, positionsToPreserve);
4828
                }
4829
                movePreservingPositions(newNode, parentNode, dom.getNodeIndex(descendantNode) + 1, positionsToPreserve);
4830
                return (descendantNode == node) ? newNode : splitNodeAt(node, parentNode, dom.getNodeIndex(newNode), positionsToPreserve);
4831
            } else if (node != descendantNode) {
4832
                newNode = descendantNode.parentNode;
4833
 
4834
                // Work out a new split point in the parent node
4835
                var newNodeIndex = dom.getNodeIndex(descendantNode);
4836
 
4837
                if (!splitAtStart) {
4838
                    newNodeIndex++;
4839
                }
4840
                return splitNodeAt(node, newNode, newNodeIndex, positionsToPreserve);
4841
            }
4842
            return node;
4843
        }
4844
 
4845
        function areElementsMergeable(el1, el2) {
4846
            return el1.namespaceURI == el2.namespaceURI &&
4847
                el1.tagName.toLowerCase() == el2.tagName.toLowerCase() &&
4848
                haveSameClasses(el1, el2) &&
4849
                elementsHaveSameNonClassAttributes(el1, el2) &&
4850
                getComputedStyleProperty(el1, "display") == "inline" &&
4851
                getComputedStyleProperty(el2, "display") == "inline";
4852
        }
4853
 
4854
        function createAdjacentMergeableTextNodeGetter(forward) {
4855
            var siblingPropName = forward ? "nextSibling" : "previousSibling";
4856
 
4857
            return function(textNode, checkParentElement) {
4858
                var el = textNode.parentNode;
4859
                var adjacentNode = textNode[siblingPropName];
4860
                if (adjacentNode) {
4861
                    // Can merge if the node's previous/next sibling is a text node
4862
                    if (adjacentNode && adjacentNode.nodeType == 3) {
4863
                        return adjacentNode;
4864
                    }
4865
                } else if (checkParentElement) {
4866
                    // Compare text node parent element with its sibling
4867
                    adjacentNode = el[siblingPropName];
4868
                    if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
4869
                        var adjacentNodeChild = adjacentNode[forward ? "firstChild" : "lastChild"];
4870
                        if (adjacentNodeChild && adjacentNodeChild.nodeType == 3) {
4871
                            return adjacentNodeChild;
4872
                        }
4873
                    }
4874
                }
4875
                return null;
4876
            };
4877
        }
4878
 
4879
        var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
4880
            getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
4881
 
4882
 
4883
        function Merge(firstNode) {
4884
            this.isElementMerge = (firstNode.nodeType == 1);
4885
            this.textNodes = [];
4886
            var firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
4887
            if (firstTextNode) {
4888
                this.textNodes[0] = firstTextNode;
4889
            }
4890
        }
4891
 
4892
        Merge.prototype = {
4893
            doMerge: function(positionsToPreserve) {
4894
                var textNodes = this.textNodes;
4895
                var firstTextNode = textNodes[0];
4896
                if (textNodes.length > 1) {
4897
                    var firstTextNodeIndex = dom.getNodeIndex(firstTextNode);
4898
                    var textParts = [], combinedTextLength = 0, textNode, parent;
4899
                    forEach(textNodes, function(textNode, i) {
4900
                        parent = textNode.parentNode;
4901
                        if (i > 0) {
4902
                            parent.removeChild(textNode);
4903
                            if (!parent.hasChildNodes()) {
4904
                                dom.removeNode(parent);
4905
                            }
4906
                            if (positionsToPreserve) {
4907
                                forEach(positionsToPreserve, function(position) {
4908
                                    // Handle case where position is inside the text node being merged into a preceding node
4909
                                    if (position.node == textNode) {
4910
                                        position.node = firstTextNode;
4911
                                        position.offset += combinedTextLength;
4912
                                    }
4913
                                    // Handle case where both text nodes precede the position within the same parent node
4914
                                    if (position.node == parent && position.offset > firstTextNodeIndex) {
4915
                                        --position.offset;
4916
                                        if (position.offset == firstTextNodeIndex + 1 && i < textNodes.length - 1) {
4917
                                            position.node = firstTextNode;
4918
                                            position.offset = combinedTextLength;
4919
                                        }
4920
                                    }
4921
                                });
4922
                            }
4923
                        }
4924
                        textParts[i] = textNode.data;
4925
                        combinedTextLength += textNode.data.length;
4926
                    });
4927
                    firstTextNode.data = textParts.join("");
4928
                }
4929
                return firstTextNode.data;
4930
            },
4931
 
4932
            getLength: function() {
4933
                var i = this.textNodes.length, len = 0;
4934
                while (i--) {
4935
                    len += this.textNodes[i].length;
4936
                }
4937
                return len;
4938
            },
4939
 
4940
            toString: function() {
4941
                var textParts = [];
4942
                forEach(this.textNodes, function(textNode, i) {
4943
                    textParts[i] = "'" + textNode.data + "'";
4944
                });
4945
                return "[Merge(" + textParts.join(",") + ")]";
4946
            }
4947
        };
4948
 
4949
        var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly", "useExistingElements",
4950
            "removeEmptyElements", "onElementCreate"];
4951
 
4952
        // TODO: Populate this with every attribute name that corresponds to a property with a different name. Really??
4953
        var attrNamesForProperties = {};
4954
 
4955
        function ClassApplier(className, options, tagNames) {
4956
            var normalize, i, len, propName, applier = this;
4957
            applier.cssClass = applier.className = className; // cssClass property is for backward compatibility
4958
 
4959
            var elementPropertiesFromOptions = null, elementAttributes = {};
4960
 
4961
            // Initialize from options object
4962
            if (typeof options == "object" && options !== null) {
4963
                if (typeof options.elementTagName !== "undefined") {
4964
                    options.elementTagName = options.elementTagName.toLowerCase();
4965
                }
4966
                tagNames = options.tagNames;
4967
                elementPropertiesFromOptions = options.elementProperties;
4968
                elementAttributes = options.elementAttributes;
4969
 
4970
                for (i = 0; propName = optionProperties[i++]; ) {
4971
                    if (options.hasOwnProperty(propName)) {
4972
                        applier[propName] = options[propName];
4973
                    }
4974
                }
4975
                normalize = options.normalize;
4976
            } else {
4977
                normalize = options;
4978
            }
4979
 
4980
            // Backward compatibility: the second parameter can also be a Boolean indicating to normalize after unapplying
4981
            applier.normalize = (typeof normalize == "undefined") ? true : normalize;
4982
 
4983
            // Initialize element properties and attribute exceptions
4984
            applier.attrExceptions = [];
4985
            var el = document.createElement(applier.elementTagName);
4986
            applier.elementProperties = applier.copyPropertiesToElement(elementPropertiesFromOptions, el, true);
4987
            each(elementAttributes, function(attrName, attrValue) {
4988
                applier.attrExceptions.push(attrName);
4989
                // Ensure each attribute value is a string
4990
                elementAttributes[attrName] = "" + attrValue;
4991
            });
4992
            applier.elementAttributes = elementAttributes;
4993
 
4994
            applier.elementSortedClassName = applier.elementProperties.hasOwnProperty("className") ?
4995
                sortClassName(applier.elementProperties.className + " " + className) : className;
4996
 
4997
            // Initialize tag names
4998
            applier.applyToAnyTagName = false;
4999
            var type = typeof tagNames;
5000
            if (type == "string") {
5001
                if (tagNames == "*") {
5002
                    applier.applyToAnyTagName = true;
5003
                } else {
5004
                    applier.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
5005
                }
5006
            } else if (type == "object" && typeof tagNames.length == "number") {
5007
                applier.tagNames = [];
5008
                for (i = 0, len = tagNames.length; i < len; ++i) {
5009
                    if (tagNames[i] == "*") {
5010
                        applier.applyToAnyTagName = true;
5011
                    } else {
5012
                        applier.tagNames.push(tagNames[i].toLowerCase());
5013
                    }
5014
                }
5015
            } else {
5016
                applier.tagNames = [applier.elementTagName];
5017
            }
5018
        }
5019
 
5020
        ClassApplier.prototype = {
5021
            elementTagName: defaultTagName,
5022
            elementProperties: {},
5023
            elementAttributes: {},
5024
            ignoreWhiteSpace: true,
5025
            applyToEditableOnly: false,
5026
            useExistingElements: true,
5027
            removeEmptyElements: true,
5028
            onElementCreate: null,
5029
 
5030
            copyPropertiesToElement: function(props, el, createCopy) {
5031
                var s, elStyle, elProps = {}, elPropsStyle, propValue, elPropValue, attrName;
5032
 
5033
                for (var p in props) {
5034
                    if (props.hasOwnProperty(p)) {
5035
                        propValue = props[p];
5036
                        elPropValue = el[p];
5037
 
5038
                        // Special case for class. The copied properties object has the applier's class as well as its own
5039
                        // to simplify checks when removing styling elements
5040
                        if (p == "className") {
5041
                            addClass(el, propValue);
5042
                            addClass(el, this.className);
5043
                            el[p] = sortClassName(el[p]);
5044
                            if (createCopy) {
5045
                                elProps[p] = propValue;
5046
                            }
5047
                        }
5048
 
5049
                        // Special case for style
5050
                        else if (p == "style") {
5051
                            elStyle = elPropValue;
5052
                            if (createCopy) {
5053
                                elProps[p] = elPropsStyle = {};
5054
                            }
5055
                            for (s in props[p]) {
5056
                                if (props[p].hasOwnProperty(s)) {
5057
                                    elStyle[s] = propValue[s];
5058
                                    if (createCopy) {
5059
                                        elPropsStyle[s] = elStyle[s];
5060
                                    }
5061
                                }
5062
                            }
5063
                            this.attrExceptions.push(p);
5064
                        } else {
5065
                            el[p] = propValue;
5066
                            // Copy the property back from the dummy element so that later comparisons to check whether
5067
                            // elements may be removed are checking against the right value. For example, the href property
5068
                            // of an element returns a fully qualified URL even if it was previously assigned a relative
5069
                            // URL.
5070
                            if (createCopy) {
5071
                                elProps[p] = el[p];
5072
 
5073
                                // Not all properties map to identically-named attributes
5074
                                attrName = attrNamesForProperties.hasOwnProperty(p) ? attrNamesForProperties[p] : p;
5075
                                this.attrExceptions.push(attrName);
5076
                            }
5077
                        }
5078
                    }
5079
                }
5080
 
5081
                return createCopy ? elProps : "";
5082
            },
5083
 
5084
            copyAttributesToElement: function(attrs, el) {
5085
                for (var attrName in attrs) {
5086
                    if (attrs.hasOwnProperty(attrName) && !/^class(?:Name)?$/i.test(attrName)) {
5087
                        el.setAttribute(attrName, attrs[attrName]);
5088
                    }
5089
                }
5090
            },
5091
 
5092
            appliesToElement: function(el) {
5093
                return contains(this.tagNames, el.tagName.toLowerCase());
5094
            },
5095
 
5096
            getEmptyElements: function(range) {
5097
                var applier = this;
5098
                return range.getNodes([1], function(el) {
5099
                    return applier.appliesToElement(el) && !el.hasChildNodes();
5100
                });
5101
            },
5102
 
5103
            hasClass: function(node) {
5104
                return node.nodeType == 1 &&
5105
                    (this.applyToAnyTagName || this.appliesToElement(node)) &&
5106
                    hasClass(node, this.className);
5107
            },
5108
 
5109
            getSelfOrAncestorWithClass: function(node) {
5110
                while (node) {
5111
                    if (this.hasClass(node)) {
5112
                        return node;
5113
                    }
5114
                    node = node.parentNode;
5115
                }
5116
                return null;
5117
            },
5118
 
5119
            isModifiable: function(node) {
5120
                return !this.applyToEditableOnly || isEditable(node);
5121
            },
5122
 
5123
            // White space adjacent to an unwrappable node can be ignored for wrapping
5124
            isIgnorableWhiteSpaceNode: function(node) {
5125
                return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
5126
            },
5127
 
5128
            // Normalizes nodes after applying a class to a Range.
5129
            postApply: function(textNodes, range, positionsToPreserve, isUndo) {
5130
                var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
5131
                var merges = [], currentMerge;
5132
                var rangeStartNode = firstNode, rangeEndNode = lastNode;
5133
                var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
5134
                var precedingTextNode;
5135
 
5136
                // Check for every required merge and create a Merge object for each
5137
                forEach(textNodes, function(textNode) {
5138
                    precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
5139
                    if (precedingTextNode) {
5140
                        if (!currentMerge) {
5141
                            currentMerge = new Merge(precedingTextNode);
5142
                            merges.push(currentMerge);
5143
                        }
5144
                        currentMerge.textNodes.push(textNode);
5145
                        if (textNode === firstNode) {
5146
                            rangeStartNode = currentMerge.textNodes[0];
5147
                            rangeStartOffset = rangeStartNode.length;
5148
                        }
5149
                        if (textNode === lastNode) {
5150
                            rangeEndNode = currentMerge.textNodes[0];
5151
                            rangeEndOffset = currentMerge.getLength();
5152
                        }
5153
                    } else {
5154
                        currentMerge = null;
5155
                    }
5156
                });
5157
 
5158
                // Test whether the first node after the range needs merging
5159
                var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
5160
 
5161
                if (nextTextNode) {
5162
                    if (!currentMerge) {
5163
                        currentMerge = new Merge(lastNode);
5164
                        merges.push(currentMerge);
5165
                    }
5166
                    currentMerge.textNodes.push(nextTextNode);
5167
                }
5168
 
5169
                // Apply the merges
5170
                if (merges.length) {
5171
                    for (var i = 0, len = merges.length; i < len; ++i) {
5172
                        merges[i].doMerge(positionsToPreserve);
5173
                    }
5174
 
5175
                    // Set the range boundaries
5176
                    range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
5177
                }
5178
            },
5179
 
5180
            createContainer: function(parentNode) {
5181
                var doc = dom.getDocument(parentNode);
5182
                var namespace;
5183
                var el = createElementNSSupported && !dom.isHtmlNamespace(parentNode) && (namespace = parentNode.namespaceURI) ?
5184
                    doc.createElementNS(parentNode.namespaceURI, this.elementTagName) :
5185
                    doc.createElement(this.elementTagName);
5186
 
5187
                this.copyPropertiesToElement(this.elementProperties, el, false);
5188
                this.copyAttributesToElement(this.elementAttributes, el);
5189
                addClass(el, this.className);
5190
                if (this.onElementCreate) {
5191
                    this.onElementCreate(el, this);
5192
                }
5193
                return el;
5194
            },
5195
 
5196
            elementHasProperties: function(el, props) {
5197
                var applier = this;
5198
                return each(props, function(p, propValue) {
5199
                    if (p == "className") {
5200
                        // For checking whether we should reuse an existing element, we just want to check that the element
5201
                        // has all the classes specified in the className property. When deciding whether the element is
5202
                        // removable when unapplying a class, there is separate special handling to check whether the
5203
                        // element has extra classes so the same simple check will do.
5204
                        return hasAllClasses(el, propValue);
5205
                    } else if (typeof propValue == "object") {
5206
                        if (!applier.elementHasProperties(el[p], propValue)) {
5207
                            return false;
5208
                        }
5209
                    } else if (el[p] !== propValue) {
5210
                        return false;
5211
                    }
5212
                });
5213
            },
5214
 
5215
            elementHasAttributes: function(el, attrs) {
5216
                return each(attrs, function(name, value) {
5217
                    if (el.getAttribute(name) !== value) {
5218
                        return false;
5219
                    }
5220
                });
5221
            },
5222
 
5223
            applyToTextNode: function(textNode, positionsToPreserve) {
5224
 
5225
                // Check whether the text node can be styled. Text within a <style> or <script> element, for example,
5226
                // should not be styled. See issue 283.
5227
                if (canTextBeStyled(textNode)) {
5228
                    var parent = textNode.parentNode;
5229
                    if (parent.childNodes.length == 1 &&
5230
                        this.useExistingElements &&
5231
                        this.appliesToElement(parent) &&
5232
                        this.elementHasProperties(parent, this.elementProperties) &&
5233
                        this.elementHasAttributes(parent, this.elementAttributes)) {
5234
 
5235
                        addClass(parent, this.className);
5236
                    } else {
5237
                        var textNodeParent = textNode.parentNode;
5238
                        var el = this.createContainer(textNodeParent);
5239
                        textNodeParent.insertBefore(el, textNode);
5240
                        el.appendChild(textNode);
5241
                    }
5242
                }
5243
 
5244
            },
5245
 
5246
            isRemovable: function(el) {
5247
                return el.tagName.toLowerCase() == this.elementTagName &&
5248
                    getSortedClassName(el) == this.elementSortedClassName &&
5249
                    this.elementHasProperties(el, this.elementProperties) &&
5250
                    !elementHasNonClassAttributes(el, this.attrExceptions) &&
5251
                    this.elementHasAttributes(el, this.elementAttributes) &&
5252
                    this.isModifiable(el);
5253
            },
5254
 
5255
            isEmptyContainer: function(el) {
5256
                var childNodeCount = el.childNodes.length;
5257
                return el.nodeType == 1 &&
5258
                    this.isRemovable(el) &&
5259
                    (childNodeCount == 0 || (childNodeCount == 1 && this.isEmptyContainer(el.firstChild)));
5260
            },
5261
 
5262
            removeEmptyContainers: function(range) {
5263
                var applier = this;
5264
                var nodesToRemove = range.getNodes([1], function(el) {
5265
                    return applier.isEmptyContainer(el);
5266
                });
5267
 
5268
                var rangesToPreserve = [range];
5269
                var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
5270
 
5271
                forEach(nodesToRemove, function(node) {
5272
                    removePreservingPositions(node, positionsToPreserve);
5273
                });
5274
 
5275
                // Update the range from the preserved boundary positions
5276
                updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
5277
            },
5278
 
5279
            undoToTextNode: function(textNode, range, ancestorWithClass, positionsToPreserve) {
5280
                if (!range.containsNode(ancestorWithClass)) {
5281
                    // Split out the portion of the ancestor from which we can remove the class
5282
                    //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
5283
                    var ancestorRange = range.cloneRange();
5284
                    ancestorRange.selectNode(ancestorWithClass);
5285
                    if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)) {
5286
                        splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, positionsToPreserve);
5287
                        range.setEndAfter(ancestorWithClass);
5288
                    }
5289
                    if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)) {
5290
                        ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, positionsToPreserve);
5291
                    }
5292
                }
5293
 
5294
                if (this.isRemovable(ancestorWithClass)) {
5295
                    replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
5296
                } else {
5297
                    removeClass(ancestorWithClass, this.className);
5298
                }
5299
            },
5300
 
5301
            splitAncestorWithClass: function(container, offset, positionsToPreserve) {
5302
                var ancestorWithClass = this.getSelfOrAncestorWithClass(container);
5303
                if (ancestorWithClass) {
5304
                    splitNodeAt(ancestorWithClass, container, offset, positionsToPreserve);
5305
                }
5306
            },
5307
 
5308
            undoToAncestor: function(ancestorWithClass, positionsToPreserve) {
5309
                if (this.isRemovable(ancestorWithClass)) {
5310
                    replaceWithOwnChildrenPreservingPositions(ancestorWithClass, positionsToPreserve);
5311
                } else {
5312
                    removeClass(ancestorWithClass, this.className);
5313
                }
5314
            },
5315
 
5316
            applyToRange: function(range, rangesToPreserve) {
5317
                var applier = this;
5318
                rangesToPreserve = rangesToPreserve || [];
5319
 
5320
                // Create an array of range boundaries to preserve
5321
                var positionsToPreserve = getRangeBoundaries(rangesToPreserve || []);
5322
 
5323
                range.splitBoundariesPreservingPositions(positionsToPreserve);
5324
 
5325
                // Tidy up the DOM by removing empty containers
5326
                if (applier.removeEmptyElements) {
5327
                    applier.removeEmptyContainers(range);
5328
                }
5329
 
5330
                var textNodes = getEffectiveTextNodes(range);
5331
 
5332
                if (textNodes.length) {
5333
                    forEach(textNodes, function(textNode) {
5334
                        if (!applier.isIgnorableWhiteSpaceNode(textNode) && !applier.getSelfOrAncestorWithClass(textNode) &&
5335
                                applier.isModifiable(textNode)) {
5336
                            applier.applyToTextNode(textNode, positionsToPreserve);
5337
                        }
5338
                    });
5339
                    var lastTextNode = textNodes[textNodes.length - 1];
5340
                    range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
5341
                    if (applier.normalize) {
5342
                        applier.postApply(textNodes, range, positionsToPreserve, false);
5343
                    }
5344
 
5345
                    // Update the ranges from the preserved boundary positions
5346
                    updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
5347
                }
5348
 
5349
                // Apply classes to any appropriate empty elements
5350
                var emptyElements = applier.getEmptyElements(range);
5351
 
5352
                forEach(emptyElements, function(el) {
5353
                    addClass(el, applier.className);
5354
                });
5355
            },
5356
 
5357
            applyToRanges: function(ranges) {
5358
 
5359
                var i = ranges.length;
5360
                while (i--) {
5361
                    this.applyToRange(ranges[i], ranges);
5362
                }
5363
 
5364
 
5365
                return ranges;
5366
            },
5367
 
5368
            applyToSelection: function(win) {
5369
                var sel = api.getSelection(win);
5370
                sel.setRanges( this.applyToRanges(sel.getAllRanges()) );
5371
            },
5372
 
5373
            undoToRange: function(range, rangesToPreserve) {
5374
                var applier = this;
5375
                // Create an array of range boundaries to preserve
5376
                rangesToPreserve = rangesToPreserve || [];
5377
                var positionsToPreserve = getRangeBoundaries(rangesToPreserve);
5378
 
5379
 
5380
                range.splitBoundariesPreservingPositions(positionsToPreserve);
5381
 
5382
                // Tidy up the DOM by removing empty containers
5383
                if (applier.removeEmptyElements) {
5384
                    applier.removeEmptyContainers(range, positionsToPreserve);
5385
                }
5386
 
5387
                var textNodes = getEffectiveTextNodes(range);
5388
                var textNode, ancestorWithClass;
5389
                var lastTextNode = textNodes[textNodes.length - 1];
5390
 
5391
                if (textNodes.length) {
5392
                    applier.splitAncestorWithClass(range.endContainer, range.endOffset, positionsToPreserve);
5393
                    applier.splitAncestorWithClass(range.startContainer, range.startOffset, positionsToPreserve);
5394
                    for (var i = 0, len = textNodes.length; i < len; ++i) {
5395
                        textNode = textNodes[i];
5396
                        ancestorWithClass = applier.getSelfOrAncestorWithClass(textNode);
5397
                        if (ancestorWithClass && applier.isModifiable(textNode)) {
5398
                            applier.undoToAncestor(ancestorWithClass, positionsToPreserve);
5399
                        }
5400
                    }
5401
                    // Ensure the range is still valid
5402
                    range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
5403
 
5404
 
5405
                    if (applier.normalize) {
5406
                        applier.postApply(textNodes, range, positionsToPreserve, true);
5407
                    }
5408
 
5409
                    // Update the ranges from the preserved boundary positions
5410
                    updateRangesFromBoundaries(rangesToPreserve, positionsToPreserve);
5411
                }
5412
 
5413
                // Remove class from any appropriate empty elements
5414
                var emptyElements = applier.getEmptyElements(range);
5415
 
5416
                forEach(emptyElements, function(el) {
5417
                    removeClass(el, applier.className);
5418
                });
5419
            },
5420
 
5421
            undoToRanges: function(ranges) {
5422
                // Get ranges returned in document order
5423
                var i = ranges.length;
5424
 
5425
                while (i--) {
5426
                    this.undoToRange(ranges[i], ranges);
5427
                }
5428
 
5429
                return ranges;
5430
            },
5431
 
5432
            undoToSelection: function(win) {
5433
                var sel = api.getSelection(win);
5434
                var ranges = api.getSelection(win).getAllRanges();
5435
                this.undoToRanges(ranges);
5436
                sel.setRanges(ranges);
5437
            },
5438
 
5439
            isAppliedToRange: function(range) {
5440
                if (range.collapsed || range.toString() == "") {
5441
                    return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
5442
                } else {
5443
                    var textNodes = range.getNodes( [3] );
5444
                    if (textNodes.length)
5445
                    for (var i = 0, textNode; textNode = textNodes[i++]; ) {
5446
                        if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) &&
5447
                                this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
5448
                            return false;
5449
                        }
5450
                    }
5451
                    return true;
5452
                }
5453
            },
5454
 
5455
            isAppliedToRanges: function(ranges) {
5456
                var i = ranges.length;
5457
                if (i == 0) {
5458
                    return false;
5459
                }
5460
                while (i--) {
5461
                    if (!this.isAppliedToRange(ranges[i])) {
5462
                        return false;
5463
                    }
5464
                }
5465
                return true;
5466
            },
5467
 
5468
            isAppliedToSelection: function(win) {
5469
                var sel = api.getSelection(win);
5470
                return this.isAppliedToRanges(sel.getAllRanges());
5471
            },
5472
 
5473
            toggleRange: function(range) {
5474
                if (this.isAppliedToRange(range)) {
5475
                    this.undoToRange(range);
5476
                } else {
5477
                    this.applyToRange(range);
5478
                }
5479
            },
5480
 
5481
            toggleSelection: function(win) {
5482
                if (this.isAppliedToSelection(win)) {
5483
                    this.undoToSelection(win);
5484
                } else {
5485
                    this.applyToSelection(win);
5486
                }
5487
            },
5488
 
5489
            getElementsWithClassIntersectingRange: function(range) {
5490
                var elements = [];
5491
                var applier = this;
5492
                range.getNodes([3], function(textNode) {
5493
                    var el = applier.getSelfOrAncestorWithClass(textNode);
5494
                    if (el && !contains(elements, el)) {
5495
                        elements.push(el);
5496
                    }
5497
                });
5498
                return elements;
5499
            },
5500
 
5501
            detach: function() {}
5502
        };
5503
 
5504
        function createClassApplier(className, options, tagNames) {
5505
            return new ClassApplier(className, options, tagNames);
5506
        }
5507
 
5508
        ClassApplier.util = {
5509
            hasClass: hasClass,
5510
            addClass: addClass,
5511
            removeClass: removeClass,
5512
            getClass: getClass,
5513
            hasSameClasses: haveSameClasses,
5514
            hasAllClasses: hasAllClasses,
5515
            replaceWithOwnChildren: replaceWithOwnChildrenPreservingPositions,
5516
            elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
5517
            elementHasNonClassAttributes: elementHasNonClassAttributes,
5518
            splitNodeAt: splitNodeAt,
5519
            isEditableElement: isEditableElement,
5520
            isEditingHost: isEditingHost,
5521
            isEditable: isEditable
5522
        };
5523
 
5524
        api.CssClassApplier = api.ClassApplier = ClassApplier;
5525
        api.createClassApplier = createClassApplier;
5526
        util.createAliasForDeprecatedMethod(api, "createCssClassApplier", "createClassApplier", module);
5527
    });
5528
 
5529
    return rangy;
5530
}, this);
5531
 
5532
/**
5533
 * Highlighter module for Rangy, a cross-browser JavaScript range and selection library
5534
 * https://github.com/timdown/rangy
5535
 *
5536
 * Depends on Rangy core, ClassApplier and optionally TextRange modules.
5537
 *
5538
 * Copyright 2022, Tim Down
5539
 * Licensed under the MIT license.
5540
 * Version: 1.3.1
5541
 * Build date: 17 August 2022
5542
 */
5543
(function(factory, root) {
5544
    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
5545
    factory(root.rangy);
5546
})(function(rangy) {
5547
    rangy.createModule("Highlighter", ["ClassApplier"], function(api, module) {
5548
        var dom = api.dom;
5549
        var contains = dom.arrayContains;
5550
        var getBody = dom.getBody;
5551
        var createOptions = api.util.createOptions;
5552
        var forEach = api.util.forEach;
5553
        var nextHighlightId = 1;
5554
 
5555
        // Puts highlights in order, last in document first.
5556
        function compareHighlights(h1, h2) {
5557
            return h1.characterRange.start - h2.characterRange.start;
5558
        }
5559
 
5560
        function getContainerElement(doc, id) {
5561
            return id ? doc.getElementById(id) : getBody(doc);
5562
        }
5563
 
5564
        /*----------------------------------------------------------------------------------------------------------------*/
5565
 
5566
        var highlighterTypes = {};
5567
 
5568
        function HighlighterType(type, converterCreator) {
5569
            this.type = type;
5570
            this.converterCreator = converterCreator;
5571
        }
5572
 
5573
        HighlighterType.prototype.create = function() {
5574
            var converter = this.converterCreator();
5575
            converter.type = this.type;
5576
            return converter;
5577
        };
5578
 
5579
        function registerHighlighterType(type, converterCreator) {
5580
            highlighterTypes[type] = new HighlighterType(type, converterCreator);
5581
        }
5582
 
5583
        function getConverter(type) {
5584
            var highlighterType = highlighterTypes[type];
5585
            if (highlighterType instanceof HighlighterType) {
5586
                return highlighterType.create();
5587
            } else {
5588
                throw new Error("Highlighter type '" + type + "' is not valid");
5589
            }
5590
        }
5591
 
5592
        api.registerHighlighterType = registerHighlighterType;
5593
 
5594
        /*----------------------------------------------------------------------------------------------------------------*/
5595
 
5596
        function CharacterRange(start, end) {
5597
            this.start = start;
5598
            this.end = end;
5599
        }
5600
 
5601
        CharacterRange.prototype = {
5602
            intersects: function(charRange) {
5603
                return this.start < charRange.end && this.end > charRange.start;
5604
            },
5605
 
5606
            isContiguousWith: function(charRange) {
5607
                return this.start == charRange.end || this.end == charRange.start;
5608
            },
5609
 
5610
            union: function(charRange) {
5611
                return new CharacterRange(Math.min(this.start, charRange.start), Math.max(this.end, charRange.end));
5612
            },
5613
 
5614
            intersection: function(charRange) {
5615
                return new CharacterRange(Math.max(this.start, charRange.start), Math.min(this.end, charRange.end));
5616
            },
5617
 
5618
            getComplements: function(charRange) {
5619
                var ranges = [];
5620
                if (this.start >= charRange.start) {
5621
                    if (this.end <= charRange.end) {
5622
                        return [];
5623
                    }
5624
                    ranges.push(new CharacterRange(charRange.end, this.end));
5625
                } else {
5626
                    ranges.push(new CharacterRange(this.start, Math.min(this.end, charRange.start)));
5627
                    if (this.end > charRange.end) {
5628
                        ranges.push(new CharacterRange(charRange.end, this.end));
5629
                    }
5630
                }
5631
                return ranges;
5632
            },
5633
 
5634
            toString: function() {
5635
                return "[CharacterRange(" + this.start + ", " + this.end + ")]";
5636
            }
5637
        };
5638
 
5639
        CharacterRange.fromCharacterRange = function(charRange) {
5640
            return new CharacterRange(charRange.start, charRange.end);
5641
        };
5642
 
5643
        /*----------------------------------------------------------------------------------------------------------------*/
5644
 
5645
        var textContentConverter = {
5646
            rangeToCharacterRange: function(range, containerNode) {
5647
                var bookmark = range.getBookmark(containerNode);
5648
                return new CharacterRange(bookmark.start, bookmark.end);
5649
            },
5650
 
5651
            characterRangeToRange: function(doc, characterRange, containerNode) {
5652
                var range = api.createRange(doc);
5653
                range.moveToBookmark({
5654
                    start: characterRange.start,
5655
                    end: characterRange.end,
5656
                    containerNode: containerNode
5657
                });
5658
 
5659
                return range;
5660
            },
5661
 
5662
            serializeSelection: function(selection, containerNode) {
5663
                var ranges = selection.getAllRanges(), rangeCount = ranges.length;
5664
                var rangeInfos = [];
5665
 
5666
                var backward = rangeCount == 1 && selection.isBackward();
5667
 
5668
                for (var i = 0, len = ranges.length; i < len; ++i) {
5669
                    rangeInfos[i] = {
5670
                        characterRange: this.rangeToCharacterRange(ranges[i], containerNode),
5671
                        backward: backward
5672
                    };
5673
                }
5674
 
5675
                return rangeInfos;
5676
            },
5677
 
5678
            restoreSelection: function(selection, savedSelection, containerNode) {
5679
                selection.removeAllRanges();
5680
                var doc = selection.win.document;
5681
                for (var i = 0, len = savedSelection.length, range, rangeInfo, characterRange; i < len; ++i) {
5682
                    rangeInfo = savedSelection[i];
5683
                    characterRange = rangeInfo.characterRange;
5684
                    range = this.characterRangeToRange(doc, rangeInfo.characterRange, containerNode);
5685
                    selection.addRange(range, rangeInfo.backward);
5686
                }
5687
            }
5688
        };
5689
 
5690
        registerHighlighterType("textContent", function() {
5691
            return textContentConverter;
5692
        });
5693
 
5694
        /*----------------------------------------------------------------------------------------------------------------*/
5695
 
5696
        // Lazily load the TextRange-based converter so that the dependency is only checked when required.
5697
        registerHighlighterType("TextRange", (function() {
5698
            var converter;
5699
 
5700
            return function() {
5701
                if (!converter) {
5702
                    // Test that textRangeModule exists and is supported
5703
                    var textRangeModule = api.modules.TextRange;
5704
                    if (!textRangeModule) {
5705
                        throw new Error("TextRange module is missing.");
5706
                    } else if (!textRangeModule.supported) {
5707
                        throw new Error("TextRange module is present but not supported.");
5708
                    }
5709
 
5710
                    converter = {
5711
                        rangeToCharacterRange: function(range, containerNode) {
5712
                            return CharacterRange.fromCharacterRange( range.toCharacterRange(containerNode) );
5713
                        },
5714
 
5715
                        characterRangeToRange: function(doc, characterRange, containerNode) {
5716
                            var range = api.createRange(doc);
5717
                            range.selectCharacters(containerNode, characterRange.start, characterRange.end);
5718
                            return range;
5719
                        },
5720
 
5721
                        serializeSelection: function(selection, containerNode) {
5722
                            return selection.saveCharacterRanges(containerNode);
5723
                        },
5724
 
5725
                        restoreSelection: function(selection, savedSelection, containerNode) {
5726
                            selection.restoreCharacterRanges(containerNode, savedSelection);
5727
                        }
5728
                    };
5729
                }
5730
 
5731
                return converter;
5732
            };
5733
        })());
5734
 
5735
        /*----------------------------------------------------------------------------------------------------------------*/
5736
 
5737
        function Highlight(doc, characterRange, classApplier, converter, id, containerElementId) {
5738
            if (id) {
5739
                this.id = id;
5740
                nextHighlightId = Math.max(nextHighlightId, id + 1);
5741
            } else {
5742
                this.id = nextHighlightId++;
5743
            }
5744
            this.characterRange = characterRange;
5745
            this.doc = doc;
5746
            this.classApplier = classApplier;
5747
            this.converter = converter;
5748
            this.containerElementId = containerElementId || null;
5749
            this.applied = false;
5750
        }
5751
 
5752
        Highlight.prototype = {
5753
            getContainerElement: function() {
5754
                return getContainerElement(this.doc, this.containerElementId);
5755
            },
5756
 
5757
            getRange: function() {
5758
                return this.converter.characterRangeToRange(this.doc, this.characterRange, this.getContainerElement());
5759
            },
5760
 
5761
            fromRange: function(range) {
5762
                this.characterRange = this.converter.rangeToCharacterRange(range, this.getContainerElement());
5763
            },
5764
 
5765
            getText: function() {
5766
                return this.getRange().toString();
5767
            },
5768
 
5769
            containsElement: function(el) {
5770
                return this.getRange().containsNodeContents(el.firstChild);
5771
            },
5772
 
5773
            unapply: function() {
5774
                this.classApplier.undoToRange(this.getRange());
5775
                this.applied = false;
5776
            },
5777
 
5778
            apply: function() {
5779
                this.classApplier.applyToRange(this.getRange());
5780
                this.applied = true;
5781
            },
5782
 
5783
            getHighlightElements: function() {
5784
                return this.classApplier.getElementsWithClassIntersectingRange(this.getRange());
5785
            },
5786
 
5787
            toString: function() {
5788
                return "[Highlight(ID: " + this.id + ", class: " + this.classApplier.className + ", character range: " +
5789
                    this.characterRange.start + " - " + this.characterRange.end + ")]";
5790
            }
5791
        };
5792
 
5793
        /*----------------------------------------------------------------------------------------------------------------*/
5794
 
5795
        function Highlighter(doc, type) {
5796
            type = type || "textContent";
5797
            this.doc = doc || document;
5798
            this.classAppliers = {};
5799
            this.highlights = [];
5800
            this.converter = getConverter(type);
5801
        }
5802
 
5803
        Highlighter.prototype = {
5804
            addClassApplier: function(classApplier) {
5805
                this.classAppliers[classApplier.className] = classApplier;
5806
            },
5807
 
5808
            getHighlightForElement: function(el) {
5809
                var highlights = this.highlights;
5810
                for (var i = 0, len = highlights.length; i < len; ++i) {
5811
                    if (highlights[i].containsElement(el)) {
5812
                        return highlights[i];
5813
                    }
5814
                }
5815
                return null;
5816
            },
5817
 
5818
            removeHighlights: function(highlights) {
5819
                for (var i = 0, len = this.highlights.length, highlight; i < len; ++i) {
5820
                    highlight = this.highlights[i];
5821
                    if (contains(highlights, highlight)) {
5822
                        highlight.unapply();
5823
                        this.highlights.splice(i--, 1);
5824
                    }
5825
                }
5826
            },
5827
 
5828
            removeAllHighlights: function() {
5829
                this.removeHighlights(this.highlights);
5830
            },
5831
 
5832
            getIntersectingHighlights: function(ranges) {
5833
                // Test each range against each of the highlighted ranges to see whether they overlap
5834
                var intersectingHighlights = [], highlights = this.highlights;
5835
                forEach(ranges, function(range) {
5836
                    //var selCharRange = converter.rangeToCharacterRange(range);
5837
                    forEach(highlights, function(highlight) {
5838
                        if (range.intersectsRange( highlight.getRange() ) && !contains(intersectingHighlights, highlight)) {
5839
                            intersectingHighlights.push(highlight);
5840
                        }
5841
                    });
5842
                });
5843
 
5844
                return intersectingHighlights;
5845
            },
5846
 
5847
            highlightCharacterRanges: function(className, charRanges, options) {
5848
                var i, len, j;
5849
                var highlights = this.highlights;
5850
                var converter = this.converter;
5851
                var doc = this.doc;
5852
                var highlightsToRemove = [];
5853
                var classApplier = className ? this.classAppliers[className] : null;
5854
 
5855
                options = createOptions(options, {
5856
                    containerElementId: null,
5857
                    exclusive: true
5858
                });
5859
 
5860
                var containerElementId = options.containerElementId;
5861
                var exclusive = options.exclusive;
5862
 
5863
                var containerElement, containerElementRange, containerElementCharRange;
5864
                if (containerElementId) {
5865
                    containerElement = this.doc.getElementById(containerElementId);
5866
                    if (containerElement) {
5867
                        containerElementRange = api.createRange(this.doc);
5868
                        containerElementRange.selectNodeContents(containerElement);
5869
                        containerElementCharRange = new CharacterRange(0, containerElementRange.toString().length);
5870
                    }
5871
                }
5872
 
5873
                var charRange, highlightCharRange, removeHighlight, isSameClassApplier, highlightsToKeep, splitHighlight;
5874
 
5875
                for (i = 0, len = charRanges.length; i < len; ++i) {
5876
                    charRange = charRanges[i];
5877
                    highlightsToKeep = [];
5878
 
5879
                    // Restrict character range to container element, if it exists
5880
                    if (containerElementCharRange) {
5881
                        charRange = charRange.intersection(containerElementCharRange);
5882
                    }
5883
 
5884
                    // Ignore empty ranges
5885
                    if (charRange.start == charRange.end) {
5886
                        continue;
5887
                    }
5888
 
5889
                    // Check for intersection with existing highlights. For each intersection, create a new highlight
5890
                    // which is the union of the highlight range and the selected range
5891
                    for (j = 0; j < highlights.length; ++j) {
5892
                        removeHighlight = false;
5893
 
5894
                        if (containerElementId == highlights[j].containerElementId) {
5895
                            highlightCharRange = highlights[j].characterRange;
5896
                            isSameClassApplier = (classApplier == highlights[j].classApplier);
5897
                            splitHighlight = !isSameClassApplier && exclusive;
5898
 
5899
                            // Replace the existing highlight if it needs to be:
5900
                            //  1. merged (isSameClassApplier)
5901
                            //  2. partially or entirely erased (className === null)
5902
                            //  3. partially or entirely replaced (isSameClassApplier == false && exclusive == true)
5903
                            if (    (highlightCharRange.intersects(charRange) || highlightCharRange.isContiguousWith(charRange)) &&
5904
                                    (isSameClassApplier || splitHighlight) ) {
5905
 
5906
                                // Remove existing highlights, keeping the unselected parts
5907
                                if (splitHighlight) {
5908
                                    forEach(highlightCharRange.getComplements(charRange), function(rangeToAdd) {
5909
                                        highlightsToKeep.push( new Highlight(doc, rangeToAdd, highlights[j].classApplier, converter, null, containerElementId) );
5910
                                    });
5911
                                }
5912
 
5913
                                removeHighlight = true;
5914
                                if (isSameClassApplier) {
5915
                                    charRange = highlightCharRange.union(charRange);
5916
                                }
5917
                            }
5918
                        }
5919
 
5920
                        if (removeHighlight) {
5921
                            highlightsToRemove.push(highlights[j]);
5922
                            highlights[j] = new Highlight(doc, highlightCharRange.union(charRange), classApplier, converter, null, containerElementId);
5923
                        } else {
5924
                            highlightsToKeep.push(highlights[j]);
5925
                        }
5926
                    }
5927
 
5928
                    // Add new range
5929
                    if (classApplier) {
5930
                        highlightsToKeep.push(new Highlight(doc, charRange, classApplier, converter, null, containerElementId));
5931
                    }
5932
                    this.highlights = highlights = highlightsToKeep;
5933
                }
5934
 
5935
                // Remove the old highlights
5936
                forEach(highlightsToRemove, function(highlightToRemove) {
5937
                    highlightToRemove.unapply();
5938
                });
5939
 
5940
                // Apply new highlights
5941
                var newHighlights = [];
5942
                forEach(highlights, function(highlight) {
5943
                    if (!highlight.applied) {
5944
                        highlight.apply();
5945
                        newHighlights.push(highlight);
5946
                    }
5947
                });
5948
 
5949
                return newHighlights;
5950
            },
5951
 
5952
            highlightRanges: function(className, ranges, options) {
5953
                var selCharRanges = [];
5954
                var converter = this.converter;
5955
 
5956
                options = createOptions(options, {
5957
                    containerElement: null,
5958
                    exclusive: true
5959
                });
5960
 
5961
                var containerElement = options.containerElement;
5962
                var containerElementId = containerElement ? containerElement.id : null;
5963
                var containerElementRange;
5964
                if (containerElement) {
5965
                    containerElementRange = api.createRange(containerElement);
5966
                    containerElementRange.selectNodeContents(containerElement);
5967
                }
5968
 
5969
                forEach(ranges, function(range) {
5970
                    var scopedRange = containerElement ? containerElementRange.intersection(range) : range;
5971
                    selCharRanges.push( converter.rangeToCharacterRange(scopedRange, containerElement || getBody(range.getDocument())) );
5972
                });
5973
 
5974
                return this.highlightCharacterRanges(className, selCharRanges, {
5975
                    containerElementId: containerElementId,
5976
                    exclusive: options.exclusive
5977
                });
5978
            },
5979
 
5980
            highlightSelection: function(className, options) {
5981
                var converter = this.converter;
5982
                var classApplier = className ? this.classAppliers[className] : false;
5983
 
5984
                options = createOptions(options, {
5985
                    containerElementId: null,
5986
                    exclusive: true
5987
                });
5988
 
5989
                var containerElementId = options.containerElementId;
5990
                var exclusive = options.exclusive;
5991
                var selection = options.selection || api.getSelection(this.doc);
5992
                var doc = selection.win.document;
5993
                var containerElement = getContainerElement(doc, containerElementId);
5994
 
5995
                if (!classApplier && className !== false) {
5996
                    throw new Error("No class applier found for class '" + className + "'");
5997
                }
5998
 
5999
                // Store the existing selection as character ranges
6000
                var serializedSelection = converter.serializeSelection(selection, containerElement);
6001
 
6002
                // Create an array of selected character ranges
6003
                var selCharRanges = [];
6004
                forEach(serializedSelection, function(rangeInfo) {
6005
                    selCharRanges.push( CharacterRange.fromCharacterRange(rangeInfo.characterRange) );
6006
                });
6007
 
6008
                var newHighlights = this.highlightCharacterRanges(className, selCharRanges, {
6009
                    containerElementId: containerElementId,
6010
                    exclusive: exclusive
6011
                });
6012
 
6013
                // Restore selection
6014
                converter.restoreSelection(selection, serializedSelection, containerElement);
6015
 
6016
                return newHighlights;
6017
            },
6018
 
6019
            unhighlightSelection: function(selection) {
6020
                selection = selection || api.getSelection(this.doc);
6021
                var intersectingHighlights = this.getIntersectingHighlights( selection.getAllRanges() );
6022
                this.removeHighlights(intersectingHighlights);
6023
                selection.removeAllRanges();
6024
                return intersectingHighlights;
6025
            },
6026
 
6027
            getHighlightsInSelection: function(selection) {
6028
                selection = selection || api.getSelection(this.doc);
6029
                return this.getIntersectingHighlights(selection.getAllRanges());
6030
            },
6031
 
6032
            selectionOverlapsHighlight: function(selection) {
6033
                return this.getHighlightsInSelection(selection).length > 0;
6034
            },
6035
 
6036
            serialize: function(options) {
6037
                var highlighter = this;
6038
                var highlights = highlighter.highlights;
6039
                var serializedType, serializedHighlights, convertType, serializationConverter;
6040
 
6041
                highlights.sort(compareHighlights);
6042
                options = createOptions(options, {
6043
                    serializeHighlightText: false,
6044
                    type: highlighter.converter.type
6045
                });
6046
 
6047
                serializedType = options.type;
6048
                convertType = (serializedType != highlighter.converter.type);
6049
 
6050
                if (convertType) {
6051
                    serializationConverter = getConverter(serializedType);
6052
                }
6053
 
6054
                serializedHighlights = ["type:" + serializedType];
6055
 
6056
                forEach(highlights, function(highlight) {
6057
                    var characterRange = highlight.characterRange;
6058
                    var containerElement;
6059
 
6060
                    // Convert to the current Highlighter's type, if different from the serialization type
6061
                    if (convertType) {
6062
                        containerElement = highlight.getContainerElement();
6063
                        characterRange = serializationConverter.rangeToCharacterRange(
6064
                            highlighter.converter.characterRangeToRange(highlighter.doc, characterRange, containerElement),
6065
                            containerElement
6066
                        );
6067
                    }
6068
 
6069
                    var parts = [
6070
                        characterRange.start,
6071
                        characterRange.end,
6072
                        highlight.id,
6073
                        highlight.classApplier.className,
6074
                        highlight.containerElementId
6075
                    ];
6076
 
6077
                    if (options.serializeHighlightText) {
6078
                        parts.push(highlight.getText());
6079
                    }
6080
                    serializedHighlights.push( parts.join("$") );
6081
                });
6082
 
6083
                return serializedHighlights.join("|");
6084
            },
6085
 
6086
            deserialize: function(serialized) {
6087
                var serializedHighlights = serialized.split("|");
6088
                var highlights = [];
6089
 
6090
                var firstHighlight = serializedHighlights[0];
6091
                var regexResult;
6092
                var serializationType, serializationConverter, convertType = false;
6093
                if ( firstHighlight && (regexResult = /^type:(\w+)$/.exec(firstHighlight)) ) {
6094
                    serializationType = regexResult[1];
6095
                    if (serializationType != this.converter.type) {
6096
                        serializationConverter = getConverter(serializationType);
6097
                        convertType = true;
6098
                    }
6099
                    serializedHighlights.shift();
6100
                } else {
6101
                    throw new Error("Serialized highlights are invalid.");
6102
                }
6103
 
6104
                var classApplier, highlight, characterRange, containerElementId, containerElement;
6105
 
6106
                for (var i = serializedHighlights.length, parts; i-- > 0; ) {
6107
                    parts = serializedHighlights[i].split("$");
6108
                    characterRange = new CharacterRange(+parts[0], +parts[1]);
6109
                    containerElementId = parts[4] || null;
6110
 
6111
                    // Convert to the current Highlighter's type, if different from the serialization type
6112
                    if (convertType) {
6113
                        containerElement = getContainerElement(this.doc, containerElementId);
6114
                        characterRange = this.converter.rangeToCharacterRange(
6115
                            serializationConverter.characterRangeToRange(this.doc, characterRange, containerElement),
6116
                            containerElement
6117
                        );
6118
                    }
6119
 
6120
                    classApplier = this.classAppliers[ parts[3] ];
6121
 
6122
                    if (!classApplier) {
6123
                        throw new Error("No class applier found for class '" + parts[3] + "'");
6124
                    }
6125
 
6126
                    highlight = new Highlight(this.doc, characterRange, classApplier, this.converter, parseInt(parts[2]), containerElementId);
6127
                    highlight.apply();
6128
                    highlights.push(highlight);
6129
                }
6130
                this.highlights = highlights;
6131
            }
6132
        };
6133
 
6134
        api.Highlighter = Highlighter;
6135
 
6136
        api.createHighlighter = function(doc, rangeCharacterOffsetConverterType) {
6137
            return new Highlighter(doc, rangeCharacterOffsetConverterType);
6138
        };
6139
    });
6140
 
6141
    return rangy;
6142
}, this);
6143
 
6144
/**
6145
 * Text range module for Rangy.
6146
 * Text-based manipulation and searching of ranges and selections.
6147
 *
6148
 * Features
6149
 *
6150
 * - Ability to move range boundaries by character or word offsets
6151
 * - Customizable word tokenizer
6152
 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
6153
 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
6154
 *   sensitivity
6155
 * - Selection and range save/restore as text offsets within a node
6156
 * - Methods to return visible text within a range or selection
6157
 * - innerText method for elements
6158
 *
6159
 * References
6160
 *
6161
 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
6162
 * http://aryeh.name/spec/innertext/innertext.html
6163
 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
6164
 *
6165
 * Part of Rangy, a cross-browser JavaScript range and selection library
6166
 * https://github.com/timdown/rangy
6167
 *
6168
 * Depends on Rangy core.
6169
 *
6170
 * Copyright 2022, Tim Down
6171
 * Licensed under the MIT license.
6172
 * Version: 1.3.1
6173
 * Build date: 17 August 2022
6174
 */
6175
 
6176
/**
6177
 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
6178
 *
6179
 * First, a <br>: this is relatively simple. For the following HTML:
6180
 *
6181
 * 1 <br>2
6182
 *
6183
 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
6184
 *   textarea, the space is present) and allow the caret to be placed after it.
6185
 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
6186
 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right
6187
 *   arrow keys show this) and includes the space in the selection.
6188
 *
6189
 * The other case is the line break or breaks implied by block elements. For the following HTML:
6190
 *
6191
 * <p>1 </p><p>2<p>
6192
 *
6193
 * - WebKit does not acknowledge the space in any way
6194
 * - Firefox, IE and Opera as per <br>
6195
 *
6196
 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
6197
 *
6198
 * <p style="white-space: pre-line">1
6199
 * 2</p>
6200
 *
6201
 * - Firefox and WebKit include the space in caret positions
6202
 * - IE does not support pre-line up to and including version 9
6203
 * - Opera ignores the space
6204
 * - Trailing space only renders if there is a non-collapsed character in the line
6205
 *
6206
 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
6207
 * feature-tested
6208
 */
6209
(function(factory, root) {
6210
    // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
6211
    factory(root.rangy);
6212
})(function(rangy) {
6213
    rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
6214
        var UNDEF = "undefined";
6215
        var CHARACTER = "character", WORD = "word";
6216
        var dom = api.dom, util = api.util;
6217
        var extend = util.extend;
6218
        var createOptions = util.createOptions;
6219
        var getBody = dom.getBody;
6220
 
6221
 
6222
        var spacesRegex = /^[ \t\f\r\n]+$/;
6223
        var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
6224
        var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
6225
        var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
6226
        var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
6227
 
6228
        var defaultLanguage = "en";
6229
 
6230
        var isDirectionBackward = api.Selection.isDirectionBackward;
6231
 
6232
        // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
6233
        // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
6234
        var trailingSpaceInBlockCollapses = false;
6235
        var trailingSpaceBeforeBrCollapses = false;
6236
        var trailingSpaceBeforeBlockCollapses = false;
6237
        var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
6238
 
6239
        (function() {
6240
            var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
6241
            var p = el.firstChild;
6242
            var sel = api.getSelection();
6243
            sel.collapse(p.lastChild, 2);
6244
            sel.setStart(p.firstChild, 0);
6245
            trailingSpaceInBlockCollapses = ("" + sel).length == 1;
6246
 
6247
            el.innerHTML = "1 <br />";
6248
            sel.collapse(el, 2);
6249
            sel.setStart(el.firstChild, 0);
6250
            trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
6251
 
6252
            el.innerHTML = "1 <p>1</p>";
6253
            sel.collapse(el, 2);
6254
            sel.setStart(el.firstChild, 0);
6255
            trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
6256
 
6257
            dom.removeNode(el);
6258
            sel.removeAllRanges();
6259
        })();
6260
 
6261
        /*----------------------------------------------------------------------------------------------------------------*/
6262
 
6263
        // This function must create word and non-word tokens for the whole of the text supplied to it
6264
        function defaultTokenizer(chars, wordOptions) {
6265
            var word = chars.join(""), result, tokenRanges = [];
6266
 
6267
            function createTokenRange(start, end, isWord) {
6268
                tokenRanges.push( { start: start, end: end, isWord: isWord } );
6269
            }
6270
 
6271
            // Match words and mark characters
6272
            var lastWordEnd = 0, wordStart, wordEnd;
6273
            while ( (result = wordOptions.wordRegex.exec(word)) ) {
6274
                wordStart = result.index;
6275
                wordEnd = wordStart + result[0].length;
6276
 
6277
                // Create token for non-word characters preceding this word
6278
                if (wordStart > lastWordEnd) {
6279
                    createTokenRange(lastWordEnd, wordStart, false);
6280
                }
6281
 
6282
                // Get trailing space characters for word
6283
                if (wordOptions.includeTrailingSpace) {
6284
                    while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
6285
                        ++wordEnd;
6286
                    }
6287
                }
6288
                createTokenRange(wordStart, wordEnd, true);
6289
                lastWordEnd = wordEnd;
6290
            }
6291
 
6292
            // Create token for trailing non-word characters, if any exist
6293
            if (lastWordEnd < chars.length) {
6294
                createTokenRange(lastWordEnd, chars.length, false);
6295
            }
6296
 
6297
            return tokenRanges;
6298
        }
6299
 
6300
        function convertCharRangeToToken(chars, tokenRange) {
6301
            var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
6302
            var token = {
6303
                isWord: tokenRange.isWord,
6304
                chars: tokenChars,
6305
                toString: function() {
6306
                    return tokenChars.join("");
6307
                }
6308
            };
6309
            for (var i = 0, len = tokenChars.length; i < len; ++i) {
6310
                tokenChars[i].token = token;
6311
            }
6312
            return token;
6313
        }
6314
 
6315
        function tokenize(chars, wordOptions, tokenizer) {
6316
            var tokenRanges = tokenizer(chars, wordOptions);
6317
            var tokens = [];
6318
            for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
6319
                tokens.push( convertCharRangeToToken(chars, tokenRange) );
6320
            }
6321
            return tokens;
6322
        }
6323
 
6324
        var defaultCharacterOptions = {
6325
            includeBlockContentTrailingSpace: true,
6326
            includeSpaceBeforeBr: true,
6327
            includeSpaceBeforeBlock: true,
6328
            includePreLineTrailingSpace: true,
6329
            ignoreCharacters: ""
6330
        };
6331
 
6332
        function normalizeIgnoredCharacters(ignoredCharacters) {
6333
            // Check if character is ignored
6334
            var ignoredChars = ignoredCharacters || "";
6335
 
6336
            // Normalize ignored characters into a string consisting of characters in ascending order of character code
6337
            var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
6338
            ignoredCharsArray.sort(function(char1, char2) {
6339
                return char1.charCodeAt(0) - char2.charCodeAt(0);
6340
            });
6341
 
6342
            /// Convert back to a string and remove duplicates
6343
            return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
6344
        }
6345
 
6346
        var defaultCaretCharacterOptions = {
6347
            includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
6348
            includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
6349
            includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
6350
            includePreLineTrailingSpace: true
6351
        };
6352
 
6353
        var defaultWordOptions = {
6354
            "en": {
6355
                wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
6356
                includeTrailingSpace: false,
6357
                tokenizer: defaultTokenizer
6358
            }
6359
        };
6360
 
6361
        var defaultFindOptions = {
6362
            caseSensitive: false,
6363
            withinRange: null,
6364
            wholeWordsOnly: false,
6365
            wrap: false,
6366
            direction: "forward",
6367
            wordOptions: null,
6368
            characterOptions: null
6369
        };
6370
 
6371
        var defaultMoveOptions = {
6372
            wordOptions: null,
6373
            characterOptions: null
6374
        };
6375
 
6376
        var defaultExpandOptions = {
6377
            wordOptions: null,
6378
            characterOptions: null,
6379
            trim: false,
6380
            trimStart: true,
6381
            trimEnd: true
6382
        };
6383
 
6384
        var defaultWordIteratorOptions = {
6385
            wordOptions: null,
6386
            characterOptions: null,
6387
            direction: "forward"
6388
        };
6389
 
6390
        function createWordOptions(options) {
6391
            var lang, defaults;
6392
            if (!options) {
6393
                return defaultWordOptions[defaultLanguage];
6394
            } else {
6395
                lang = options.language || defaultLanguage;
6396
                defaults = {};
6397
                extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
6398
                extend(defaults, options);
6399
                return defaults;
6400
            }
6401
        }
6402
 
6403
        function createNestedOptions(optionsParam, defaults) {
6404
            var options = createOptions(optionsParam, defaults);
6405
            if (defaults.hasOwnProperty("wordOptions")) {
6406
                options.wordOptions = createWordOptions(options.wordOptions);
6407
            }
6408
            if (defaults.hasOwnProperty("characterOptions")) {
6409
                options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
6410
            }
6411
            return options;
6412
        }
6413
 
6414
        /*----------------------------------------------------------------------------------------------------------------*/
6415
 
6416
        /* DOM utility functions */
6417
        var getComputedStyleProperty = dom.getComputedStyleProperty;
6418
 
6419
        // Create cachable versions of DOM functions
6420
 
6421
        // Test for old IE's incorrect display properties
6422
        var tableCssDisplayBlock;
6423
        (function() {
6424
            var table = document.createElement("table");
6425
            var body = getBody(document);
6426
            body.appendChild(table);
6427
            tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
6428
            body.removeChild(table);
6429
        })();
6430
 
6431
        var defaultDisplayValueForTag = {
6432
            table: "table",
6433
            caption: "table-caption",
6434
            colgroup: "table-column-group",
6435
            col: "table-column",
6436
            thead: "table-header-group",
6437
            tbody: "table-row-group",
6438
            tfoot: "table-footer-group",
6439
            tr: "table-row",
6440
            td: "table-cell",
6441
            th: "table-cell"
6442
        };
6443
 
6444
        // Corrects IE's "block" value for table-related elements
6445
        function getComputedDisplay(el, win) {
6446
            var display = getComputedStyleProperty(el, "display", win);
6447
            var tagName = el.tagName.toLowerCase();
6448
            return (display == "block" &&
6449
                    tableCssDisplayBlock &&
6450
                    defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
6451
                defaultDisplayValueForTag[tagName] : display;
6452
        }
6453
 
6454
        function isHidden(node) {
6455
            var ancestors = getAncestorsAndSelf(node);
6456
            for (var i = 0, len = ancestors.length; i < len; ++i) {
6457
                if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
6458
                    return true;
6459
                }
6460
            }
6461
 
6462
            return false;
6463
        }
6464
 
6465
        function isVisibilityHiddenTextNode(textNode) {
6466
            var el;
6467
            return textNode.nodeType == 3 &&
6468
                (el = textNode.parentNode) &&
6469
                getComputedStyleProperty(el, "visibility") == "hidden";
6470
        }
6471
 
6472
        /*----------------------------------------------------------------------------------------------------------------*/
6473
 
6474
 
6475
        // "A block node is either an Element whose "display" property does not have
6476
        // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
6477
        // Document, or a DocumentFragment."
6478
        function isBlockNode(node) {
6479
            return node &&
6480
                ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
6481
                node.nodeType == 9 || node.nodeType == 11);
6482
        }
6483
 
6484
        function getLastDescendantOrSelf(node) {
6485
            var lastChild = node.lastChild;
6486
            return lastChild ? getLastDescendantOrSelf(lastChild) : node;
6487
        }
6488
 
6489
        function containsPositions(node) {
6490
            return dom.isCharacterDataNode(node) ||
6491
                !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
6492
        }
6493
 
6494
        function getAncestors(node) {
6495
            var ancestors = [];
6496
            while (node.parentNode) {
6497
                ancestors.unshift(node.parentNode);
6498
                node = node.parentNode;
6499
            }
6500
            return ancestors;
6501
        }
6502
 
6503
        function getAncestorsAndSelf(node) {
6504
            return getAncestors(node).concat([node]);
6505
        }
6506
 
6507
        function nextNodeDescendants(node) {
6508
            while (node && !node.nextSibling) {
6509
                node = node.parentNode;
6510
            }
6511
            if (!node) {
6512
                return null;
6513
            }
6514
            return node.nextSibling;
6515
        }
6516
 
6517
        function nextNode(node, excludeChildren) {
6518
            if (!excludeChildren && node.hasChildNodes()) {
6519
                return node.firstChild;
6520
            }
6521
            return nextNodeDescendants(node);
6522
        }
6523
 
6524
        function previousNode(node) {
6525
            var previous = node.previousSibling;
6526
            if (previous) {
6527
                node = previous;
6528
                while (node.hasChildNodes()) {
6529
                    node = node.lastChild;
6530
                }
6531
                return node;
6532
            }
6533
            var parent = node.parentNode;
6534
            if (parent && parent.nodeType == 1) {
6535
                return parent;
6536
            }
6537
            return null;
6538
        }
6539
 
6540
        // Adpated from Aryeh's code.
6541
        // "A whitespace node is either a Text node whose data is the empty string; or
6542
        // a Text node whose data consists only of one or more tabs (0x0009), line
6543
        // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
6544
        // parent is an Element whose resolved value for "white-space" is "normal" or
6545
        // "nowrap"; or a Text node whose data consists only of one or more tabs
6546
        // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
6547
        // parent is an Element whose resolved value for "white-space" is "pre-line"."
6548
        function isWhitespaceNode(node) {
6549
            if (!node || node.nodeType != 3) {
6550
                return false;
6551
            }
6552
            var text = node.data;
6553
            if (text === "") {
6554
                return true;
6555
            }
6556
            var parent = node.parentNode;
6557
            if (!parent || parent.nodeType != 1) {
6558
                return false;
6559
            }
6560
            var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
6561
 
6562
            return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
6563
                (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
6564
        }
6565
 
6566
        // Adpated from Aryeh's code.
6567
        // "node is a collapsed whitespace node if the following algorithm returns
6568
        // true:"
6569
        function isCollapsedWhitespaceNode(node) {
6570
            // "If node's data is the empty string, return true."
6571
            if (node.data === "") {
6572
                return true;
6573
            }
6574
 
6575
            // "If node is not a whitespace node, return false."
6576
            if (!isWhitespaceNode(node)) {
6577
                return false;
6578
            }
6579
 
6580
            // "Let ancestor be node's parent."
6581
            var ancestor = node.parentNode;
6582
 
6583
            // "If ancestor is null, return true."
6584
            if (!ancestor) {
6585
                return true;
6586
            }
6587
 
6588
            // "If the "display" property of some ancestor of node has resolved value "none", return true."
6589
            if (isHidden(node)) {
6590
                return true;
6591
            }
6592
 
6593
            return false;
6594
        }
6595
 
6596
        function isCollapsedNode(node) {
6597
            var type = node.nodeType;
6598
            return type == 7 /* PROCESSING_INSTRUCTION */ ||
6599
                type == 8 /* COMMENT */ ||
6600
                isHidden(node) ||
6601
                /^(script|style)$/i.test(node.nodeName) ||
6602
                isVisibilityHiddenTextNode(node) ||
6603
                isCollapsedWhitespaceNode(node);
6604
        }
6605
 
6606
        function isIgnoredNode(node, win) {
6607
            var type = node.nodeType;
6608
            return type == 7 /* PROCESSING_INSTRUCTION */ ||
6609
                type == 8 /* COMMENT */ ||
6610
                (type == 1 && getComputedDisplay(node, win) == "none");
6611
        }
6612
 
6613
        /*----------------------------------------------------------------------------------------------------------------*/
6614
 
6615
        // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
6616
 
6617
        function Cache() {
6618
            this.store = {};
6619
        }
6620
 
6621
        Cache.prototype = {
6622
            get: function(key) {
6623
                return this.store.hasOwnProperty(key) ? this.store[key] : null;
6624
            },
6625
 
6626
            set: function(key, value) {
6627
                return this.store[key] = value;
6628
            }
6629
        };
6630
 
6631
        var cachedCount = 0, uncachedCount = 0;
6632
 
6633
        function createCachingGetter(methodName, func, objProperty) {
6634
            return function(args) {
6635
                var cache = this.cache;
6636
                if (cache.hasOwnProperty(methodName)) {
6637
                    cachedCount++;
6638
                    return cache[methodName];
6639
                } else {
6640
                    uncachedCount++;
6641
                    var value = func.call(this, objProperty ? this[objProperty] : this, args);
6642
                    cache[methodName] = value;
6643
                    return value;
6644
                }
6645
            };
6646
        }
6647
 
6648
        /*----------------------------------------------------------------------------------------------------------------*/
6649
 
6650
        function NodeWrapper(node, session) {
6651
            this.node = node;
6652
            this.session = session;
6653
            this.cache = new Cache();
6654
            this.positions = new Cache();
6655
        }
6656
 
6657
        var nodeProto = {
6658
            getPosition: function(offset) {
6659
                var positions = this.positions;
6660
                return positions.get(offset) || positions.set(offset, new Position(this, offset));
6661
            },
6662
 
6663
            toString: function() {
6664
                return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
6665
            }
6666
        };
6667
 
6668
        NodeWrapper.prototype = nodeProto;
6669
 
6670
        var EMPTY = "EMPTY",
6671
            NON_SPACE = "NON_SPACE",
6672
            UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
6673
            COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
6674
            TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
6675
            TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
6676
            TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
6677
            PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
6678
            TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
6679
            INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
6680
 
6681
        extend(nodeProto, {
6682
            isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
6683
            getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
6684
            getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
6685
            containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
6686
            isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
6687
            isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
6688
            getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
6689
            isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
6690
            isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
6691
            next: createCachingGetter("nextPos", nextNode, "node"),
6692
            previous: createCachingGetter("previous", previousNode, "node"),
6693
 
6694
            getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
6695
                var spaceRegex = null, collapseSpaces = false;
6696
                var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
6697
                var preLine = (cssWhitespace == "pre-line");
6698
                if (preLine) {
6699
                    spaceRegex = spacesMinusLineBreaksRegex;
6700
                    collapseSpaces = true;
6701
                } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
6702
                    spaceRegex = spacesRegex;
6703
                    collapseSpaces = true;
6704
                }
6705
 
6706
                return {
6707
                    node: textNode,
6708
                    text: textNode.data,
6709
                    spaceRegex: spaceRegex,
6710
                    collapseSpaces: collapseSpaces,
6711
                    preLine: preLine
6712
                };
6713
            }, "node"),
6714
 
6715
            hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
6716
                var session = this.session;
6717
                var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
6718
                var firstPosInEl = session.getPosition(el, 0);
6719
 
6720
                var pos = backward ? posAfterEl : firstPosInEl;
6721
                var endPos = backward ? firstPosInEl : posAfterEl;
6722
 
6723
                /*
6724
                 <body><p>X  </p><p>Y</p></body>
6725
 
6726
                 Positions:
6727
 
6728
                 body:0:""
6729
                 p:0:""
6730
                 text:0:""
6731
                 text:1:"X"
6732
                 text:2:TRAILING_SPACE_IN_BLOCK
6733
                 text:3:COLLAPSED_SPACE
6734
                 p:1:""
6735
                 body:1:"\n"
6736
                 p:0:""
6737
                 text:0:""
6738
                 text:1:"Y"
6739
 
6740
                 A character is a TRAILING_SPACE_IN_BLOCK iff:
6741
 
6742
                 - There is no uncollapsed character after it within the visible containing block element
6743
 
6744
                 A character is a TRAILING_SPACE_BEFORE_BR iff:
6745
 
6746
                 - There is no uncollapsed character after it preceding a <br> element
6747
 
6748
                 An element has inner text iff
6749
 
6750
                 - It is not hidden
6751
                 - It contains an uncollapsed character
6752
 
6753
                 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
6754
                 */
6755
 
6756
                while (pos !== endPos) {
6757
                    pos.prepopulateChar();
6758
                    if (pos.isDefinitelyNonEmpty()) {
6759
                        return true;
6760
                    }
6761
                    pos = backward ? pos.previousVisible() : pos.nextVisible();
6762
                }
6763
 
6764
                return false;
6765
            }, "node"),
6766
 
6767
            isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
6768
                // Ensure that a block element containing a <br> is considered to have inner text
6769
                var brs = el.getElementsByTagName("br");
6770
                for (var i = 0, len = brs.length; i < len; ++i) {
6771
                    if (!isCollapsedNode(brs[i])) {
6772
                        return true;
6773
                    }
6774
                }
6775
                return this.hasInnerText();
6776
            }, "node"),
6777
 
6778
            getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
6779
                if (el.tagName.toLowerCase() == "br") {
6780
                    return "";
6781
                } else {
6782
                    switch (this.getComputedDisplay()) {
6783
                        case "inline":
6784
                            var child = el.lastChild;
6785
                            while (child) {
6786
                                if (!isIgnoredNode(child)) {
6787
                                    return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
6788
                                }
6789
                                child = child.previousSibling;
6790
                            }
6791
                            break;
6792
                        case "inline-block":
6793
                        case "inline-table":
6794
                        case "none":
6795
                        case "table-column":
6796
                        case "table-column-group":
6797
                            break;
6798
                        case "table-cell":
6799
                            return "\t";
6800
                        default:
6801
                            return this.isRenderedBlock(true) ? "\n" : "";
6802
                    }
6803
                }
6804
                return "";
6805
            }, "node"),
6806
 
6807
            getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
6808
                switch (this.getComputedDisplay()) {
6809
                    case "inline":
6810
                    case "inline-block":
6811
                    case "inline-table":
6812
                    case "none":
6813
                    case "table-column":
6814
                    case "table-column-group":
6815
                    case "table-cell":
6816
                        break;
6817
                    default:
6818
                        return this.isRenderedBlock(false) ? "\n" : "";
6819
                }
6820
                return "";
6821
            }, "node")
6822
        });
6823
 
6824
        /*----------------------------------------------------------------------------------------------------------------*/
6825
 
6826
        function Position(nodeWrapper, offset) {
6827
            this.offset = offset;
6828
            this.nodeWrapper = nodeWrapper;
6829
            this.node = nodeWrapper.node;
6830
            this.session = nodeWrapper.session;
6831
            this.cache = new Cache();
6832
        }
6833
 
6834
        function inspectPosition() {
6835
            return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
6836
        }
6837
 
6838
        var positionProto = {
6839
            character: "",
6840
            characterType: EMPTY,
6841
            isBr: false,
6842
 
6843
            /*
6844
            This method:
6845
            - Fully populates positions that have characters that can be determined independently of any other characters.
6846
            - Populates most types of space positions with a provisional character. The character is finalized later.
6847
             */
6848
            prepopulateChar: function() {
6849
                var pos = this;
6850
                if (!pos.prepopulatedChar) {
6851
                    var node = pos.node, offset = pos.offset;
6852
                    var visibleChar = "", charType = EMPTY;
6853
                    var finalizedChar = false;
6854
                    if (offset > 0) {
6855
                        if (node.nodeType == 3) {
6856
                            var text = node.data;
6857
                            var textChar = text.charAt(offset - 1);
6858
 
6859
                            var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
6860
                            var spaceRegex = nodeInfo.spaceRegex;
6861
                            if (nodeInfo.collapseSpaces) {
6862
                                if (spaceRegex.test(textChar)) {
6863
                                    // "If the character at position is from set, append a single space (U+0020) to newdata and advance
6864
                                    // position until the character at position is not from set."
6865
 
6866
                                    // We also need to check for the case where we're in a pre-line and we have a space preceding a
6867
                                    // line break, because such spaces are collapsed in some browsers
6868
                                    if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
6869
                                    } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
6870
                                        visibleChar = " ";
6871
                                        charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
6872
                                    } else {
6873
                                        visibleChar = " ";
6874
                                        //pos.checkForFollowingLineBreak = true;
6875
                                        charType = COLLAPSIBLE_SPACE;
6876
                                    }
6877
                                } else {
6878
                                    visibleChar = textChar;
6879
                                    charType = NON_SPACE;
6880
                                    finalizedChar = true;
6881
                                }
6882
                            } else {
6883
                                visibleChar = textChar;
6884
                                charType = UNCOLLAPSIBLE_SPACE;
6885
                                finalizedChar = true;
6886
                            }
6887
                        } else {
6888
                            var nodePassed = node.childNodes[offset - 1];
6889
                            if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
6890
                                if (nodePassed.tagName.toLowerCase() == "br") {
6891
                                    visibleChar = "\n";
6892
                                    pos.isBr = true;
6893
                                    charType = COLLAPSIBLE_SPACE;
6894
                                    finalizedChar = false;
6895
                                } else {
6896
                                    pos.checkForTrailingSpace = true;
6897
                                }
6898
                            }
6899
 
6900
                            // Check the leading space of the next node for the case when a block element follows an inline
6901
                            // element or text node. In that case, there is an implied line break between the two nodes.
6902
                            if (!visibleChar) {
6903
                                var nextNode = node.childNodes[offset];
6904
                                if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
6905
                                    pos.checkForLeadingSpace = true;
6906
                                }
6907
                            }
6908
                        }
6909
                    }
6910
 
6911
                    pos.prepopulatedChar = true;
6912
                    pos.character = visibleChar;
6913
                    pos.characterType = charType;
6914
                    pos.isCharInvariant = finalizedChar;
6915
                }
6916
            },
6917
 
6918
            isDefinitelyNonEmpty: function() {
6919
                var charType = this.characterType;
6920
                return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
6921
            },
6922
 
6923
            // Resolve leading and trailing spaces, which may involve prepopulating other positions
6924
            resolveLeadingAndTrailingSpaces: function() {
6925
                if (!this.prepopulatedChar) {
6926
                    this.prepopulateChar();
6927
                }
6928
                if (this.checkForTrailingSpace) {
6929
                    var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
6930
                    if (trailingSpace) {
6931
                        this.isTrailingSpace = true;
6932
                        this.character = trailingSpace;
6933
                        this.characterType = COLLAPSIBLE_SPACE;
6934
                    }
6935
                    this.checkForTrailingSpace = false;
6936
                }
6937
                if (this.checkForLeadingSpace) {
6938
                    var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
6939
                    if (leadingSpace) {
6940
                        this.isLeadingSpace = true;
6941
                        this.character = leadingSpace;
6942
                        this.characterType = COLLAPSIBLE_SPACE;
6943
                    }
6944
                    this.checkForLeadingSpace = false;
6945
                }
6946
            },
6947
 
6948
            getPrecedingUncollapsedPosition: function(characterOptions) {
6949
                var pos = this, character;
6950
                while ( (pos = pos.previousVisible()) ) {
6951
                    character = pos.getCharacter(characterOptions);
6952
                    if (character !== "") {
6953
                        return pos;
6954
                    }
6955
                }
6956
 
6957
                return null;
6958
            },
6959
 
6960
            getCharacter: function(characterOptions) {
6961
                this.resolveLeadingAndTrailingSpaces();
6962
 
6963
                var thisChar = this.character, returnChar;
6964
 
6965
                // Check if character is ignored
6966
                var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
6967
                var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
6968
 
6969
                // Check if this position's  character is invariant (i.e. not dependent on character options) and return it
6970
                // if so
6971
                if (this.isCharInvariant) {
6972
                    returnChar = isIgnoredCharacter ? "" : thisChar;
6973
                    return returnChar;
6974
                }
6975
 
6976
                var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
6977
                var cachedChar = this.cache.get(cacheKey);
6978
                if (cachedChar !== null) {
6979
                    return cachedChar;
6980
                }
6981
 
6982
                // We need to actually get the character now
6983
                var character = "";
6984
                var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
6985
 
6986
                var nextPos, previousPos;
6987
                var gotPreviousPos = false;
6988
                var pos = this;
6989
 
6990
                function getPreviousPos() {
6991
                    if (!gotPreviousPos) {
6992
                        previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
6993
                        gotPreviousPos = true;
6994
                    }
6995
                    return previousPos;
6996
                }
6997
 
6998
                // Disallow a collapsible space that is followed by a line break or is the last character
6999
                if (collapsible) {
7000
                    // Allow a trailing space that we've previously determined should be included
7001
                    if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
7002
                        character = "\n";
7003
                    }
7004
                    // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
7005
                    // or follows a collapsible included space
7006
                    else if (thisChar == " " &&
7007
                            (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
7008
                    }
7009
                    // Allow a leading line break unless it follows a line break
7010
                    else if (thisChar == "\n" && this.isLeadingSpace) {
7011
                        if (getPreviousPos() && previousPos.character != "\n") {
7012
                            character = "\n";
7013
                        } else {
7014
                        }
7015
                    } else {
7016
                        nextPos = this.nextUncollapsed();
7017
                        if (nextPos) {
7018
                            if (nextPos.isBr) {
7019
                                this.type = TRAILING_SPACE_BEFORE_BR;
7020
                            } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
7021
                                this.type = TRAILING_SPACE_IN_BLOCK;
7022
                            } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
7023
                                this.type = TRAILING_SPACE_BEFORE_BLOCK;
7024
                            }
7025
 
7026
                            if (nextPos.character == "\n") {
7027
                                if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
7028
                                } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
7029
                                } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
7030
                                } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
7031
                                } else if (thisChar == "\n") {
7032
                                    if (nextPos.isTrailingSpace) {
7033
                                        if (this.isTrailingSpace) {
7034
                                        } else if (this.isBr) {
7035
                                            nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
7036
 
7037
                                            if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
7038
                                                nextPos.character = "";
7039
                                            } else {
7040
                                                nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
7041
                                            }
7042
                                        }
7043
                                    } else {
7044
                                        character = "\n";
7045
                                    }
7046
                                } else if (thisChar == " ") {
7047
                                    character = " ";
7048
                                } else {
7049
                                }
7050
                            } else {
7051
                                character = thisChar;
7052
                            }
7053
                        } else {
7054
                        }
7055
                    }
7056
                }
7057
 
7058
                if (ignoredChars.indexOf(character) > -1) {
7059
                    character = "";
7060
                }
7061
 
7062
 
7063
                this.cache.set(cacheKey, character);
7064
 
7065
                return character;
7066
            },
7067
 
7068
            equals: function(pos) {
7069
                return !!pos && this.node === pos.node && this.offset === pos.offset;
7070
            },
7071
 
7072
            inspect: inspectPosition,
7073
 
7074
            toString: function() {
7075
                return this.character;
7076
            }
7077
        };
7078
 
7079
        Position.prototype = positionProto;
7080
 
7081
        extend(positionProto, {
7082
            next: createCachingGetter("nextPos", function(pos) {
7083
                var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
7084
                if (!node) {
7085
                    return null;
7086
                }
7087
                var nextNode, nextOffset, child;
7088
                if (offset == nodeWrapper.getLength()) {
7089
                    // Move onto the next node
7090
                    nextNode = node.parentNode;
7091
                    nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
7092
                } else {
7093
                    if (nodeWrapper.isCharacterDataNode()) {
7094
                        nextNode = node;
7095
                        nextOffset = offset + 1;
7096
                    } else {
7097
                        child = node.childNodes[offset];
7098
                        // Go into the children next, if children there are
7099
                        if (session.getNodeWrapper(child).containsPositions()) {
7100
                            nextNode = child;
7101
                            nextOffset = 0;
7102
                        } else {
7103
                            nextNode = node;
7104
                            nextOffset = offset + 1;
7105
                        }
7106
                    }
7107
                }
7108
 
7109
                return nextNode ? session.getPosition(nextNode, nextOffset) : null;
7110
            }),
7111
 
7112
            previous: createCachingGetter("previous", function(pos) {
7113
                var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
7114
                var previousNode, previousOffset, child;
7115
                if (offset == 0) {
7116
                    previousNode = node.parentNode;
7117
                    previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
7118
                } else {
7119
                    if (nodeWrapper.isCharacterDataNode()) {
7120
                        previousNode = node;
7121
                        previousOffset = offset - 1;
7122
                    } else {
7123
                        child = node.childNodes[offset - 1];
7124
                        // Go into the children next, if children there are
7125
                        if (session.getNodeWrapper(child).containsPositions()) {
7126
                            previousNode = child;
7127
                            previousOffset = dom.getNodeLength(child);
7128
                        } else {
7129
                            previousNode = node;
7130
                            previousOffset = offset - 1;
7131
                        }
7132
                    }
7133
                }
7134
                return previousNode ? session.getPosition(previousNode, previousOffset) : null;
7135
            }),
7136
 
7137
            /*
7138
             Next and previous position moving functions that filter out
7139
 
7140
             - Hidden (CSS visibility/display) elements
7141
             - Script and style elements
7142
             */
7143
            nextVisible: createCachingGetter("nextVisible", function(pos) {
7144
                var next = pos.next();
7145
                if (!next) {
7146
                    return null;
7147
                }
7148
                var nodeWrapper = next.nodeWrapper, node = next.node;
7149
                var newPos = next;
7150
                if (nodeWrapper.isCollapsed()) {
7151
                    // We're skipping this node and all its descendants
7152
                    newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
7153
                }
7154
                return newPos;
7155
            }),
7156
 
7157
            nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
7158
                var nextPos = pos;
7159
                while ( (nextPos = nextPos.nextVisible()) ) {
7160
                    nextPos.resolveLeadingAndTrailingSpaces();
7161
                    if (nextPos.character !== "") {
7162
                        return nextPos;
7163
                    }
7164
                }
7165
                return null;
7166
            }),
7167
 
7168
            previousVisible: createCachingGetter("previousVisible", function(pos) {
7169
                var previous = pos.previous();
7170
                if (!previous) {
7171
                    return null;
7172
                }
7173
                var nodeWrapper = previous.nodeWrapper, node = previous.node;
7174
                var newPos = previous;
7175
                if (nodeWrapper.isCollapsed()) {
7176
                    // We're skipping this node and all its descendants
7177
                    newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
7178
                }
7179
                return newPos;
7180
            })
7181
        });
7182
 
7183
        /*----------------------------------------------------------------------------------------------------------------*/
7184
 
7185
        var currentSession = null;
7186
 
7187
        var Session = (function() {
7188
            function createWrapperCache(nodeProperty) {
7189
                var cache = new Cache();
7190
 
7191
                return {
7192
                    get: function(node) {
7193
                        var wrappersByProperty = cache.get(node[nodeProperty]);
7194
                        if (wrappersByProperty) {
7195
                            for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
7196
                                if (wrapper.node === node) {
7197
                                    return wrapper;
7198
                                }
7199
                            }
7200
                        }
7201
                        return null;
7202
                    },
7203
 
7204
                    set: function(nodeWrapper) {
7205
                        var property = nodeWrapper.node[nodeProperty];
7206
                        var wrappersByProperty = cache.get(property) || cache.set(property, []);
7207
                        wrappersByProperty.push(nodeWrapper);
7208
                    }
7209
                };
7210
            }
7211
 
7212
            var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
7213
 
7214
            function Session() {
7215
                this.initCaches();
7216
            }
7217
 
7218
            Session.prototype = {
7219
                initCaches: function() {
7220
                    this.elementCache = uniqueIDSupported ? (function() {
7221
                        var elementsCache = new Cache();
7222
 
7223
                        return {
7224
                            get: function(el) {
7225
                                return elementsCache.get(el.uniqueID);
7226
                            },
7227
 
7228
                            set: function(elWrapper) {
7229
                                elementsCache.set(elWrapper.node.uniqueID, elWrapper);
7230
                            }
7231
                        };
7232
                    })() : createWrapperCache("tagName");
7233
 
7234
                    // Store text nodes keyed by data, although we may need to truncate this
7235
                    this.textNodeCache = createWrapperCache("data");
7236
                    this.otherNodeCache = createWrapperCache("nodeName");
7237
                },
7238
 
7239
                getNodeWrapper: function(node) {
7240
                    var wrapperCache;
7241
                    switch (node.nodeType) {
7242
                        case 1:
7243
                            wrapperCache = this.elementCache;
7244
                            break;
7245
                        case 3:
7246
                            wrapperCache = this.textNodeCache;
7247
                            break;
7248
                        default:
7249
                            wrapperCache = this.otherNodeCache;
7250
                            break;
7251
                    }
7252
 
7253
                    var wrapper = wrapperCache.get(node);
7254
                    if (!wrapper) {
7255
                        wrapper = new NodeWrapper(node, this);
7256
                        wrapperCache.set(wrapper);
7257
                    }
7258
                    return wrapper;
7259
                },
7260
 
7261
                getPosition: function(node, offset) {
7262
                    return this.getNodeWrapper(node).getPosition(offset);
7263
                },
7264
 
7265
                getRangeBoundaryPosition: function(range, isStart) {
7266
                    var prefix = isStart ? "start" : "end";
7267
                    return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
7268
                },
7269
 
7270
                detach: function() {
7271
                    this.elementCache = this.textNodeCache = this.otherNodeCache = null;
7272
                }
7273
            };
7274
 
7275
            return Session;
7276
        })();
7277
 
7278
        /*----------------------------------------------------------------------------------------------------------------*/
7279
 
7280
        function startSession() {
7281
            endSession();
7282
            return (currentSession = new Session());
7283
        }
7284
 
7285
        function getSession() {
7286
            return currentSession || startSession();
7287
        }
7288
 
7289
        function endSession() {
7290
            if (currentSession) {
7291
                currentSession.detach();
7292
            }
7293
            currentSession = null;
7294
        }
7295
 
7296
        /*----------------------------------------------------------------------------------------------------------------*/
7297
 
7298
        // Extensions to the rangy.dom utility object
7299
 
7300
        extend(dom, {
7301
            nextNode: nextNode,
7302
            previousNode: previousNode
7303
        });
7304
 
7305
        /*----------------------------------------------------------------------------------------------------------------*/
7306
 
7307
        function createCharacterIterator(startPos, backward, endPos, characterOptions) {
7308
 
7309
            // Adjust the end position to ensure that it is actually reached
7310
            if (endPos) {
7311
                if (backward) {
7312
                    if (isCollapsedNode(endPos.node)) {
7313
                        endPos = startPos.previousVisible();
7314
                    }
7315
                } else {
7316
                    if (isCollapsedNode(endPos.node)) {
7317
                        endPos = endPos.nextVisible();
7318
                    }
7319
                }
7320
            }
7321
 
7322
            var pos = startPos, finished = false;
7323
 
7324
            function next() {
7325
                var charPos = null;
7326
                if (backward) {
7327
                    charPos = pos;
7328
                    if (!finished) {
7329
                        pos = pos.previousVisible();
7330
                        finished = !pos || (endPos && pos.equals(endPos));
7331
                    }
7332
                } else {
7333
                    if (!finished) {
7334
                        charPos = pos = pos.nextVisible();
7335
                        finished = !pos || (endPos && pos.equals(endPos));
7336
                    }
7337
                }
7338
                if (finished) {
7339
                    pos = null;
7340
                }
7341
                return charPos;
7342
            }
7343
 
7344
            var previousTextPos, returnPreviousTextPos = false;
7345
 
7346
            return {
7347
                next: function() {
7348
                    if (returnPreviousTextPos) {
7349
                        returnPreviousTextPos = false;
7350
                        return previousTextPos;
7351
                    } else {
7352
                        var pos, character;
7353
                        while ( (pos = next()) ) {
7354
                            character = pos.getCharacter(characterOptions);
7355
                            if (character) {
7356
                                previousTextPos = pos;
7357
                                return pos;
7358
                            }
7359
                        }
7360
                        return null;
7361
                    }
7362
                },
7363
 
7364
                rewind: function() {
7365
                    if (previousTextPos) {
7366
                        returnPreviousTextPos = true;
7367
                    } else {
7368
                        throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
7369
                    }
7370
                },
7371
 
7372
                dispose: function() {
7373
                    startPos = endPos = null;
7374
                }
7375
            };
7376
        }
7377
 
7378
        var arrayIndexOf = Array.prototype.indexOf ?
7379
            function(arr, val) {
7380
                return arr.indexOf(val);
7381
            } :
7382
            function(arr, val) {
7383
                for (var i = 0, len = arr.length; i < len; ++i) {
7384
                    if (arr[i] === val) {
7385
                        return i;
7386
                    }
7387
                }
7388
                return -1;
7389
            };
7390
 
7391
        // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
7392
        // is called and there is no more tokenized text
7393
        function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
7394
            var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
7395
            var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
7396
            var tokenizer = wordOptions.tokenizer;
7397
 
7398
            // Consumes a word and the whitespace beyond it
7399
            function consumeWord(forward) {
7400
                var pos, textChar;
7401
                var newChars = [], it = forward ? forwardIterator : backwardIterator;
7402
 
7403
                var passedWordBoundary = false, insideWord = false;
7404
 
7405
                while ( (pos = it.next()) ) {
7406
                    textChar = pos.character;
7407
 
7408
 
7409
                    if (allWhiteSpaceRegex.test(textChar)) {
7410
                        if (insideWord) {
7411
                            insideWord = false;
7412
                            passedWordBoundary = true;
7413
                        }
7414
                    } else {
7415
                        if (passedWordBoundary) {
7416
                            it.rewind();
7417
                            break;
7418
                        } else {
7419
                            insideWord = true;
7420
                        }
7421
                    }
7422
                    newChars.push(pos);
7423
                }
7424
 
7425
 
7426
                return newChars;
7427
            }
7428
 
7429
            // Get initial word surrounding initial position and tokenize it
7430
            var forwardChars = consumeWord(true);
7431
            var backwardChars = consumeWord(false).reverse();
7432
            var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
7433
 
7434
            // Create initial token buffers
7435
            var forwardTokensBuffer = forwardChars.length ?
7436
                tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
7437
 
7438
            var backwardTokensBuffer = backwardChars.length ?
7439
                tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
7440
 
7441
            function inspectBuffer(buffer) {
7442
                var textPositions = ["[" + buffer.length + "]"];
7443
                for (var i = 0; i < buffer.length; ++i) {
7444
                    textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
7445
                }
7446
                return textPositions;
7447
            }
7448
 
7449
 
7450
            return {
7451
                nextEndToken: function() {
7452
                    var lastToken, forwardChars;
7453
 
7454
                    // If we're down to the last token, consume character chunks until we have a word or run out of
7455
                    // characters to consume
7456
                    while ( forwardTokensBuffer.length == 1 &&
7457
                        !(lastToken = forwardTokensBuffer[0]).isWord &&
7458
                        (forwardChars = consumeWord(true)).length > 0) {
7459
 
7460
                        // Merge trailing non-word into next word and tokenize
7461
                        forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
7462
                    }
7463
 
7464
                    return forwardTokensBuffer.shift();
7465
                },
7466
 
7467
                previousStartToken: function() {
7468
                    var lastToken, backwardChars;
7469
 
7470
                    // If we're down to the last token, consume character chunks until we have a word or run out of
7471
                    // characters to consume
7472
                    while ( backwardTokensBuffer.length == 1 &&
7473
                        !(lastToken = backwardTokensBuffer[0]).isWord &&
7474
                        (backwardChars = consumeWord(false)).length > 0) {
7475
 
7476
                        // Merge leading non-word into next word and tokenize
7477
                        backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
7478
                    }
7479
 
7480
                    return backwardTokensBuffer.pop();
7481
                },
7482
 
7483
                dispose: function() {
7484
                    forwardIterator.dispose();
7485
                    backwardIterator.dispose();
7486
                    forwardTokensBuffer = backwardTokensBuffer = null;
7487
                }
7488
            };
7489
        }
7490
 
7491
        function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
7492
            var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
7493
            if (count !== 0) {
7494
                var backward = (count < 0);
7495
 
7496
                switch (unit) {
7497
                    case CHARACTER:
7498
                        charIterator = createCharacterIterator(pos, backward, null, characterOptions);
7499
                        while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
7500
                            ++unitsMoved;
7501
                            newPos = currentPos;
7502
                        }
7503
                        nextPos = currentPos;
7504
                        charIterator.dispose();
7505
                        break;
7506
                    case WORD:
7507
                        var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
7508
                        var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
7509
 
7510
                        while ( (token = next()) && unitsMoved < absCount ) {
7511
                            if (token.isWord) {
7512
                                ++unitsMoved;
7513
                                newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
7514
                            }
7515
                        }
7516
                        break;
7517
                    default:
7518
                        throw new Error("movePositionBy: unit '" + unit + "' not implemented");
7519
                }
7520
 
7521
                // Perform any necessary position tweaks
7522
                if (backward) {
7523
                    newPos = newPos.previousVisible();
7524
                    unitsMoved = -unitsMoved;
7525
                } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
7526
                    // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
7527
                    // before a block element (for example, the line break between "1" and "2" in the following HTML:
7528
                    // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
7529
                    // corresponds with a different selection position in most browsers from the one we want (i.e. at the
7530
                    // start of the contents of the block element). We get round this by advancing the position returned to
7531
                    // the last possible equivalent visible position.
7532
                    if (unit == WORD) {
7533
                        charIterator = createCharacterIterator(pos, false, null, characterOptions);
7534
                        nextPos = charIterator.next();
7535
                        charIterator.dispose();
7536
                    }
7537
                    if (nextPos) {
7538
                        newPos = nextPos.previousVisible();
7539
                    }
7540
                }
7541
            }
7542
 
7543
 
7544
            return {
7545
                position: newPos,
7546
                unitsMoved: unitsMoved
7547
            };
7548
        }
7549
 
7550
        function createRangeCharacterIterator(session, range, characterOptions, backward) {
7551
            var rangeStart = session.getRangeBoundaryPosition(range, true);
7552
            var rangeEnd = session.getRangeBoundaryPosition(range, false);
7553
            var itStart = backward ? rangeEnd : rangeStart;
7554
            var itEnd = backward ? rangeStart : rangeEnd;
7555
 
7556
            return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
7557
        }
7558
 
7559
        function getRangeCharacters(session, range, characterOptions) {
7560
 
7561
            var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
7562
            while ( (pos = it.next()) ) {
7563
                chars.push(pos);
7564
            }
7565
 
7566
            it.dispose();
7567
            return chars;
7568
        }
7569
 
7570
        function isWholeWord(startPos, endPos, wordOptions) {
7571
            var range = api.createRange(startPos.node);
7572
            range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
7573
            return !range.expand("word", { wordOptions: wordOptions });
7574
        }
7575
 
7576
        function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
7577
            var backward = isDirectionBackward(findOptions.direction);
7578
            var it = createCharacterIterator(
7579
                initialPos,
7580
                backward,
7581
                initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
7582
                findOptions.characterOptions
7583
            );
7584
            var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
7585
            var result, insideRegexMatch;
7586
            var returnValue = null;
7587
 
7588
            function handleMatch(startIndex, endIndex) {
7589
                var startPos = chars[startIndex].previousVisible();
7590
                var endPos = chars[endIndex - 1];
7591
                var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
7592
 
7593
                return {
7594
                    startPos: startPos,
7595
                    endPos: endPos,
7596
                    valid: valid
7597
                };
7598
            }
7599
 
7600
            while ( (pos = it.next()) ) {
7601
                currentChar = pos.character;
7602
                if (!isRegex && !findOptions.caseSensitive) {
7603
                    currentChar = currentChar.toLowerCase();
7604
                }
7605
 
7606
                if (backward) {
7607
                    chars.unshift(pos);
7608
                    text = currentChar + text;
7609
                } else {
7610
                    chars.push(pos);
7611
                    text += currentChar;
7612
                }
7613
 
7614
                if (isRegex) {
7615
                    result = searchTerm.exec(text);
7616
                    if (result) {
7617
                        matchStartIndex = result.index;
7618
                        matchEndIndex = matchStartIndex + result[0].length;
7619
                        if (insideRegexMatch) {
7620
                            // Check whether the match is now over
7621
                            if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
7622
                                returnValue = handleMatch(matchStartIndex, matchEndIndex);
7623
                                break;
7624
                            }
7625
                        } else {
7626
                            insideRegexMatch = true;
7627
                        }
7628
                    }
7629
                } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
7630
                    returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
7631
                    break;
7632
                }
7633
            }
7634
 
7635
            // Check whether regex match extends to the end of the range
7636
            if (insideRegexMatch) {
7637
                returnValue = handleMatch(matchStartIndex, matchEndIndex);
7638
            }
7639
            it.dispose();
7640
 
7641
            return returnValue;
7642
        }
7643
 
7644
        function createEntryPointFunction(func) {
7645
            return function() {
7646
                var sessionRunning = !!currentSession;
7647
                var session = getSession();
7648
                var args = [session].concat( util.toArray(arguments) );
7649
                var returnValue = func.apply(this, args);
7650
                if (!sessionRunning) {
7651
                    endSession();
7652
                }
7653
                return returnValue;
7654
            };
7655
        }
7656
 
7657
        /*----------------------------------------------------------------------------------------------------------------*/
7658
 
7659
        // Extensions to the Rangy Range object
7660
 
7661
        function createRangeBoundaryMover(isStart, collapse) {
7662
            /*
7663
             Unit can be "character" or "word"
7664
             Options:
7665
 
7666
             - includeTrailingSpace
7667
             - wordRegex
7668
             - tokenizer
7669
             - collapseSpaceBeforeLineBreak
7670
             */
7671
            return createEntryPointFunction(
7672
                function(session, unit, count, moveOptions) {
7673
                    if (typeof count == UNDEF) {
7674
                        count = unit;
7675
                        unit = CHARACTER;
7676
                    }
7677
                    moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
7678
 
7679
                    var boundaryIsStart = isStart;
7680
                    if (collapse) {
7681
                        boundaryIsStart = (count >= 0);
7682
                        this.collapse(!boundaryIsStart);
7683
                    }
7684
                    var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
7685
                    var newPos = moveResult.position;
7686
                    this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
7687
                    return moveResult.unitsMoved;
7688
                }
7689
            );
7690
        }
7691
 
7692
        function createRangeTrimmer(isStart) {
7693
            return createEntryPointFunction(
7694
                function(session, characterOptions) {
7695
                    characterOptions = createOptions(characterOptions, defaultCharacterOptions);
7696
                    var pos;
7697
                    var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
7698
                    var trimCharCount = 0;
7699
                    while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
7700
                        ++trimCharCount;
7701
                    }
7702
                    it.dispose();
7703
                    var trimmed = (trimCharCount > 0);
7704
                    if (trimmed) {
7705
                        this[isStart ? "moveStart" : "moveEnd"](
7706
                            "character",
7707
                            isStart ? trimCharCount : -trimCharCount,
7708
                            { characterOptions: characterOptions }
7709
                        );
7710
                    }
7711
                    return trimmed;
7712
                }
7713
            );
7714
        }
7715
 
7716
        extend(api.rangePrototype, {
7717
            moveStart: createRangeBoundaryMover(true, false),
7718
 
7719
            moveEnd: createRangeBoundaryMover(false, false),
7720
 
7721
            move: createRangeBoundaryMover(true, true),
7722
 
7723
            trimStart: createRangeTrimmer(true),
7724
 
7725
            trimEnd: createRangeTrimmer(false),
7726
 
7727
            trim: createEntryPointFunction(
7728
                function(session, characterOptions) {
7729
                    var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
7730
                    return startTrimmed || endTrimmed;
7731
                }
7732
            ),
7733
 
7734
            expand: createEntryPointFunction(
7735
                function(session, unit, expandOptions) {
7736
                    var moved = false;
7737
                    expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
7738
                    var characterOptions = expandOptions.characterOptions;
7739
                    if (!unit) {
7740
                        unit = CHARACTER;
7741
                    }
7742
                    if (unit == WORD) {
7743
                        var wordOptions = expandOptions.wordOptions;
7744
                        var startPos = session.getRangeBoundaryPosition(this, true);
7745
                        var endPos = session.getRangeBoundaryPosition(this, false);
7746
 
7747
                        var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
7748
                        var startToken = startTokenizedTextProvider.nextEndToken();
7749
                        var newStartPos = startToken.chars[0].previousVisible();
7750
                        var endToken, newEndPos;
7751
 
7752
                        if (this.collapsed) {
7753
                            endToken = startToken;
7754
                        } else {
7755
                            var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
7756
                            endToken = endTokenizedTextProvider.previousStartToken();
7757
                        }
7758
                        newEndPos = endToken.chars[endToken.chars.length - 1];
7759
 
7760
                        if (!newStartPos.equals(startPos)) {
7761
                            this.setStart(newStartPos.node, newStartPos.offset);
7762
                            moved = true;
7763
                        }
7764
                        if (newEndPos && !newEndPos.equals(endPos)) {
7765
                            this.setEnd(newEndPos.node, newEndPos.offset);
7766
                            moved = true;
7767
                        }
7768
 
7769
                        if (expandOptions.trim) {
7770
                            if (expandOptions.trimStart) {
7771
                                moved = this.trimStart(characterOptions) || moved;
7772
                            }
7773
                            if (expandOptions.trimEnd) {
7774
                                moved = this.trimEnd(characterOptions) || moved;
7775
                            }
7776
                        }
7777
 
7778
                        return moved;
7779
                    } else {
7780
                        return this.moveEnd(CHARACTER, 1, expandOptions);
7781
                    }
7782
                }
7783
            ),
7784
 
7785
            text: createEntryPointFunction(
7786
                function(session, characterOptions) {
7787
                    return this.collapsed ?
7788
                        "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
7789
                }
7790
            ),
7791
 
7792
            selectCharacters: createEntryPointFunction(
7793
                function(session, containerNode, startIndex, endIndex, characterOptions) {
7794
                    var moveOptions = { characterOptions: characterOptions };
7795
                    if (!containerNode) {
7796
                        containerNode = getBody( this.getDocument() );
7797
                    }
7798
                    this.selectNodeContents(containerNode);
7799
                    this.collapse(true);
7800
                    this.moveStart("character", startIndex, moveOptions);
7801
                    this.collapse(true);
7802
                    this.moveEnd("character", endIndex - startIndex, moveOptions);
7803
                }
7804
            ),
7805
 
7806
            // Character indexes are relative to the start of node
7807
            toCharacterRange: createEntryPointFunction(
7808
                function(session, containerNode, characterOptions) {
7809
                    if (!containerNode) {
7810
                        containerNode = getBody( this.getDocument() );
7811
                    }
7812
                    var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
7813
                    var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
7814
                    var rangeBetween = this.cloneRange();
7815
                    var startIndex, endIndex;
7816
                    if (rangeStartsBeforeNode) {
7817
                        rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
7818
                        startIndex = -rangeBetween.text(characterOptions).length;
7819
                    } else {
7820
                        rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
7821
                        startIndex = rangeBetween.text(characterOptions).length;
7822
                    }
7823
                    endIndex = startIndex + this.text(characterOptions).length;
7824
 
7825
                    return {
7826
                        start: startIndex,
7827
                        end: endIndex
7828
                    };
7829
                }
7830
            ),
7831
 
7832
            findText: createEntryPointFunction(
7833
                function(session, searchTermParam, findOptions) {
7834
                    // Set up options
7835
                    findOptions = createNestedOptions(findOptions, defaultFindOptions);
7836
 
7837
                    // Create word options if we're matching whole words only
7838
                    if (findOptions.wholeWordsOnly) {
7839
                        // We don't ever want trailing spaces for search results
7840
                        findOptions.wordOptions.includeTrailingSpace = false;
7841
                    }
7842
 
7843
                    var backward = isDirectionBackward(findOptions.direction);
7844
 
7845
                    // Create a range representing the search scope if none was provided
7846
                    var searchScopeRange = findOptions.withinRange;
7847
                    if (!searchScopeRange) {
7848
                        searchScopeRange = api.createRange();
7849
                        searchScopeRange.selectNodeContents(this.getDocument());
7850
                    }
7851
 
7852
                    // Examine and prepare the search term
7853
                    var searchTerm = searchTermParam, isRegex = false;
7854
                    if (typeof searchTerm == "string") {
7855
                        if (!findOptions.caseSensitive) {
7856
                            searchTerm = searchTerm.toLowerCase();
7857
                        }
7858
                    } else {
7859
                        isRegex = true;
7860
                    }
7861
 
7862
                    var initialPos = session.getRangeBoundaryPosition(this, !backward);
7863
 
7864
                    // Adjust initial position if it lies outside the search scope
7865
                    var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
7866
 
7867
                    if (comparison === -1) {
7868
                        initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
7869
                    } else if (comparison === 1) {
7870
                        initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
7871
                    }
7872
 
7873
                    var pos = initialPos;
7874
                    var wrappedAround = false;
7875
 
7876
                    // Try to find a match and ignore invalid ones
7877
                    var findResult;
7878
                    while (true) {
7879
                        findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
7880
 
7881
                        if (findResult) {
7882
                            if (findResult.valid) {
7883
                                this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
7884
                                return true;
7885
                            } else {
7886
                                // We've found a match that is not a whole word, so we carry on searching from the point immediately
7887
                                // after the match
7888
                                pos = backward ? findResult.startPos : findResult.endPos;
7889
                            }
7890
                        } else if (findOptions.wrap && !wrappedAround) {
7891
                            // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
7892
                            searchScopeRange = searchScopeRange.cloneRange();
7893
                            pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
7894
                            searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
7895
                            wrappedAround = true;
7896
                        } else {
7897
                            // Nothing found and we can't wrap around, so we're done
7898
                            return false;
7899
                        }
7900
                    }
7901
                }
7902
            ),
7903
 
7904
            pasteHtml: function(html) {
7905
                this.deleteContents();
7906
                if (html) {
7907
                    var frag = this.createContextualFragment(html);
7908
                    var lastChild = frag.lastChild;
7909
                    this.insertNode(frag);
7910
                    this.collapseAfter(lastChild);
7911
                }
7912
            }
7913
        });
7914
 
7915
        /*----------------------------------------------------------------------------------------------------------------*/
7916
 
7917
        // Extensions to the Rangy Selection object
7918
 
7919
        function createSelectionTrimmer(methodName) {
7920
            return createEntryPointFunction(
7921
                function(session, characterOptions) {
7922
                    var trimmed = false;
7923
                    this.changeEachRange(function(range) {
7924
                        trimmed = range[methodName](characterOptions) || trimmed;
7925
                    });
7926
                    return trimmed;
7927
                }
7928
            );
7929
        }
7930
 
7931
        extend(api.selectionPrototype, {
7932
            expand: createEntryPointFunction(
7933
                function(session, unit, expandOptions) {
7934
                    this.changeEachRange(function(range) {
7935
                        range.expand(unit, expandOptions);
7936
                    });
7937
                }
7938
            ),
7939
 
7940
            move: createEntryPointFunction(
7941
                function(session, unit, count, options) {
7942
                    var unitsMoved = 0;
7943
                    if (this.focusNode) {
7944
                        this.collapse(this.focusNode, this.focusOffset);
7945
                        var range = this.getRangeAt(0);
7946
                        if (!options) {
7947
                            options = {};
7948
                        }
7949
                        options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
7950
                        unitsMoved = range.move(unit, count, options);
7951
                        this.setSingleRange(range);
7952
                    }
7953
                    return unitsMoved;
7954
                }
7955
            ),
7956
 
7957
            trimStart: createSelectionTrimmer("trimStart"),
7958
            trimEnd: createSelectionTrimmer("trimEnd"),
7959
            trim: createSelectionTrimmer("trim"),
7960
 
7961
            selectCharacters: createEntryPointFunction(
7962
                function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
7963
                    var range = api.createRange(containerNode);
7964
                    range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
7965
                    this.setSingleRange(range, direction);
7966
                }
7967
            ),
7968
 
7969
            saveCharacterRanges: createEntryPointFunction(
7970
                function(session, containerNode, characterOptions) {
7971
                    var ranges = this.getAllRanges(), rangeCount = ranges.length;
7972
                    var rangeInfos = [];
7973
 
7974
                    var backward = rangeCount == 1 && this.isBackward();
7975
 
7976
                    for (var i = 0, len = ranges.length; i < len; ++i) {
7977
                        rangeInfos[i] = {
7978
                            characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
7979
                            backward: backward,
7980
                            characterOptions: characterOptions
7981
                        };
7982
                    }
7983
 
7984
                    return rangeInfos;
7985
                }
7986
            ),
7987
 
7988
            restoreCharacterRanges: createEntryPointFunction(
7989
                function(session, containerNode, saved) {
7990
                    this.removeAllRanges();
7991
                    for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
7992
                        rangeInfo = saved[i];
7993
                        characterRange = rangeInfo.characterRange;
7994
                        range = api.createRange(containerNode);
7995
                        range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
7996
                        this.addRange(range, rangeInfo.backward);
7997
                    }
7998
                }
7999
            ),
8000
 
8001
            text: createEntryPointFunction(
8002
                function(session, characterOptions) {
8003
                    var rangeTexts = [];
8004
                    for (var i = 0, len = this.rangeCount; i < len; ++i) {
8005
                        rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
8006
                    }
8007
                    return rangeTexts.join("");
8008
                }
8009
            )
8010
        });
8011
 
8012
        /*----------------------------------------------------------------------------------------------------------------*/
8013
 
8014
        // Extensions to the core rangy object
8015
 
8016
        api.innerText = function(el, characterOptions) {
8017
            var range = api.createRange(el);
8018
            range.selectNodeContents(el);
8019
            var text = range.text(characterOptions);
8020
            return text;
8021
        };
8022
 
8023
        api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
8024
            var session = getSession();
8025
            iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
8026
            var startPos = session.getPosition(startNode, startOffset);
8027
            var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
8028
            var backward = isDirectionBackward(iteratorOptions.direction);
8029
 
8030
            return {
8031
                next: function() {
8032
                    return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
8033
                },
8034
 
8035
                dispose: function() {
8036
                    tokenizedTextProvider.dispose();
8037
                    this.next = function() {};
8038
                }
8039
            };
8040
        };
8041
 
8042
        /*----------------------------------------------------------------------------------------------------------------*/
8043
 
8044
        api.noMutation = function(func) {
8045
            var session = getSession();
8046
            func(session);
8047
            endSession();
8048
        };
8049
 
8050
        api.noMutation.createEntryPointFunction = createEntryPointFunction;
8051
 
8052
        api.textRange = {
8053
            isBlockNode: isBlockNode,
8054
            isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
8055
 
8056
            createPosition: createEntryPointFunction(
8057
                function(session, node, offset) {
8058
                    return session.getPosition(node, offset);
8059
                }
8060
            )
8061
        };
8062
    });
8063
 
8064
    return rangy;
8065
}, this);YUI.add('moodle-editor_atto-rangy', function (Y, NAME) {
8066
 
8067
// This file is part of Moodle - http://moodle.org/
8068
//
8069
// Moodle is free software: you can redistribute it and/or modify
8070
// it under the terms of the GNU General Public License as published by
8071
// the Free Software Foundation, either version 3 of the License, or
8072
// (at your option) any later version.
8073
//
8074
// Moodle is distributed in the hope that it will be useful,
8075
// but WITHOUT ANY WARRANTY; without even the implied warranty of
8076
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8077
// GNU General Public License for more details.
8078
//
8079
// You should have received a copy of the GNU General Public License
8080
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
8081
 
8082
if (!rangy.initialized) {
8083
    rangy.init();
8084
}
8085
 
8086
 
8087
}, '@VERSION@', {"requires": []});