PageSourceSearch

https://rockbarn.com/public/assets/js/browserify_src/bundle_browserify.js

js rockbarn.com collected 2026-09-24 21:20:06 UTC 408,873 bytes, 9,725 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.