r110015 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r110014‎ | r110015 | r110016 >
Date:18:54, 25 January 2012
Author:inez
Status:deferred (Comments)
Tags:visualeditor 
Comment:
Hybrid of ContentEditable and Wikidom - initial import
Modified paths:
  • /trunk/extensions/VisualEditor/contentEditable (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/diff_match_patch.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/index.php (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/main.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/rangy (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/rangy/rangy-core.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/rangy/rangy-cssclassapplier.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/rangy/rangy-selectionsaverestore.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/rangy/rangy-serializer.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/views (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/views/es.ContentView.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/views/es.DocumentView.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/views/es.DocumentViewLeafNode.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/views/es.ParagraphView.js (added) (history)
  • /trunk/extensions/VisualEditor/contentEditable/views/es.SurfaceView.js (added) (history)
  • /trunk/extensions/VisualEditor/test (deleted) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/contentEditable/main.js
@@ -0,0 +1,109 @@
 2+$(document).ready( function() {
 3+ window.wikiDom = {
 4+ 'type': 'document',
 5+ 'children': [
 6+ {
 7+ 'type': 'paragraph',
 8+ 'content': {
 9+ 'text': 'Barack Hussein Obama II is the 44th and current President of the United States. He is the first African American to hold the office. Obama previously served as a United States Senator from Illinois, from January 2005 until he resigned following his victory in the 2008 presidential election.',
 10+ 'annotations': [
 11+ {
 12+ 'type': 'textStyle/bold',
 13+ 'range': {
 14+ 'start': 7,
 15+ 'end': 14
 16+ }
 17+ },
 18+ {
 19+ 'type': 'object/template',
 20+ 'data': {
 21+ 'html': '<sup><small>[<a href="#">citation needed</a>]</small></sup>'
 22+ },
 23+ 'range': {
 24+ 'start': 36,
 25+ 'end': 37
 26+ }
 27+ }
 28+ ]
 29+ }
 30+ },
 31+ {
 32+ 'type': 'paragraph',
 33+ 'content': { 'text': 'Born in Honolulu, Hawaii, Obama is a graduate of Columbia University and Harvard Law School, where he was the president of the Harvard Law Review. He was a community organizer in Chicago before earning his law degree. He worked as a civil rights attorney in Chicago and taught constitutional law at the University of Chicago Law School from 1992 to 2004. He served three terms representing the 13th District in the Illinois Senate from 1997 to 2004.' }
 34+ }
 35+ ]
 36+ };
 37+
 38+ window.documentModel = es.DocumentModel.newFromPlainObject( window.wikiDom );
 39+ window.surfaceModel = new es.SurfaceModel( window.documentModel );
 40+ window.surfaceView = new es.SurfaceView( $( '#es-editor' ), window.surfaceModel );
 41+
 42+
 43+ $('#es-editor')[0].addEventListener("DOMSubtreeModified", function() {
 44+ var selection = rangy.getSelection();
 45+ console.log(selection);
 46+ if(selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) {
 47+ $node = $(selection.anchorNode);
 48+ while(!$node.hasClass('es-paragraphView')) {
 49+ $node = $node.parent();
 50+ }
 51+ console.log($node.data('view'));
 52+ console.log(selection);
 53+ }
 54+ });
 55+
 56+
 57+ /*
 58+ $('#es-editor')[0].addEventListener("DOMSubtreeModified", function() {
 59+ var selection = rangy.getSelection();
 60+ console.log(selection);
 61+ if(selection.anchorNode === selection.focusNode && selection.anchorOffset === selection.focusOffset) {
 62+ $node = $(selection.anchorNode);
 63+ while(!$node.hasClass('es-paragraphView')) {
 64+ $node = $node.parent();
 65+ }
 66+ var newText = $node[0].textContent;
 67+ var view = $node.data('view');
 68+ var offset = surfaceView.documentView.getOffsetFromNode(view);
 69+ var oldText = documentModel.getContentText(new es.Range(offset, offset + 1 + view.getContentLength()));
 70+
 71+ newText = newText.replace(/\xA0/g,' ');
 72+ oldText = oldText.replace(/\xA0/g,' ');
 73+
 74+ if(newText.length > oldText.length) {
 75+ for( var i = 0; i < oldText.length; i++ ) {
 76+ if(newText[i] !== oldText[i]) {
 77+ var differenceStart = i;
 78+ break;
 79+ }
 80+ }
 81+
 82+ for( var i = oldText.length - 1; i >= 0; i--) {
 83+ if(newText[i + newText.length - oldText.length] !== oldText[i]) {
 84+ var differenceStop = i;
 85+ break;
 86+ }
 87+ }
 88+
 89+ var tx = documentModel.prepareRemoval(new es.Range(1+differenceStart,1+1+differenceStop));
 90+ documentModel.commit(tx);
 91+ var difference = newText.substring(differenceStart, 1+differenceStop + newText.length - oldText.length);
 92+ var tx = documentModel.prepareInsertion(differenceStart+1, difference.split());
 93+ documentModel.commit(tx);
 94+
 95+
 96+
 97+ }
 98+ }
 99+ });
 100+
 101+ $('#es-editor')[0].addEventListener("DOMCharacterDataModified", function() {
 102+ });
 103+ */
 104+ refreshPreview();
 105+ setInterval(refreshPreview, 500);
 106+} );
 107+
 108+function refreshPreview() {
 109+ $('#es-preview').text( es.WikitextSerializer.stringify( window.documentModel.getPlainObject() ) );
 110+}
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-core.js
@@ -0,0 +1,3211 @@
 2+/**
 3+ * @license Rangy, a cross-browser JavaScript range and selection library
 4+ * http://code.google.com/p/rangy/
 5+ *
 6+ * Copyright 2011, Tim Down
 7+ * Licensed under the MIT license.
 8+ * Version: 1.2.2
 9+ * Build date: 13 November 2011
 10+ */
 11+window['rangy'] = (function() {
 12+
 13+
 14+ var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
 15+
 16+ var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
 17+ "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
 18+
 19+ var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
 20+ "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
 21+ "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
 22+
 23+ var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
 24+
 25+ // Subset of TextRange's full set of methods that we're interested in
 26+ var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
 27+ "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
 28+
 29+ /*----------------------------------------------------------------------------------------------------------------*/
 30+
 31+ // Trio of functions taken from Peter Michaux's article:
 32+ // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
 33+ function isHostMethod(o, p) {
 34+ var t = typeof o[p];
 35+ return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
 36+ }
 37+
 38+ function isHostObject(o, p) {
 39+ return !!(typeof o[p] == OBJECT && o[p]);
 40+ }
 41+
 42+ function isHostProperty(o, p) {
 43+ return typeof o[p] != UNDEFINED;
 44+ }
 45+
 46+ // Creates a convenience function to save verbose repeated calls to tests functions
 47+ function createMultiplePropertyTest(testFunc) {
 48+ return function(o, props) {
 49+ var i = props.length;
 50+ while (i--) {
 51+ if (!testFunc(o, props[i])) {
 52+ return false;
 53+ }
 54+ }
 55+ return true;
 56+ };
 57+ }
 58+
 59+ // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
 60+ var areHostMethods = createMultiplePropertyTest(isHostMethod);
 61+ var areHostObjects = createMultiplePropertyTest(isHostObject);
 62+ var areHostProperties = createMultiplePropertyTest(isHostProperty);
 63+
 64+ function isTextRange(range) {
 65+ return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
 66+ }
 67+
 68+ var api = {
 69+ version: "1.2.2",
 70+ initialized: false,
 71+ supported: true,
 72+
 73+ util: {
 74+ isHostMethod: isHostMethod,
 75+ isHostObject: isHostObject,
 76+ isHostProperty: isHostProperty,
 77+ areHostMethods: areHostMethods,
 78+ areHostObjects: areHostObjects,
 79+ areHostProperties: areHostProperties,
 80+ isTextRange: isTextRange
 81+ },
 82+
 83+ features: {},
 84+
 85+ modules: {},
 86+ config: {
 87+ alertOnWarn: false,
 88+ preferTextRange: false
 89+ }
 90+ };
 91+
 92+ function fail(reason) {
 93+ window.alert("Rangy not supported in your browser. Reason: " + reason);
 94+ api.initialized = true;
 95+ api.supported = false;
 96+ }
 97+
 98+ api.fail = fail;
 99+
 100+ function warn(msg) {
 101+ var warningMessage = "Rangy warning: " + msg;
 102+ if (api.config.alertOnWarn) {
 103+ window.alert(warningMessage);
 104+ } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
 105+ window.console.log(warningMessage);
 106+ }
 107+ }
 108+
 109+ api.warn = warn;
 110+
 111+ if ({}.hasOwnProperty) {
 112+ api.util.extend = function(o, props) {
 113+ for (var i in props) {
 114+ if (props.hasOwnProperty(i)) {
 115+ o[i] = props[i];
 116+ }
 117+ }
 118+ };
 119+ } else {
 120+ fail("hasOwnProperty not supported");
 121+ }
 122+
 123+ var initListeners = [];
 124+ var moduleInitializers = [];
 125+
 126+ // Initialization
 127+ function init() {
 128+ if (api.initialized) {
 129+ return;
 130+ }
 131+ var testRange;
 132+ var implementsDomRange = false, implementsTextRange = false;
 133+
 134+ // First, perform basic feature tests
 135+
 136+ if (isHostMethod(document, "createRange")) {
 137+ testRange = document.createRange();
 138+ if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
 139+ implementsDomRange = true;
 140+ }
 141+ testRange.detach();
 142+ }
 143+
 144+ var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
 145+
 146+ if (body && isHostMethod(body, "createTextRange")) {
 147+ testRange = body.createTextRange();
 148+ if (isTextRange(testRange)) {
 149+ implementsTextRange = true;
 150+ }
 151+ }
 152+
 153+ if (!implementsDomRange && !implementsTextRange) {
 154+ fail("Neither Range nor TextRange are implemented");
 155+ }
 156+
 157+ api.initialized = true;
 158+ api.features = {
 159+ implementsDomRange: implementsDomRange,
 160+ implementsTextRange: implementsTextRange
 161+ };
 162+
 163+ // Initialize modules and call init listeners
 164+ var allListeners = moduleInitializers.concat(initListeners);
 165+ for (var i = 0, len = allListeners.length; i < len; ++i) {
 166+ try {
 167+ allListeners[i](api);
 168+ } catch (ex) {
 169+ if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
 170+ window.console.log("Init listener threw an exception. Continuing.", ex);
 171+ }
 172+
 173+ }
 174+ }
 175+ }
 176+
 177+ // Allow external scripts to initialize this library in case it's loaded after the document has loaded
 178+ api.init = init;
 179+
 180+ // Execute listener immediately if already initialized
 181+ api.addInitListener = function(listener) {
 182+ if (api.initialized) {
 183+ listener(api);
 184+ } else {
 185+ initListeners.push(listener);
 186+ }
 187+ };
 188+
 189+ var createMissingNativeApiListeners = [];
 190+
 191+ api.addCreateMissingNativeApiListener = function(listener) {
 192+ createMissingNativeApiListeners.push(listener);
 193+ };
 194+
 195+ function createMissingNativeApi(win) {
 196+ win = win || window;
 197+ init();
 198+
 199+ // Notify listeners
 200+ for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
 201+ createMissingNativeApiListeners[i](win);
 202+ }
 203+ }
 204+
 205+ api.createMissingNativeApi = createMissingNativeApi;
 206+
 207+ /**
 208+ * @constructor
 209+ */
 210+ function Module(name) {
 211+ this.name = name;
 212+ this.initialized = false;
 213+ this.supported = false;
 214+ }
 215+
 216+ Module.prototype.fail = function(reason) {
 217+ this.initialized = true;
 218+ this.supported = false;
 219+
 220+ throw new Error("Module '" + this.name + "' failed to load: " + reason);
 221+ };
 222+
 223+ Module.prototype.warn = function(msg) {
 224+ api.warn("Module " + this.name + ": " + msg);
 225+ };
 226+
 227+ Module.prototype.createError = function(msg) {
 228+ return new Error("Error in Rangy " + this.name + " module: " + msg);
 229+ };
 230+
 231+ api.createModule = function(name, initFunc) {
 232+ var module = new Module(name);
 233+ api.modules[name] = module;
 234+
 235+ moduleInitializers.push(function(api) {
 236+ initFunc(api, module);
 237+ module.initialized = true;
 238+ module.supported = true;
 239+ });
 240+ };
 241+
 242+ api.requireModules = function(modules) {
 243+ for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
 244+ moduleName = modules[i];
 245+ module = api.modules[moduleName];
 246+ if (!module || !(module instanceof Module)) {
 247+ throw new Error("Module '" + moduleName + "' not found");
 248+ }
 249+ if (!module.supported) {
 250+ throw new Error("Module '" + moduleName + "' not supported");
 251+ }
 252+ }
 253+ };
 254+
 255+ /*----------------------------------------------------------------------------------------------------------------*/
 256+
 257+ // Wait for document to load before running tests
 258+
 259+ var docReady = false;
 260+
 261+ var loadHandler = function(e) {
 262+
 263+ if (!docReady) {
 264+ docReady = true;
 265+ if (!api.initialized) {
 266+ init();
 267+ }
 268+ }
 269+ };
 270+
 271+ // Test whether we have window and document objects that we will need
 272+ if (typeof window == UNDEFINED) {
 273+ fail("No window found");
 274+ return;
 275+ }
 276+ if (typeof document == UNDEFINED) {
 277+ fail("No document found");
 278+ return;
 279+ }
 280+
 281+ if (isHostMethod(document, "addEventListener")) {
 282+ document.addEventListener("DOMContentLoaded", loadHandler, false);
 283+ }
 284+
 285+ // Add a fallback in case the DOMContentLoaded event isn't supported
 286+ if (isHostMethod(window, "addEventListener")) {
 287+ window.addEventListener("load", loadHandler, false);
 288+ } else if (isHostMethod(window, "attachEvent")) {
 289+ window.attachEvent("onload", loadHandler);
 290+ } else {
 291+ fail("Window does not have required addEventListener or attachEvent method");
 292+ }
 293+
 294+ return api;
 295+})();
 296+rangy.createModule("DomUtil", function(api, module) {
 297+
 298+ var UNDEF = "undefined";
 299+ var util = api.util;
 300+
 301+ // Perform feature tests
 302+ if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
 303+ module.fail("document missing a Node creation method");
 304+ }
 305+
 306+ if (!util.isHostMethod(document, "getElementsByTagName")) {
 307+ module.fail("document missing getElementsByTagName method");
 308+ }
 309+
 310+ var el = document.createElement("div");
 311+ if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
 312+ !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
 313+ module.fail("Incomplete Element implementation");
 314+ }
 315+
 316+ // innerHTML is required for Range's createContextualFragment method
 317+ if (!util.isHostProperty(el, "innerHTML")) {
 318+ module.fail("Element is missing innerHTML property");
 319+ }
 320+
 321+ var textNode = document.createTextNode("test");
 322+ if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
 323+ !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
 324+ !util.areHostProperties(textNode, ["data"]))) {
 325+ module.fail("Incomplete Text Node implementation");
 326+ }
 327+
 328+ /*----------------------------------------------------------------------------------------------------------------*/
 329+
 330+ // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
 331+ // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
 332+ // contains just the document as a single element and the value searched for is the document.
 333+ var arrayContains = /*Array.prototype.indexOf ?
 334+ function(arr, val) {
 335+ return arr.indexOf(val) > -1;
 336+ }:*/
 337+
 338+ function(arr, val) {
 339+ var i = arr.length;
 340+ while (i--) {
 341+ if (arr[i] === val) {
 342+ return true;
 343+ }
 344+ }
 345+ return false;
 346+ };
 347+
 348+ // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
 349+ function isHtmlNamespace(node) {
 350+ var ns;
 351+ return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
 352+ }
 353+
 354+ function parentElement(node) {
 355+ var parent = node.parentNode;
 356+ return (parent.nodeType == 1) ? parent : null;
 357+ }
 358+
 359+ function getNodeIndex(node) {
 360+ var i = 0;
 361+ while( (node = node.previousSibling) ) {
 362+ i++;
 363+ }
 364+ return i;
 365+ }
 366+
 367+ function getNodeLength(node) {
 368+ var childNodes;
 369+ return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
 370+ }
 371+
 372+ function getCommonAncestor(node1, node2) {
 373+ var ancestors = [], n;
 374+ for (n = node1; n; n = n.parentNode) {
 375+ ancestors.push(n);
 376+ }
 377+
 378+ for (n = node2; n; n = n.parentNode) {
 379+ if (arrayContains(ancestors, n)) {
 380+ return n;
 381+ }
 382+ }
 383+
 384+ return null;
 385+ }
 386+
 387+ function isAncestorOf(ancestor, descendant, selfIsAncestor) {
 388+ var n = selfIsAncestor ? descendant : descendant.parentNode;
 389+ while (n) {
 390+ if (n === ancestor) {
 391+ return true;
 392+ } else {
 393+ n = n.parentNode;
 394+ }
 395+ }
 396+ return false;
 397+ }
 398+
 399+ function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
 400+ var p, n = selfIsAncestor ? node : node.parentNode;
 401+ while (n) {
 402+ p = n.parentNode;
 403+ if (p === ancestor) {
 404+ return n;
 405+ }
 406+ n = p;
 407+ }
 408+ return null;
 409+ }
 410+
 411+ function isCharacterDataNode(node) {
 412+ var t = node.nodeType;
 413+ return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
 414+ }
 415+
 416+ function insertAfter(node, precedingNode) {
 417+ var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
 418+ if (nextNode) {
 419+ parent.insertBefore(node, nextNode);
 420+ } else {
 421+ parent.appendChild(node);
 422+ }
 423+ return node;
 424+ }
 425+
 426+ // Note that we cannot use splitText() because it is bugridden in IE 9.
 427+ function splitDataNode(node, index) {
 428+ var newNode = node.cloneNode(false);
 429+ newNode.deleteData(0, index);
 430+ node.deleteData(index, node.length - index);
 431+ insertAfter(newNode, node);
 432+ return newNode;
 433+ }
 434+
 435+ function getDocument(node) {
 436+ if (node.nodeType == 9) {
 437+ return node;
 438+ } else if (typeof node.ownerDocument != UNDEF) {
 439+ return node.ownerDocument;
 440+ } else if (typeof node.document != UNDEF) {
 441+ return node.document;
 442+ } else if (node.parentNode) {
 443+ return getDocument(node.parentNode);
 444+ } else {
 445+ throw new Error("getDocument: no document found for node");
 446+ }
 447+ }
 448+
 449+ function getWindow(node) {
 450+ var doc = getDocument(node);
 451+ if (typeof doc.defaultView != UNDEF) {
 452+ return doc.defaultView;
 453+ } else if (typeof doc.parentWindow != UNDEF) {
 454+ return doc.parentWindow;
 455+ } else {
 456+ throw new Error("Cannot get a window object for node");
 457+ }
 458+ }
 459+
 460+ function getIframeDocument(iframeEl) {
 461+ if (typeof iframeEl.contentDocument != UNDEF) {
 462+ return iframeEl.contentDocument;
 463+ } else if (typeof iframeEl.contentWindow != UNDEF) {
 464+ return iframeEl.contentWindow.document;
 465+ } else {
 466+ throw new Error("getIframeWindow: No Document object found for iframe element");
 467+ }
 468+ }
 469+
 470+ function getIframeWindow(iframeEl) {
 471+ if (typeof iframeEl.contentWindow != UNDEF) {
 472+ return iframeEl.contentWindow;
 473+ } else if (typeof iframeEl.contentDocument != UNDEF) {
 474+ return iframeEl.contentDocument.defaultView;
 475+ } else {
 476+ throw new Error("getIframeWindow: No Window object found for iframe element");
 477+ }
 478+ }
 479+
 480+ function getBody(doc) {
 481+ return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
 482+ }
 483+
 484+ function getRootContainer(node) {
 485+ var parent;
 486+ while ( (parent = node.parentNode) ) {
 487+ node = parent;
 488+ }
 489+ return node;
 490+ }
 491+
 492+ function comparePoints(nodeA, offsetA, nodeB, offsetB) {
 493+ // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
 494+ var nodeC, root, childA, childB, n;
 495+ if (nodeA == nodeB) {
 496+
 497+ // Case 1: nodes are the same
 498+ return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
 499+ } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
 500+
 501+ // Case 2: node C (container B or an ancestor) is a child node of A
 502+ return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
 503+ } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
 504+
 505+ // Case 3: node C (container A or an ancestor) is a child node of B
 506+ return getNodeIndex(nodeC) < offsetB ? -1 : 1;
 507+ } else {
 508+
 509+ // Case 4: containers are siblings or descendants of siblings
 510+ root = getCommonAncestor(nodeA, nodeB);
 511+ childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
 512+ childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
 513+
 514+ if (childA === childB) {
 515+ // This shouldn't be possible
 516+
 517+ throw new Error("comparePoints got to case 4 and childA and childB are the same!");
 518+ } else {
 519+ n = root.firstChild;
 520+ while (n) {
 521+ if (n === childA) {
 522+ return -1;
 523+ } else if (n === childB) {
 524+ return 1;
 525+ }
 526+ n = n.nextSibling;
 527+ }
 528+ throw new Error("Should not be here!");
 529+ }
 530+ }
 531+ }
 532+
 533+ function fragmentFromNodeChildren(node) {
 534+ var fragment = getDocument(node).createDocumentFragment(), child;
 535+ while ( (child = node.firstChild) ) {
 536+ fragment.appendChild(child);
 537+ }
 538+ return fragment;
 539+ }
 540+
 541+ function inspectNode(node) {
 542+ if (!node) {
 543+ return "[No node]";
 544+ }
 545+ if (isCharacterDataNode(node)) {
 546+ return '"' + node.data + '"';
 547+ } else if (node.nodeType == 1) {
 548+ var idAttr = node.id ? ' id="' + node.id + '"' : "";
 549+ return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
 550+ } else {
 551+ return node.nodeName;
 552+ }
 553+ }
 554+
 555+ /**
 556+ * @constructor
 557+ */
 558+ function NodeIterator(root) {
 559+ this.root = root;
 560+ this._next = root;
 561+ }
 562+
 563+ NodeIterator.prototype = {
 564+ _current: null,
 565+
 566+ hasNext: function() {
 567+ return !!this._next;
 568+ },
 569+
 570+ next: function() {
 571+ var n = this._current = this._next;
 572+ var child, next;
 573+ if (this._current) {
 574+ child = n.firstChild;
 575+ if (child) {
 576+ this._next = child;
 577+ } else {
 578+ next = null;
 579+ while ((n !== this.root) && !(next = n.nextSibling)) {
 580+ n = n.parentNode;
 581+ }
 582+ this._next = next;
 583+ }
 584+ }
 585+ return this._current;
 586+ },
 587+
 588+ detach: function() {
 589+ this._current = this._next = this.root = null;
 590+ }
 591+ };
 592+
 593+ function createIterator(root) {
 594+ return new NodeIterator(root);
 595+ }
 596+
 597+ /**
 598+ * @constructor
 599+ */
 600+ function DomPosition(node, offset) {
 601+ this.node = node;
 602+ this.offset = offset;
 603+ }
 604+
 605+ DomPosition.prototype = {
 606+ equals: function(pos) {
 607+ return this.node === pos.node & this.offset == pos.offset;
 608+ },
 609+
 610+ inspect: function() {
 611+ return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
 612+ }
 613+ };
 614+
 615+ /**
 616+ * @constructor
 617+ */
 618+ function DOMException(codeName) {
 619+ this.code = this[codeName];
 620+ this.codeName = codeName;
 621+ this.message = "DOMException: " + this.codeName;
 622+ }
 623+
 624+ DOMException.prototype = {
 625+ INDEX_SIZE_ERR: 1,
 626+ HIERARCHY_REQUEST_ERR: 3,
 627+ WRONG_DOCUMENT_ERR: 4,
 628+ NO_MODIFICATION_ALLOWED_ERR: 7,
 629+ NOT_FOUND_ERR: 8,
 630+ NOT_SUPPORTED_ERR: 9,
 631+ INVALID_STATE_ERR: 11
 632+ };
 633+
 634+ DOMException.prototype.toString = function() {
 635+ return this.message;
 636+ };
 637+
 638+ api.dom = {
 639+ arrayContains: arrayContains,
 640+ isHtmlNamespace: isHtmlNamespace,
 641+ parentElement: parentElement,
 642+ getNodeIndex: getNodeIndex,
 643+ getNodeLength: getNodeLength,
 644+ getCommonAncestor: getCommonAncestor,
 645+ isAncestorOf: isAncestorOf,
 646+ getClosestAncestorIn: getClosestAncestorIn,
 647+ isCharacterDataNode: isCharacterDataNode,
 648+ insertAfter: insertAfter,
 649+ splitDataNode: splitDataNode,
 650+ getDocument: getDocument,
 651+ getWindow: getWindow,
 652+ getIframeWindow: getIframeWindow,
 653+ getIframeDocument: getIframeDocument,
 654+ getBody: getBody,
 655+ getRootContainer: getRootContainer,
 656+ comparePoints: comparePoints,
 657+ inspectNode: inspectNode,
 658+ fragmentFromNodeChildren: fragmentFromNodeChildren,
 659+ createIterator: createIterator,
 660+ DomPosition: DomPosition
 661+ };
 662+
 663+ api.DOMException = DOMException;
 664+});rangy.createModule("DomRange", function(api, module) {
 665+ api.requireModules( ["DomUtil"] );
 666+
 667+
 668+ var dom = api.dom;
 669+ var DomPosition = dom.DomPosition;
 670+ var DOMException = api.DOMException;
 671+
 672+ /*----------------------------------------------------------------------------------------------------------------*/
 673+
 674+ // Utility functions
 675+
 676+ function isNonTextPartiallySelected(node, range) {
 677+ return (node.nodeType != 3) &&
 678+ (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
 679+ }
 680+
 681+ function getRangeDocument(range) {
 682+ return dom.getDocument(range.startContainer);
 683+ }
 684+
 685+ function dispatchEvent(range, type, args) {
 686+ var listeners = range._listeners[type];
 687+ if (listeners) {
 688+ for (var i = 0, len = listeners.length; i < len; ++i) {
 689+ listeners[i].call(range, {target: range, args: args});
 690+ }
 691+ }
 692+ }
 693+
 694+ function getBoundaryBeforeNode(node) {
 695+ return new DomPosition(node.parentNode, dom.getNodeIndex(node));
 696+ }
 697+
 698+ function getBoundaryAfterNode(node) {
 699+ return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
 700+ }
 701+
 702+ function insertNodeAtPosition(node, n, o) {
 703+ var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
 704+ if (dom.isCharacterDataNode(n)) {
 705+ if (o == n.length) {
 706+ dom.insertAfter(node, n);
 707+ } else {
 708+ n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
 709+ }
 710+ } else if (o >= n.childNodes.length) {
 711+ n.appendChild(node);
 712+ } else {
 713+ n.insertBefore(node, n.childNodes[o]);
 714+ }
 715+ return firstNodeInserted;
 716+ }
 717+
 718+ function cloneSubtree(iterator) {
 719+ var partiallySelected;
 720+ for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
 721+ partiallySelected = iterator.isPartiallySelectedSubtree();
 722+
 723+ node = node.cloneNode(!partiallySelected);
 724+ if (partiallySelected) {
 725+ subIterator = iterator.getSubtreeIterator();
 726+ node.appendChild(cloneSubtree(subIterator));
 727+ subIterator.detach(true);
 728+ }
 729+
 730+ if (node.nodeType == 10) { // DocumentType
 731+ throw new DOMException("HIERARCHY_REQUEST_ERR");
 732+ }
 733+ frag.appendChild(node);
 734+ }
 735+ return frag;
 736+ }
 737+
 738+ function iterateSubtree(rangeIterator, func, iteratorState) {
 739+ var it, n;
 740+ iteratorState = iteratorState || { stop: false };
 741+ for (var node, subRangeIterator; node = rangeIterator.next(); ) {
 742+ //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
 743+ if (rangeIterator.isPartiallySelectedSubtree()) {
 744+ // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
 745+ // node selected by the Range.
 746+ if (func(node) === false) {
 747+ iteratorState.stop = true;
 748+ return;
 749+ } else {
 750+ subRangeIterator = rangeIterator.getSubtreeIterator();
 751+ iterateSubtree(subRangeIterator, func, iteratorState);
 752+ subRangeIterator.detach(true);
 753+ if (iteratorState.stop) {
 754+ return;
 755+ }
 756+ }
 757+ } else {
 758+ // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
 759+ // descendant
 760+ it = dom.createIterator(node);
 761+ while ( (n = it.next()) ) {
 762+ if (func(n) === false) {
 763+ iteratorState.stop = true;
 764+ return;
 765+ }
 766+ }
 767+ }
 768+ }
 769+ }
 770+
 771+ function deleteSubtree(iterator) {
 772+ var subIterator;
 773+ while (iterator.next()) {
 774+ if (iterator.isPartiallySelectedSubtree()) {
 775+ subIterator = iterator.getSubtreeIterator();
 776+ deleteSubtree(subIterator);
 777+ subIterator.detach(true);
 778+ } else {
 779+ iterator.remove();
 780+ }
 781+ }
 782+ }
 783+
 784+ function extractSubtree(iterator) {
 785+
 786+ for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
 787+
 788+
 789+ if (iterator.isPartiallySelectedSubtree()) {
 790+ node = node.cloneNode(false);
 791+ subIterator = iterator.getSubtreeIterator();
 792+ node.appendChild(extractSubtree(subIterator));
 793+ subIterator.detach(true);
 794+ } else {
 795+ iterator.remove();
 796+ }
 797+ if (node.nodeType == 10) { // DocumentType
 798+ throw new DOMException("HIERARCHY_REQUEST_ERR");
 799+ }
 800+ frag.appendChild(node);
 801+ }
 802+ return frag;
 803+ }
 804+
 805+ function getNodesInRange(range, nodeTypes, filter) {
 806+ //log.info("getNodesInRange, " + nodeTypes.join(","));
 807+ var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
 808+ var filterExists = !!filter;
 809+ if (filterNodeTypes) {
 810+ regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
 811+ }
 812+
 813+ var nodes = [];
 814+ iterateSubtree(new RangeIterator(range, false), function(node) {
 815+ if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
 816+ nodes.push(node);
 817+ }
 818+ });
 819+ return nodes;
 820+ }
 821+
 822+ function inspect(range) {
 823+ var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
 824+ return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
 825+ dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
 826+ }
 827+
 828+ /*----------------------------------------------------------------------------------------------------------------*/
 829+
 830+ // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
 831+
 832+ /**
 833+ * @constructor
 834+ */
 835+ function RangeIterator(range, clonePartiallySelectedTextNodes) {
 836+ this.range = range;
 837+ this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
 838+
 839+
 840+
 841+ if (!range.collapsed) {
 842+ this.sc = range.startContainer;
 843+ this.so = range.startOffset;
 844+ this.ec = range.endContainer;
 845+ this.eo = range.endOffset;
 846+ var root = range.commonAncestorContainer;
 847+
 848+ if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
 849+ this.isSingleCharacterDataNode = true;
 850+ this._first = this._last = this._next = this.sc;
 851+ } else {
 852+ this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
 853+ this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
 854+ this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
 855+ this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
 856+ }
 857+
 858+ }
 859+ }
 860+
 861+ RangeIterator.prototype = {
 862+ _current: null,
 863+ _next: null,
 864+ _first: null,
 865+ _last: null,
 866+ isSingleCharacterDataNode: false,
 867+
 868+ reset: function() {
 869+ this._current = null;
 870+ this._next = this._first;
 871+ },
 872+
 873+ hasNext: function() {
 874+ return !!this._next;
 875+ },
 876+
 877+ next: function() {
 878+ // Move to next node
 879+ var current = this._current = this._next;
 880+ if (current) {
 881+ this._next = (current !== this._last) ? current.nextSibling : null;
 882+
 883+ // Check for partially selected text nodes
 884+ if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
 885+ if (current === this.ec) {
 886+
 887+ (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
 888+ }
 889+ if (this._current === this.sc) {
 890+
 891+ (current = current.cloneNode(true)).deleteData(0, this.so);
 892+ }
 893+ }
 894+ }
 895+
 896+ return current;
 897+ },
 898+
 899+ remove: function() {
 900+ var current = this._current, start, end;
 901+
 902+ if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
 903+ start = (current === this.sc) ? this.so : 0;
 904+ end = (current === this.ec) ? this.eo : current.length;
 905+ if (start != end) {
 906+ current.deleteData(start, end - start);
 907+ }
 908+ } else {
 909+ if (current.parentNode) {
 910+ current.parentNode.removeChild(current);
 911+ } else {
 912+
 913+ }
 914+ }
 915+ },
 916+
 917+ // Checks if the current node is partially selected
 918+ isPartiallySelectedSubtree: function() {
 919+ var current = this._current;
 920+ return isNonTextPartiallySelected(current, this.range);
 921+ },
 922+
 923+ getSubtreeIterator: function() {
 924+ var subRange;
 925+ if (this.isSingleCharacterDataNode) {
 926+ subRange = this.range.cloneRange();
 927+ subRange.collapse();
 928+ } else {
 929+ subRange = new Range(getRangeDocument(this.range));
 930+ var current = this._current;
 931+ var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
 932+
 933+ if (dom.isAncestorOf(current, this.sc, true)) {
 934+ startContainer = this.sc;
 935+ startOffset = this.so;
 936+ }
 937+ if (dom.isAncestorOf(current, this.ec, true)) {
 938+ endContainer = this.ec;
 939+ endOffset = this.eo;
 940+ }
 941+
 942+ updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
 943+ }
 944+ return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
 945+ },
 946+
 947+ detach: function(detachRange) {
 948+ if (detachRange) {
 949+ this.range.detach();
 950+ }
 951+ this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
 952+ }
 953+ };
 954+
 955+ /*----------------------------------------------------------------------------------------------------------------*/
 956+
 957+ // Exceptions
 958+
 959+ /**
 960+ * @constructor
 961+ */
 962+ function RangeException(codeName) {
 963+ this.code = this[codeName];
 964+ this.codeName = codeName;
 965+ this.message = "RangeException: " + this.codeName;
 966+ }
 967+
 968+ RangeException.prototype = {
 969+ BAD_BOUNDARYPOINTS_ERR: 1,
 970+ INVALID_NODE_TYPE_ERR: 2
 971+ };
 972+
 973+ RangeException.prototype.toString = function() {
 974+ return this.message;
 975+ };
 976+
 977+ /*----------------------------------------------------------------------------------------------------------------*/
 978+
 979+ /**
 980+ * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
 981+ * TODO: Look into making this a proper iterator, not requiring preloading everything first
 982+ * @constructor
 983+ */
 984+ function RangeNodeIterator(range, nodeTypes, filter) {
 985+ this.nodes = getNodesInRange(range, nodeTypes, filter);
 986+ this._next = this.nodes[0];
 987+ this._position = 0;
 988+ }
 989+
 990+ RangeNodeIterator.prototype = {
 991+ _current: null,
 992+
 993+ hasNext: function() {
 994+ return !!this._next;
 995+ },
 996+
 997+ next: function() {
 998+ this._current = this._next;
 999+ this._next = this.nodes[ ++this._position ];
 1000+ return this._current;
 1001+ },
 1002+
 1003+ detach: function() {
 1004+ this._current = this._next = this.nodes = null;
 1005+ }
 1006+ };
 1007+
 1008+ var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
 1009+ var rootContainerNodeTypes = [2, 9, 11];
 1010+ var readonlyNodeTypes = [5, 6, 10, 12];
 1011+ var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
 1012+ var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
 1013+
 1014+ function createAncestorFinder(nodeTypes) {
 1015+ return function(node, selfIsAncestor) {
 1016+ var t, n = selfIsAncestor ? node : node.parentNode;
 1017+ while (n) {
 1018+ t = n.nodeType;
 1019+ if (dom.arrayContains(nodeTypes, t)) {
 1020+ return n;
 1021+ }
 1022+ n = n.parentNode;
 1023+ }
 1024+ return null;
 1025+ };
 1026+ }
 1027+
 1028+ var getRootContainer = dom.getRootContainer;
 1029+ var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
 1030+ var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
 1031+ var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
 1032+
 1033+ function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
 1034+ if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
 1035+ throw new RangeException("INVALID_NODE_TYPE_ERR");
 1036+ }
 1037+ }
 1038+
 1039+ function assertNotDetached(range) {
 1040+ if (!range.startContainer) {
 1041+ throw new DOMException("INVALID_STATE_ERR");
 1042+ }
 1043+ }
 1044+
 1045+ function assertValidNodeType(node, invalidTypes) {
 1046+ if (!dom.arrayContains(invalidTypes, node.nodeType)) {
 1047+ throw new RangeException("INVALID_NODE_TYPE_ERR");
 1048+ }
 1049+ }
 1050+
 1051+ function assertValidOffset(node, offset) {
 1052+ if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
 1053+ throw new DOMException("INDEX_SIZE_ERR");
 1054+ }
 1055+ }
 1056+
 1057+ function assertSameDocumentOrFragment(node1, node2) {
 1058+ if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
 1059+ throw new DOMException("WRONG_DOCUMENT_ERR");
 1060+ }
 1061+ }
 1062+
 1063+ function assertNodeNotReadOnly(node) {
 1064+ if (getReadonlyAncestor(node, true)) {
 1065+ throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
 1066+ }
 1067+ }
 1068+
 1069+ function assertNode(node, codeName) {
 1070+ if (!node) {
 1071+ throw new DOMException(codeName);
 1072+ }
 1073+ }
 1074+
 1075+ function isOrphan(node) {
 1076+ return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
 1077+ }
 1078+
 1079+ function isValidOffset(node, offset) {
 1080+ return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
 1081+ }
 1082+
 1083+ function assertRangeValid(range) {
 1084+ assertNotDetached(range);
 1085+ if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
 1086+ !isValidOffset(range.startContainer, range.startOffset) ||
 1087+ !isValidOffset(range.endContainer, range.endOffset)) {
 1088+ throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
 1089+ }
 1090+ }
 1091+
 1092+ /*----------------------------------------------------------------------------------------------------------------*/
 1093+
 1094+ // Test the browser's innerHTML support to decide how to implement createContextualFragment
 1095+ var styleEl = document.createElement("style");
 1096+ var htmlParsingConforms = false;
 1097+ try {
 1098+ styleEl.innerHTML = "<b>x</b>";
 1099+ htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
 1100+ } catch (e) {
 1101+ // IE 6 and 7 throw
 1102+ }
 1103+
 1104+ api.features.htmlParsingConforms = htmlParsingConforms;
 1105+
 1106+ var createContextualFragment = htmlParsingConforms ?
 1107+
 1108+ // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
 1109+ // discussion and base code for this implementation at issue 67.
 1110+ // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
 1111+ // Thanks to Aleks Williams.
 1112+ function(fragmentStr) {
 1113+ // "Let node the context object's start's node."
 1114+ var node = this.startContainer;
 1115+ var doc = dom.getDocument(node);
 1116+
 1117+ // "If the context object's start's node is null, raise an INVALID_STATE_ERR
 1118+ // exception and abort these steps."
 1119+ if (!node) {
 1120+ throw new DOMException("INVALID_STATE_ERR");
 1121+ }
 1122+
 1123+ // "Let element be as follows, depending on node's interface:"
 1124+ // Document, Document Fragment: null
 1125+ var el = null;
 1126+
 1127+ // "Element: node"
 1128+ if (node.nodeType == 1) {
 1129+ el = node;
 1130+
 1131+ // "Text, Comment: node's parentElement"
 1132+ } else if (dom.isCharacterDataNode(node)) {
 1133+ el = dom.parentElement(node);
 1134+ }
 1135+
 1136+ // "If either element is null or element's ownerDocument is an HTML document
 1137+ // and element's local name is "html" and element's namespace is the HTML
 1138+ // namespace"
 1139+ if (el === null || (
 1140+ el.nodeName == "HTML"
 1141+ && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
 1142+ && dom.isHtmlNamespace(el)
 1143+ )) {
 1144+
 1145+ // "let element be a new Element with "body" as its local name and the HTML
 1146+ // namespace as its namespace.""
 1147+ el = doc.createElement("body");
 1148+ } else {
 1149+ el = el.cloneNode(false);
 1150+ }
 1151+
 1152+ // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
 1153+ // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
 1154+ // "In either case, the algorithm must be invoked with fragment as the input
 1155+ // and element as the context element."
 1156+ el.innerHTML = fragmentStr;
 1157+
 1158+ // "If this raises an exception, then abort these steps. Otherwise, let new
 1159+ // children be the nodes returned."
 1160+
 1161+ // "Let fragment be a new DocumentFragment."
 1162+ // "Append all new children to fragment."
 1163+ // "Return fragment."
 1164+ return dom.fragmentFromNodeChildren(el);
 1165+ } :
 1166+
 1167+ // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
 1168+ // previous versions of Rangy used (with the exception of using a body element rather than a div)
 1169+ function(fragmentStr) {
 1170+ assertNotDetached(this);
 1171+ var doc = getRangeDocument(this);
 1172+ var el = doc.createElement("body");
 1173+ el.innerHTML = fragmentStr;
 1174+
 1175+ return dom.fragmentFromNodeChildren(el);
 1176+ };
 1177+
 1178+ /*----------------------------------------------------------------------------------------------------------------*/
 1179+
 1180+ var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
 1181+ "commonAncestorContainer"];
 1182+
 1183+ var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
 1184+ var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
 1185+
 1186+ function RangePrototype() {}
 1187+
 1188+ RangePrototype.prototype = {
 1189+ attachListener: function(type, listener) {
 1190+ this._listeners[type].push(listener);
 1191+ },
 1192+
 1193+ compareBoundaryPoints: function(how, range) {
 1194+ assertRangeValid(this);
 1195+ assertSameDocumentOrFragment(this.startContainer, range.startContainer);
 1196+
 1197+ var nodeA, offsetA, nodeB, offsetB;
 1198+ var prefixA = (how == e2s || how == s2s) ? "start" : "end";
 1199+ var prefixB = (how == s2e || how == s2s) ? "start" : "end";
 1200+ nodeA = this[prefixA + "Container"];
 1201+ offsetA = this[prefixA + "Offset"];
 1202+ nodeB = range[prefixB + "Container"];
 1203+ offsetB = range[prefixB + "Offset"];
 1204+ return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
 1205+ },
 1206+
 1207+ insertNode: function(node) {
 1208+ assertRangeValid(this);
 1209+ assertValidNodeType(node, insertableNodeTypes);
 1210+ assertNodeNotReadOnly(this.startContainer);
 1211+
 1212+ if (dom.isAncestorOf(node, this.startContainer, true)) {
 1213+ throw new DOMException("HIERARCHY_REQUEST_ERR");
 1214+ }
 1215+
 1216+ // No check for whether the container of the start of the Range is of a type that does not allow
 1217+ // children of the type of node: the browser's DOM implementation should do this for us when we attempt
 1218+ // to add the node
 1219+
 1220+ var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
 1221+ this.setStartBefore(firstNodeInserted);
 1222+ },
 1223+
 1224+ cloneContents: function() {
 1225+ assertRangeValid(this);
 1226+
 1227+ var clone, frag;
 1228+ if (this.collapsed) {
 1229+ return getRangeDocument(this).createDocumentFragment();
 1230+ } else {
 1231+ if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
 1232+ clone = this.startContainer.cloneNode(true);
 1233+ clone.data = clone.data.slice(this.startOffset, this.endOffset);
 1234+ frag = getRangeDocument(this).createDocumentFragment();
 1235+ frag.appendChild(clone);
 1236+ return frag;
 1237+ } else {
 1238+ var iterator = new RangeIterator(this, true);
 1239+ clone = cloneSubtree(iterator);
 1240+ iterator.detach();
 1241+ }
 1242+ return clone;
 1243+ }
 1244+ },
 1245+
 1246+ canSurroundContents: function() {
 1247+ assertRangeValid(this);
 1248+ assertNodeNotReadOnly(this.startContainer);
 1249+ assertNodeNotReadOnly(this.endContainer);
 1250+
 1251+ // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
 1252+ // no non-text nodes.
 1253+ var iterator = new RangeIterator(this, true);
 1254+ var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
 1255+ (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
 1256+ iterator.detach();
 1257+ return !boundariesInvalid;
 1258+ },
 1259+
 1260+ surroundContents: function(node) {
 1261+ assertValidNodeType(node, surroundNodeTypes);
 1262+
 1263+ if (!this.canSurroundContents()) {
 1264+ throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
 1265+ }
 1266+
 1267+ // Extract the contents
 1268+ var content = this.extractContents();
 1269+
 1270+ // Clear the children of the node
 1271+ if (node.hasChildNodes()) {
 1272+ while (node.lastChild) {
 1273+ node.removeChild(node.lastChild);
 1274+ }
 1275+ }
 1276+
 1277+ // Insert the new node and add the extracted contents
 1278+ insertNodeAtPosition(node, this.startContainer, this.startOffset);
 1279+ node.appendChild(content);
 1280+
 1281+ this.selectNode(node);
 1282+ },
 1283+
 1284+ cloneRange: function() {
 1285+ assertRangeValid(this);
 1286+ var range = new Range(getRangeDocument(this));
 1287+ var i = rangeProperties.length, prop;
 1288+ while (i--) {
 1289+ prop = rangeProperties[i];
 1290+ range[prop] = this[prop];
 1291+ }
 1292+ return range;
 1293+ },
 1294+
 1295+ toString: function() {
 1296+ assertRangeValid(this);
 1297+ var sc = this.startContainer;
 1298+ if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
 1299+ return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
 1300+ } else {
 1301+ var textBits = [], iterator = new RangeIterator(this, true);
 1302+
 1303+ iterateSubtree(iterator, function(node) {
 1304+ // Accept only text or CDATA nodes, not comments
 1305+
 1306+ if (node.nodeType == 3 || node.nodeType == 4) {
 1307+ textBits.push(node.data);
 1308+ }
 1309+ });
 1310+ iterator.detach();
 1311+ return textBits.join("");
 1312+ }
 1313+ },
 1314+
 1315+ // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
 1316+ // been removed from Mozilla.
 1317+
 1318+ compareNode: function(node) {
 1319+ assertRangeValid(this);
 1320+
 1321+ var parent = node.parentNode;
 1322+ var nodeIndex = dom.getNodeIndex(node);
 1323+
 1324+ if (!parent) {
 1325+ throw new DOMException("NOT_FOUND_ERR");
 1326+ }
 1327+
 1328+ var startComparison = this.comparePoint(parent, nodeIndex),
 1329+ endComparison = this.comparePoint(parent, nodeIndex + 1);
 1330+
 1331+ if (startComparison < 0) { // Node starts before
 1332+ return (endComparison > 0) ? n_b_a : n_b;
 1333+ } else {
 1334+ return (endComparison > 0) ? n_a : n_i;
 1335+ }
 1336+ },
 1337+
 1338+ comparePoint: function(node, offset) {
 1339+ assertRangeValid(this);
 1340+ assertNode(node, "HIERARCHY_REQUEST_ERR");
 1341+ assertSameDocumentOrFragment(node, this.startContainer);
 1342+
 1343+ if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
 1344+ return -1;
 1345+ } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
 1346+ return 1;
 1347+ }
 1348+ return 0;
 1349+ },
 1350+
 1351+ createContextualFragment: createContextualFragment,
 1352+
 1353+ toHtml: function() {
 1354+ assertRangeValid(this);
 1355+ var container = getRangeDocument(this).createElement("div");
 1356+ container.appendChild(this.cloneContents());
 1357+ return container.innerHTML;
 1358+ },
 1359+
 1360+ // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
 1361+ // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
 1362+ intersectsNode: function(node, touchingIsIntersecting) {
 1363+ assertRangeValid(this);
 1364+ assertNode(node, "NOT_FOUND_ERR");
 1365+ if (dom.getDocument(node) !== getRangeDocument(this)) {
 1366+ return false;
 1367+ }
 1368+
 1369+ var parent = node.parentNode, offset = dom.getNodeIndex(node);
 1370+ assertNode(parent, "NOT_FOUND_ERR");
 1371+
 1372+ var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
 1373+ endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
 1374+
 1375+ return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
 1376+ },
 1377+
 1378+
 1379+ isPointInRange: function(node, offset) {
 1380+ assertRangeValid(this);
 1381+ assertNode(node, "HIERARCHY_REQUEST_ERR");
 1382+ assertSameDocumentOrFragment(node, this.startContainer);
 1383+
 1384+ return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
 1385+ (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
 1386+ },
 1387+
 1388+ // The methods below are non-standard and invented by me.
 1389+
 1390+ // Sharing a boundary start-to-end or end-to-start does not count as intersection.
 1391+ intersectsRange: function(range, touchingIsIntersecting) {
 1392+ assertRangeValid(this);
 1393+
 1394+ if (getRangeDocument(range) != getRangeDocument(this)) {
 1395+ throw new DOMException("WRONG_DOCUMENT_ERR");
 1396+ }
 1397+
 1398+ var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
 1399+ endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
 1400+
 1401+ return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
 1402+ },
 1403+
 1404+ intersection: function(range) {
 1405+ if (this.intersectsRange(range)) {
 1406+ var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
 1407+ endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
 1408+
 1409+ var intersectionRange = this.cloneRange();
 1410+
 1411+ if (startComparison == -1) {
 1412+ intersectionRange.setStart(range.startContainer, range.startOffset);
 1413+ }
 1414+ if (endComparison == 1) {
 1415+ intersectionRange.setEnd(range.endContainer, range.endOffset);
 1416+ }
 1417+ return intersectionRange;
 1418+ }
 1419+ return null;
 1420+ },
 1421+
 1422+ union: function(range) {
 1423+ if (this.intersectsRange(range, true)) {
 1424+ var unionRange = this.cloneRange();
 1425+ if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
 1426+ unionRange.setStart(range.startContainer, range.startOffset);
 1427+ }
 1428+ if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
 1429+ unionRange.setEnd(range.endContainer, range.endOffset);
 1430+ }
 1431+ return unionRange;
 1432+ } else {
 1433+ throw new RangeException("Ranges do not intersect");
 1434+ }
 1435+ },
 1436+
 1437+ containsNode: function(node, allowPartial) {
 1438+ if (allowPartial) {
 1439+ return this.intersectsNode(node, false);
 1440+ } else {
 1441+ return this.compareNode(node) == n_i;
 1442+ }
 1443+ },
 1444+
 1445+ containsNodeContents: function(node) {
 1446+ return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
 1447+ },
 1448+
 1449+ containsRange: function(range) {
 1450+ return this.intersection(range).equals(range);
 1451+ },
 1452+
 1453+ containsNodeText: function(node) {
 1454+ var nodeRange = this.cloneRange();
 1455+ nodeRange.selectNode(node);
 1456+ var textNodes = nodeRange.getNodes([3]);
 1457+ if (textNodes.length > 0) {
 1458+ nodeRange.setStart(textNodes[0], 0);
 1459+ var lastTextNode = textNodes.pop();
 1460+ nodeRange.setEnd(lastTextNode, lastTextNode.length);
 1461+ var contains = this.containsRange(nodeRange);
 1462+ nodeRange.detach();
 1463+ return contains;
 1464+ } else {
 1465+ return this.containsNodeContents(node);
 1466+ }
 1467+ },
 1468+
 1469+ createNodeIterator: function(nodeTypes, filter) {
 1470+ assertRangeValid(this);
 1471+ return new RangeNodeIterator(this, nodeTypes, filter);
 1472+ },
 1473+
 1474+ getNodes: function(nodeTypes, filter) {
 1475+ assertRangeValid(this);
 1476+ return getNodesInRange(this, nodeTypes, filter);
 1477+ },
 1478+
 1479+ getDocument: function() {
 1480+ return getRangeDocument(this);
 1481+ },
 1482+
 1483+ collapseBefore: function(node) {
 1484+ assertNotDetached(this);
 1485+
 1486+ this.setEndBefore(node);
 1487+ this.collapse(false);
 1488+ },
 1489+
 1490+ collapseAfter: function(node) {
 1491+ assertNotDetached(this);
 1492+
 1493+ this.setStartAfter(node);
 1494+ this.collapse(true);
 1495+ },
 1496+
 1497+ getName: function() {
 1498+ return "DomRange";
 1499+ },
 1500+
 1501+ equals: function(range) {
 1502+ return Range.rangesEqual(this, range);
 1503+ },
 1504+
 1505+ inspect: function() {
 1506+ return inspect(this);
 1507+ }
 1508+ };
 1509+
 1510+ function copyComparisonConstantsToObject(obj) {
 1511+ obj.START_TO_START = s2s;
 1512+ obj.START_TO_END = s2e;
 1513+ obj.END_TO_END = e2e;
 1514+ obj.END_TO_START = e2s;
 1515+
 1516+ obj.NODE_BEFORE = n_b;
 1517+ obj.NODE_AFTER = n_a;
 1518+ obj.NODE_BEFORE_AND_AFTER = n_b_a;
 1519+ obj.NODE_INSIDE = n_i;
 1520+ }
 1521+
 1522+ function copyComparisonConstants(constructor) {
 1523+ copyComparisonConstantsToObject(constructor);
 1524+ copyComparisonConstantsToObject(constructor.prototype);
 1525+ }
 1526+
 1527+ function createRangeContentRemover(remover, boundaryUpdater) {
 1528+ return function() {
 1529+ assertRangeValid(this);
 1530+
 1531+ var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
 1532+
 1533+ var iterator = new RangeIterator(this, true);
 1534+
 1535+ // Work out where to position the range after content removal
 1536+ var node, boundary;
 1537+ if (sc !== root) {
 1538+ node = dom.getClosestAncestorIn(sc, root, true);
 1539+ boundary = getBoundaryAfterNode(node);
 1540+ sc = boundary.node;
 1541+ so = boundary.offset;
 1542+ }
 1543+
 1544+ // Check none of the range is read-only
 1545+ iterateSubtree(iterator, assertNodeNotReadOnly);
 1546+
 1547+ iterator.reset();
 1548+
 1549+ // Remove the content
 1550+ var returnValue = remover(iterator);
 1551+ iterator.detach();
 1552+
 1553+ // Move to the new position
 1554+ boundaryUpdater(this, sc, so, sc, so);
 1555+
 1556+ return returnValue;
 1557+ };
 1558+ }
 1559+
 1560+ function createPrototypeRange(constructor, boundaryUpdater, detacher) {
 1561+ function createBeforeAfterNodeSetter(isBefore, isStart) {
 1562+ return function(node) {
 1563+ assertNotDetached(this);
 1564+ assertValidNodeType(node, beforeAfterNodeTypes);
 1565+ assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
 1566+
 1567+ var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
 1568+ (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
 1569+ };
 1570+ }
 1571+
 1572+ function setRangeStart(range, node, offset) {
 1573+ var ec = range.endContainer, eo = range.endOffset;
 1574+ if (node !== range.startContainer || offset !== range.startOffset) {
 1575+ // Check the root containers of the range and the new boundary, and also check whether the new boundary
 1576+ // is after the current end. In either case, collapse the range to the new position
 1577+ if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
 1578+ ec = node;
 1579+ eo = offset;
 1580+ }
 1581+ boundaryUpdater(range, node, offset, ec, eo);
 1582+ }
 1583+ }
 1584+
 1585+ function setRangeEnd(range, node, offset) {
 1586+ var sc = range.startContainer, so = range.startOffset;
 1587+ if (node !== range.endContainer || offset !== range.endOffset) {
 1588+ // Check the root containers of the range and the new boundary, and also check whether the new boundary
 1589+ // is after the current end. In either case, collapse the range to the new position
 1590+ if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
 1591+ sc = node;
 1592+ so = offset;
 1593+ }
 1594+ boundaryUpdater(range, sc, so, node, offset);
 1595+ }
 1596+ }
 1597+
 1598+ function setRangeStartAndEnd(range, node, offset) {
 1599+ if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
 1600+ boundaryUpdater(range, node, offset, node, offset);
 1601+ }
 1602+ }
 1603+
 1604+ constructor.prototype = new RangePrototype();
 1605+
 1606+ api.util.extend(constructor.prototype, {
 1607+ setStart: function(node, offset) {
 1608+ assertNotDetached(this);
 1609+ assertNoDocTypeNotationEntityAncestor(node, true);
 1610+ assertValidOffset(node, offset);
 1611+
 1612+ setRangeStart(this, node, offset);
 1613+ },
 1614+
 1615+ setEnd: function(node, offset) {
 1616+ assertNotDetached(this);
 1617+ assertNoDocTypeNotationEntityAncestor(node, true);
 1618+ assertValidOffset(node, offset);
 1619+
 1620+ setRangeEnd(this, node, offset);
 1621+ },
 1622+
 1623+ setStartBefore: createBeforeAfterNodeSetter(true, true),
 1624+ setStartAfter: createBeforeAfterNodeSetter(false, true),
 1625+ setEndBefore: createBeforeAfterNodeSetter(true, false),
 1626+ setEndAfter: createBeforeAfterNodeSetter(false, false),
 1627+
 1628+ collapse: function(isStart) {
 1629+ assertRangeValid(this);
 1630+ if (isStart) {
 1631+ boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
 1632+ } else {
 1633+ boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
 1634+ }
 1635+ },
 1636+
 1637+ selectNodeContents: function(node) {
 1638+ // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
 1639+ // could be taken to mean only its children. However, browsers implement this the same as selectNode for
 1640+ // text nodes, so I shall do likewise
 1641+ assertNotDetached(this);
 1642+ assertNoDocTypeNotationEntityAncestor(node, true);
 1643+
 1644+ boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
 1645+ },
 1646+
 1647+ selectNode: function(node) {
 1648+ assertNotDetached(this);
 1649+ assertNoDocTypeNotationEntityAncestor(node, false);
 1650+ assertValidNodeType(node, beforeAfterNodeTypes);
 1651+
 1652+ var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
 1653+ boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
 1654+ },
 1655+
 1656+ extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
 1657+
 1658+ deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
 1659+
 1660+ canSurroundContents: function() {
 1661+ assertRangeValid(this);
 1662+ assertNodeNotReadOnly(this.startContainer);
 1663+ assertNodeNotReadOnly(this.endContainer);
 1664+
 1665+ // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
 1666+ // no non-text nodes.
 1667+ var iterator = new RangeIterator(this, true);
 1668+ var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
 1669+ (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
 1670+ iterator.detach();
 1671+ return !boundariesInvalid;
 1672+ },
 1673+
 1674+ detach: function() {
 1675+ detacher(this);
 1676+ },
 1677+
 1678+ splitBoundaries: function() {
 1679+ assertRangeValid(this);
 1680+
 1681+
 1682+ var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
 1683+ var startEndSame = (sc === ec);
 1684+
 1685+ if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
 1686+ dom.splitDataNode(ec, eo);
 1687+
 1688+ }
 1689+
 1690+ if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
 1691+
 1692+ sc = dom.splitDataNode(sc, so);
 1693+ if (startEndSame) {
 1694+ eo -= so;
 1695+ ec = sc;
 1696+ } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
 1697+ eo++;
 1698+ }
 1699+ so = 0;
 1700+
 1701+ }
 1702+ boundaryUpdater(this, sc, so, ec, eo);
 1703+ },
 1704+
 1705+ normalizeBoundaries: function() {
 1706+ assertRangeValid(this);
 1707+
 1708+ var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
 1709+
 1710+ var mergeForward = function(node) {
 1711+ var sibling = node.nextSibling;
 1712+ if (sibling && sibling.nodeType == node.nodeType) {
 1713+ ec = node;
 1714+ eo = node.length;
 1715+ node.appendData(sibling.data);
 1716+ sibling.parentNode.removeChild(sibling);
 1717+ }
 1718+ };
 1719+
 1720+ var mergeBackward = function(node) {
 1721+ var sibling = node.previousSibling;
 1722+ if (sibling && sibling.nodeType == node.nodeType) {
 1723+ sc = node;
 1724+ var nodeLength = node.length;
 1725+ so = sibling.length;
 1726+ node.insertData(0, sibling.data);
 1727+ sibling.parentNode.removeChild(sibling);
 1728+ if (sc == ec) {
 1729+ eo += so;
 1730+ ec = sc;
 1731+ } else if (ec == node.parentNode) {
 1732+ var nodeIndex = dom.getNodeIndex(node);
 1733+ if (eo == nodeIndex) {
 1734+ ec = node;
 1735+ eo = nodeLength;
 1736+ } else if (eo > nodeIndex) {
 1737+ eo--;
 1738+ }
 1739+ }
 1740+ }
 1741+ };
 1742+
 1743+ var normalizeStart = true;
 1744+
 1745+ if (dom.isCharacterDataNode(ec)) {
 1746+ if (ec.length == eo) {
 1747+ mergeForward(ec);
 1748+ }
 1749+ } else {
 1750+ if (eo > 0) {
 1751+ var endNode = ec.childNodes[eo - 1];
 1752+ if (endNode && dom.isCharacterDataNode(endNode)) {
 1753+ mergeForward(endNode);
 1754+ }
 1755+ }
 1756+ normalizeStart = !this.collapsed;
 1757+ }
 1758+
 1759+ if (normalizeStart) {
 1760+ if (dom.isCharacterDataNode(sc)) {
 1761+ if (so == 0) {
 1762+ mergeBackward(sc);
 1763+ }
 1764+ } else {
 1765+ if (so < sc.childNodes.length) {
 1766+ var startNode = sc.childNodes[so];
 1767+ if (startNode && dom.isCharacterDataNode(startNode)) {
 1768+ mergeBackward(startNode);
 1769+ }
 1770+ }
 1771+ }
 1772+ } else {
 1773+ sc = ec;
 1774+ so = eo;
 1775+ }
 1776+
 1777+ boundaryUpdater(this, sc, so, ec, eo);
 1778+ },
 1779+
 1780+ collapseToPoint: function(node, offset) {
 1781+ assertNotDetached(this);
 1782+
 1783+ assertNoDocTypeNotationEntityAncestor(node, true);
 1784+ assertValidOffset(node, offset);
 1785+
 1786+ setRangeStartAndEnd(this, node, offset);
 1787+ }
 1788+ });
 1789+
 1790+ copyComparisonConstants(constructor);
 1791+ }
 1792+
 1793+ /*----------------------------------------------------------------------------------------------------------------*/
 1794+
 1795+ // Updates commonAncestorContainer and collapsed after boundary change
 1796+ function updateCollapsedAndCommonAncestor(range) {
 1797+ range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
 1798+ range.commonAncestorContainer = range.collapsed ?
 1799+ range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
 1800+ }
 1801+
 1802+ function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
 1803+ var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
 1804+ var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
 1805+
 1806+ range.startContainer = startContainer;
 1807+ range.startOffset = startOffset;
 1808+ range.endContainer = endContainer;
 1809+ range.endOffset = endOffset;
 1810+
 1811+ updateCollapsedAndCommonAncestor(range);
 1812+ dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
 1813+ }
 1814+
 1815+ function detach(range) {
 1816+ assertNotDetached(range);
 1817+ range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
 1818+ range.collapsed = range.commonAncestorContainer = null;
 1819+ dispatchEvent(range, "detach", null);
 1820+ range._listeners = null;
 1821+ }
 1822+
 1823+ /**
 1824+ * @constructor
 1825+ */
 1826+ function Range(doc) {
 1827+ this.startContainer = doc;
 1828+ this.startOffset = 0;
 1829+ this.endContainer = doc;
 1830+ this.endOffset = 0;
 1831+ this._listeners = {
 1832+ boundarychange: [],
 1833+ detach: []
 1834+ };
 1835+ updateCollapsedAndCommonAncestor(this);
 1836+ }
 1837+
 1838+ createPrototypeRange(Range, updateBoundaries, detach);
 1839+
 1840+ api.rangePrototype = RangePrototype.prototype;
 1841+
 1842+ Range.rangeProperties = rangeProperties;
 1843+ Range.RangeIterator = RangeIterator;
 1844+ Range.copyComparisonConstants = copyComparisonConstants;
 1845+ Range.createPrototypeRange = createPrototypeRange;
 1846+ Range.inspect = inspect;
 1847+ Range.getRangeDocument = getRangeDocument;
 1848+ Range.rangesEqual = function(r1, r2) {
 1849+ return r1.startContainer === r2.startContainer &&
 1850+ r1.startOffset === r2.startOffset &&
 1851+ r1.endContainer === r2.endContainer &&
 1852+ r1.endOffset === r2.endOffset;
 1853+ };
 1854+
 1855+ api.DomRange = Range;
 1856+ api.RangeException = RangeException;
 1857+});rangy.createModule("WrappedRange", function(api, module) {
 1858+ api.requireModules( ["DomUtil", "DomRange"] );
 1859+
 1860+ /**
 1861+ * @constructor
 1862+ */
 1863+ var WrappedRange;
 1864+ var dom = api.dom;
 1865+ var DomPosition = dom.DomPosition;
 1866+ var DomRange = api.DomRange;
 1867+
 1868+
 1869+
 1870+ /*----------------------------------------------------------------------------------------------------------------*/
 1871+
 1872+ /*
 1873+ This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
 1874+ method. For example, in the following (where pipes denote the selection boundaries):
 1875+
 1876+ <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
 1877+
 1878+ var range = document.selection.createRange();
 1879+ alert(range.parentElement().id); // Should alert "ul" but alerts "b"
 1880+
 1881+ This method returns the common ancestor node of the following:
 1882+ - the parentElement() of the textRange
 1883+ - the parentElement() of the textRange after calling collapse(true)
 1884+ - the parentElement() of the textRange after calling collapse(false)
 1885+ */
 1886+ function getTextRangeContainerElement(textRange) {
 1887+ var parentEl = textRange.parentElement();
 1888+
 1889+ var range = textRange.duplicate();
 1890+ range.collapse(true);
 1891+ var startEl = range.parentElement();
 1892+ range = textRange.duplicate();
 1893+ range.collapse(false);
 1894+ var endEl = range.parentElement();
 1895+ var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
 1896+
 1897+ return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
 1898+ }
 1899+
 1900+ function textRangeIsCollapsed(textRange) {
 1901+ return textRange.compareEndPoints("StartToEnd", textRange) == 0;
 1902+ }
 1903+
 1904+ // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
 1905+ // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
 1906+ // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
 1907+ // for inputs and images, plus optimizations.
 1908+ function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
 1909+ var workingRange = textRange.duplicate();
 1910+
 1911+ workingRange.collapse(isStart);
 1912+ var containerElement = workingRange.parentElement();
 1913+
 1914+ // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
 1915+ // check for that
 1916+ // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
 1917+ if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
 1918+ containerElement = wholeRangeContainerElement;
 1919+
 1920+ }
 1921+
 1922+
 1923+
 1924+ // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
 1925+ // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
 1926+ if (!containerElement.canHaveHTML) {
 1927+ return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
 1928+ }
 1929+
 1930+ var workingNode = dom.getDocument(containerElement).createElement("span");
 1931+ var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
 1932+ var previousNode, nextNode, boundaryPosition, boundaryNode;
 1933+
 1934+ // Move the working range through the container's children, starting at the end and working backwards, until the
 1935+ // working range reaches or goes past the boundary we're interested in
 1936+ do {
 1937+ containerElement.insertBefore(workingNode, workingNode.previousSibling);
 1938+ workingRange.moveToElementText(workingNode);
 1939+ } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
 1940+ workingNode.previousSibling);
 1941+
 1942+ // We've now reached or gone past the boundary of the text range we're interested in
 1943+ // so have identified the node we want
 1944+ boundaryNode = workingNode.nextSibling;
 1945+
 1946+ if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
 1947+ // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
 1948+ // node containing the text range's boundary, so we move the end of the working range to the boundary point
 1949+ // and measure the length of its text to get the boundary's offset within the node.
 1950+ workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
 1951+
 1952+
 1953+ var offset;
 1954+
 1955+ if (/[\r\n]/.test(boundaryNode.data)) {
 1956+ /*
 1957+ For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
 1958+ for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
 1959+
 1960+ - Each line break is represented as \r in the text node's data/nodeValue properties
 1961+ - Each line break is represented as \r\n in the TextRange's 'text' property
 1962+ - The 'text' property of the TextRange does not contain trailing line breaks
 1963+
 1964+ To get round the problem presented by the final fact above, we can use the fact that TextRange's
 1965+ moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
 1966+ the same as the number of characters it was instructed to move. The simplest approach is to use this to
 1967+ store the characters moved when moving both the start and end of the range to the start of the document
 1968+ body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
 1969+ However, this is extremely slow when the document is large and the range is near the end of it. Clearly
 1970+ doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
 1971+ problem.
 1972+
 1973+ Another approach that works is to use moveStart() to move the start boundary of the range up to the end
 1974+ boundary one character at a time and incrementing a counter with the value returned by the moveStart()
 1975+ call. However, the check for whether the start boundary has reached the end boundary is expensive, so
 1976+ this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
 1977+ the range within the document).
 1978+
 1979+ The method below is a hybrid of the two methods above. It uses the fact that a string containing the
 1980+ TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
 1981+ text of the TextRange, so the start of the range is moved that length initially and then a character at
 1982+ a time to make up for any trailing line breaks not contained in the 'text' property. This has good
 1983+ performance in most situations compared to the previous two methods.
 1984+ */
 1985+ var tempRange = workingRange.duplicate();
 1986+ var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
 1987+
 1988+ offset = tempRange.moveStart("character", rangeLength);
 1989+ while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
 1990+ offset++;
 1991+ tempRange.moveStart("character", 1);
 1992+ }
 1993+ } else {
 1994+ offset = workingRange.text.length;
 1995+ }
 1996+ boundaryPosition = new DomPosition(boundaryNode, offset);
 1997+ } else {
 1998+
 1999+
 2000+ // If the boundary immediately follows a character data node and this is the end boundary, we should favour
 2001+ // a position within that, and likewise for a start boundary preceding a character data node
 2002+ previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
 2003+ nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
 2004+
 2005+
 2006+
 2007+ if (nextNode && dom.isCharacterDataNode(nextNode)) {
 2008+ boundaryPosition = new DomPosition(nextNode, 0);
 2009+ } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
 2010+ boundaryPosition = new DomPosition(previousNode, previousNode.length);
 2011+ } else {
 2012+ boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
 2013+ }
 2014+ }
 2015+
 2016+ // Clean up
 2017+ workingNode.parentNode.removeChild(workingNode);
 2018+
 2019+ return boundaryPosition;
 2020+ }
 2021+
 2022+ // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
 2023+ // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
 2024+ // (http://code.google.com/p/ierange/)
 2025+ function createBoundaryTextRange(boundaryPosition, isStart) {
 2026+ var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
 2027+ var doc = dom.getDocument(boundaryPosition.node);
 2028+ var workingNode, childNodes, workingRange = doc.body.createTextRange();
 2029+ var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
 2030+
 2031+ if (nodeIsDataNode) {
 2032+ boundaryNode = boundaryPosition.node;
 2033+ boundaryParent = boundaryNode.parentNode;
 2034+ } else {
 2035+ childNodes = boundaryPosition.node.childNodes;
 2036+ boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
 2037+ boundaryParent = boundaryPosition.node;
 2038+ }
 2039+
 2040+ // Position the range immediately before the node containing the boundary
 2041+ workingNode = doc.createElement("span");
 2042+
 2043+ // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
 2044+ // element rather than immediately before or after it, which is what we want
 2045+ workingNode.innerHTML = "&#feff;";
 2046+
 2047+ // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
 2048+ // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
 2049+ if (boundaryNode) {
 2050+ boundaryParent.insertBefore(workingNode, boundaryNode);
 2051+ } else {
 2052+ boundaryParent.appendChild(workingNode);
 2053+ }
 2054+
 2055+ workingRange.moveToElementText(workingNode);
 2056+ workingRange.collapse(!isStart);
 2057+
 2058+ // Clean up
 2059+ boundaryParent.removeChild(workingNode);
 2060+
 2061+ // Move the working range to the text offset, if required
 2062+ if (nodeIsDataNode) {
 2063+ workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
 2064+ }
 2065+
 2066+ return workingRange;
 2067+ }
 2068+
 2069+ /*----------------------------------------------------------------------------------------------------------------*/
 2070+
 2071+ if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
 2072+ // This is a wrapper around the browser's native DOM Range. It has two aims:
 2073+ // - Provide workarounds for specific browser bugs
 2074+ // - provide convenient extensions, which are inherited from Rangy's DomRange
 2075+
 2076+ (function() {
 2077+ var rangeProto;
 2078+ var rangeProperties = DomRange.rangeProperties;
 2079+ var canSetRangeStartAfterEnd;
 2080+
 2081+ function updateRangeProperties(range) {
 2082+ var i = rangeProperties.length, prop;
 2083+ while (i--) {
 2084+ prop = rangeProperties[i];
 2085+ range[prop] = range.nativeRange[prop];
 2086+ }
 2087+ }
 2088+
 2089+ function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
 2090+ var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
 2091+ var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
 2092+
 2093+ // Always set both boundaries for the benefit of IE9 (see issue 35)
 2094+ if (startMoved || endMoved) {
 2095+ range.setEnd(endContainer, endOffset);
 2096+ range.setStart(startContainer, startOffset);
 2097+ }
 2098+ }
 2099+
 2100+ function detach(range) {
 2101+ range.nativeRange.detach();
 2102+ range.detached = true;
 2103+ var i = rangeProperties.length, prop;
 2104+ while (i--) {
 2105+ prop = rangeProperties[i];
 2106+ range[prop] = null;
 2107+ }
 2108+ }
 2109+
 2110+ var createBeforeAfterNodeSetter;
 2111+
 2112+ WrappedRange = function(range) {
 2113+ if (!range) {
 2114+ throw new Error("Range must be specified");
 2115+ }
 2116+ this.nativeRange = range;
 2117+ updateRangeProperties(this);
 2118+ };
 2119+
 2120+ DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
 2121+
 2122+ rangeProto = WrappedRange.prototype;
 2123+
 2124+ rangeProto.selectNode = function(node) {
 2125+ this.nativeRange.selectNode(node);
 2126+ updateRangeProperties(this);
 2127+ };
 2128+
 2129+ rangeProto.deleteContents = function() {
 2130+ this.nativeRange.deleteContents();
 2131+ updateRangeProperties(this);
 2132+ };
 2133+
 2134+ rangeProto.extractContents = function() {
 2135+ var frag = this.nativeRange.extractContents();
 2136+ updateRangeProperties(this);
 2137+ return frag;
 2138+ };
 2139+
 2140+ rangeProto.cloneContents = function() {
 2141+ return this.nativeRange.cloneContents();
 2142+ };
 2143+
 2144+ // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
 2145+ // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
 2146+ // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
 2147+ // insertNode, which works but is almost certainly slower than the native implementation.
 2148+/*
 2149+ rangeProto.insertNode = function(node) {
 2150+ this.nativeRange.insertNode(node);
 2151+ updateRangeProperties(this);
 2152+ };
 2153+*/
 2154+
 2155+ rangeProto.surroundContents = function(node) {
 2156+ this.nativeRange.surroundContents(node);
 2157+ updateRangeProperties(this);
 2158+ };
 2159+
 2160+ rangeProto.collapse = function(isStart) {
 2161+ this.nativeRange.collapse(isStart);
 2162+ updateRangeProperties(this);
 2163+ };
 2164+
 2165+ rangeProto.cloneRange = function() {
 2166+ return new WrappedRange(this.nativeRange.cloneRange());
 2167+ };
 2168+
 2169+ rangeProto.refresh = function() {
 2170+ updateRangeProperties(this);
 2171+ };
 2172+
 2173+ rangeProto.toString = function() {
 2174+ return this.nativeRange.toString();
 2175+ };
 2176+
 2177+ // Create test range and node for feature detection
 2178+
 2179+ var testTextNode = document.createTextNode("test");
 2180+ dom.getBody(document).appendChild(testTextNode);
 2181+ var range = document.createRange();
 2182+
 2183+ /*--------------------------------------------------------------------------------------------------------*/
 2184+
 2185+ // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
 2186+ // correct for it
 2187+
 2188+ range.setStart(testTextNode, 0);
 2189+ range.setEnd(testTextNode, 0);
 2190+
 2191+ try {
 2192+ range.setStart(testTextNode, 1);
 2193+ canSetRangeStartAfterEnd = true;
 2194+
 2195+ rangeProto.setStart = function(node, offset) {
 2196+ this.nativeRange.setStart(node, offset);
 2197+ updateRangeProperties(this);
 2198+ };
 2199+
 2200+ rangeProto.setEnd = function(node, offset) {
 2201+ this.nativeRange.setEnd(node, offset);
 2202+ updateRangeProperties(this);
 2203+ };
 2204+
 2205+ createBeforeAfterNodeSetter = function(name) {
 2206+ return function(node) {
 2207+ this.nativeRange[name](node);
 2208+ updateRangeProperties(this);
 2209+ };
 2210+ };
 2211+
 2212+ } catch(ex) {
 2213+
 2214+
 2215+ canSetRangeStartAfterEnd = false;
 2216+
 2217+ rangeProto.setStart = function(node, offset) {
 2218+ try {
 2219+ this.nativeRange.setStart(node, offset);
 2220+ } catch (ex) {
 2221+ this.nativeRange.setEnd(node, offset);
 2222+ this.nativeRange.setStart(node, offset);
 2223+ }
 2224+ updateRangeProperties(this);
 2225+ };
 2226+
 2227+ rangeProto.setEnd = function(node, offset) {
 2228+ try {
 2229+ this.nativeRange.setEnd(node, offset);
 2230+ } catch (ex) {
 2231+ this.nativeRange.setStart(node, offset);
 2232+ this.nativeRange.setEnd(node, offset);
 2233+ }
 2234+ updateRangeProperties(this);
 2235+ };
 2236+
 2237+ createBeforeAfterNodeSetter = function(name, oppositeName) {
 2238+ return function(node) {
 2239+ try {
 2240+ this.nativeRange[name](node);
 2241+ } catch (ex) {
 2242+ this.nativeRange[oppositeName](node);
 2243+ this.nativeRange[name](node);
 2244+ }
 2245+ updateRangeProperties(this);
 2246+ };
 2247+ };
 2248+ }
 2249+
 2250+ rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
 2251+ rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
 2252+ rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
 2253+ rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
 2254+
 2255+ /*--------------------------------------------------------------------------------------------------------*/
 2256+
 2257+ // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
 2258+ // the 0th character of the text node
 2259+ range.selectNodeContents(testTextNode);
 2260+ if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
 2261+ range.startOffset == 0 && range.endOffset == testTextNode.length) {
 2262+ rangeProto.selectNodeContents = function(node) {
 2263+ this.nativeRange.selectNodeContents(node);
 2264+ updateRangeProperties(this);
 2265+ };
 2266+ } else {
 2267+ rangeProto.selectNodeContents = function(node) {
 2268+ this.setStart(node, 0);
 2269+ this.setEnd(node, DomRange.getEndOffset(node));
 2270+ };
 2271+ }
 2272+
 2273+ /*--------------------------------------------------------------------------------------------------------*/
 2274+
 2275+ // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
 2276+ // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
 2277+
 2278+ range.selectNodeContents(testTextNode);
 2279+ range.setEnd(testTextNode, 3);
 2280+
 2281+ var range2 = document.createRange();
 2282+ range2.selectNodeContents(testTextNode);
 2283+ range2.setEnd(testTextNode, 4);
 2284+ range2.setStart(testTextNode, 2);
 2285+
 2286+ if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
 2287+ range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
 2288+ // This is the wrong way round, so correct for it
 2289+
 2290+
 2291+ rangeProto.compareBoundaryPoints = function(type, range) {
 2292+ range = range.nativeRange || range;
 2293+ if (type == range.START_TO_END) {
 2294+ type = range.END_TO_START;
 2295+ } else if (type == range.END_TO_START) {
 2296+ type = range.START_TO_END;
 2297+ }
 2298+ return this.nativeRange.compareBoundaryPoints(type, range);
 2299+ };
 2300+ } else {
 2301+ rangeProto.compareBoundaryPoints = function(type, range) {
 2302+ return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
 2303+ };
 2304+ }
 2305+
 2306+ /*--------------------------------------------------------------------------------------------------------*/
 2307+
 2308+ // Test for existence of createContextualFragment and delegate to it if it exists
 2309+ if (api.util.isHostMethod(range, "createContextualFragment")) {
 2310+ rangeProto.createContextualFragment = function(fragmentStr) {
 2311+ return this.nativeRange.createContextualFragment(fragmentStr);
 2312+ };
 2313+ }
 2314+
 2315+ /*--------------------------------------------------------------------------------------------------------*/
 2316+
 2317+ // Clean up
 2318+ dom.getBody(document).removeChild(testTextNode);
 2319+ range.detach();
 2320+ range2.detach();
 2321+ })();
 2322+
 2323+ api.createNativeRange = function(doc) {
 2324+ doc = doc || document;
 2325+ return doc.createRange();
 2326+ };
 2327+ } else if (api.features.implementsTextRange) {
 2328+ // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
 2329+ // prototype
 2330+
 2331+ WrappedRange = function(textRange) {
 2332+ this.textRange = textRange;
 2333+ this.refresh();
 2334+ };
 2335+
 2336+ WrappedRange.prototype = new DomRange(document);
 2337+
 2338+ WrappedRange.prototype.refresh = function() {
 2339+ var start, end;
 2340+
 2341+ // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
 2342+ var rangeContainerElement = getTextRangeContainerElement(this.textRange);
 2343+
 2344+ if (textRangeIsCollapsed(this.textRange)) {
 2345+ end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
 2346+ } else {
 2347+
 2348+ start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
 2349+ end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
 2350+ }
 2351+
 2352+ this.setStart(start.node, start.offset);
 2353+ this.setEnd(end.node, end.offset);
 2354+ };
 2355+
 2356+ DomRange.copyComparisonConstants(WrappedRange);
 2357+
 2358+ // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
 2359+ var globalObj = (function() { return this; })();
 2360+ if (typeof globalObj.Range == "undefined") {
 2361+ globalObj.Range = WrappedRange;
 2362+ }
 2363+
 2364+ api.createNativeRange = function(doc) {
 2365+ doc = doc || document;
 2366+ return doc.body.createTextRange();
 2367+ };
 2368+ }
 2369+
 2370+ if (api.features.implementsTextRange) {
 2371+ WrappedRange.rangeToTextRange = function(range) {
 2372+ if (range.collapsed) {
 2373+ var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 2374+
 2375+
 2376+
 2377+ return tr;
 2378+
 2379+ //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 2380+ } else {
 2381+ var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
 2382+ var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
 2383+ var textRange = dom.getDocument(range.startContainer).body.createTextRange();
 2384+ textRange.setEndPoint("StartToStart", startRange);
 2385+ textRange.setEndPoint("EndToEnd", endRange);
 2386+ return textRange;
 2387+ }
 2388+ };
 2389+ }
 2390+
 2391+ WrappedRange.prototype.getName = function() {
 2392+ return "WrappedRange";
 2393+ };
 2394+
 2395+ api.WrappedRange = WrappedRange;
 2396+
 2397+ api.createRange = function(doc) {
 2398+ doc = doc || document;
 2399+ return new WrappedRange(api.createNativeRange(doc));
 2400+ };
 2401+
 2402+ api.createRangyRange = function(doc) {
 2403+ doc = doc || document;
 2404+ return new DomRange(doc);
 2405+ };
 2406+
 2407+ api.createIframeRange = function(iframeEl) {
 2408+ return api.createRange(dom.getIframeDocument(iframeEl));
 2409+ };
 2410+
 2411+ api.createIframeRangyRange = function(iframeEl) {
 2412+ return api.createRangyRange(dom.getIframeDocument(iframeEl));
 2413+ };
 2414+
 2415+ api.addCreateMissingNativeApiListener(function(win) {
 2416+ var doc = win.document;
 2417+ if (typeof doc.createRange == "undefined") {
 2418+ doc.createRange = function() {
 2419+ return api.createRange(this);
 2420+ };
 2421+ }
 2422+ doc = win = null;
 2423+ });
 2424+});rangy.createModule("WrappedSelection", function(api, module) {
 2425+ // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
 2426+ // spec (http://html5.org/specs/dom-range.html)
 2427+
 2428+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
 2429+
 2430+ api.config.checkSelectionRanges = true;
 2431+
 2432+ var BOOLEAN = "boolean",
 2433+ windowPropertyName = "_rangySelection",
 2434+ dom = api.dom,
 2435+ util = api.util,
 2436+ DomRange = api.DomRange,
 2437+ WrappedRange = api.WrappedRange,
 2438+ DOMException = api.DOMException,
 2439+ DomPosition = dom.DomPosition,
 2440+ getSelection,
 2441+ selectionIsCollapsed,
 2442+ CONTROL = "Control";
 2443+
 2444+
 2445+
 2446+ function getWinSelection(winParam) {
 2447+ return (winParam || window).getSelection();
 2448+ }
 2449+
 2450+ function getDocSelection(winParam) {
 2451+ return (winParam || window).document.selection;
 2452+ }
 2453+
 2454+ // Test for the Range/TextRange and Selection features required
 2455+ // Test for ability to retrieve selection
 2456+ var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
 2457+ implementsDocSelection = api.util.isHostObject(document, "selection");
 2458+
 2459+ var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
 2460+
 2461+ if (useDocumentSelection) {
 2462+ getSelection = getDocSelection;
 2463+ api.isSelectionValid = function(winParam) {
 2464+ var doc = (winParam || window).document, nativeSel = doc.selection;
 2465+
 2466+ // Check whether the selection TextRange is actually contained within the correct document
 2467+ return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
 2468+ };
 2469+ } else if (implementsWinGetSelection) {
 2470+ getSelection = getWinSelection;
 2471+ api.isSelectionValid = function() {
 2472+ return true;
 2473+ };
 2474+ } else {
 2475+ module.fail("Neither document.selection or window.getSelection() detected.");
 2476+ }
 2477+
 2478+ api.getNativeSelection = getSelection;
 2479+
 2480+ var testSelection = getSelection();
 2481+ var testRange = api.createNativeRange(document);
 2482+ var body = dom.getBody(document);
 2483+
 2484+ // Obtaining a range from a selection
 2485+ var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
 2486+ util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
 2487+ api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
 2488+
 2489+ // Test for existence of native selection extend() method
 2490+ var selectionHasExtend = util.isHostMethod(testSelection, "extend");
 2491+ api.features.selectionHasExtend = selectionHasExtend;
 2492+
 2493+ // Test if rangeCount exists
 2494+ var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
 2495+ api.features.selectionHasRangeCount = selectionHasRangeCount;
 2496+
 2497+ var selectionSupportsMultipleRanges = false;
 2498+ var collapsedNonEditableSelectionsSupported = true;
 2499+
 2500+ if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
 2501+ typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
 2502+
 2503+ (function() {
 2504+ var iframe = document.createElement("iframe");
 2505+ body.appendChild(iframe);
 2506+
 2507+ var iframeDoc = dom.getIframeDocument(iframe);
 2508+ iframeDoc.open();
 2509+ iframeDoc.write("<html><head></head><body>12</body></html>");
 2510+ iframeDoc.close();
 2511+
 2512+ var sel = dom.getIframeWindow(iframe).getSelection();
 2513+ var docEl = iframeDoc.documentElement;
 2514+ var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
 2515+
 2516+ // Test whether the native selection will allow a collapsed selection within a non-editable element
 2517+ var r1 = iframeDoc.createRange();
 2518+ r1.setStart(textNode, 1);
 2519+ r1.collapse(true);
 2520+ sel.addRange(r1);
 2521+ collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
 2522+ sel.removeAllRanges();
 2523+
 2524+ // Test whether the native selection is capable of supporting multiple ranges
 2525+ var r2 = r1.cloneRange();
 2526+ r1.setStart(textNode, 0);
 2527+ r2.setEnd(textNode, 2);
 2528+ sel.addRange(r1);
 2529+ sel.addRange(r2);
 2530+
 2531+ selectionSupportsMultipleRanges = (sel.rangeCount == 2);
 2532+
 2533+ // Clean up
 2534+ r1.detach();
 2535+ r2.detach();
 2536+
 2537+ body.removeChild(iframe);
 2538+ })();
 2539+ }
 2540+
 2541+ api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
 2542+ api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
 2543+
 2544+ // ControlRanges
 2545+ var implementsControlRange = false, testControlRange;
 2546+
 2547+ if (body && util.isHostMethod(body, "createControlRange")) {
 2548+ testControlRange = body.createControlRange();
 2549+ if (util.areHostProperties(testControlRange, ["item", "add"])) {
 2550+ implementsControlRange = true;
 2551+ }
 2552+ }
 2553+ api.features.implementsControlRange = implementsControlRange;
 2554+
 2555+ // Selection collapsedness
 2556+ if (selectionHasAnchorAndFocus) {
 2557+ selectionIsCollapsed = function(sel) {
 2558+ return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
 2559+ };
 2560+ } else {
 2561+ selectionIsCollapsed = function(sel) {
 2562+ return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
 2563+ };
 2564+ }
 2565+
 2566+ function updateAnchorAndFocusFromRange(sel, range, backwards) {
 2567+ var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
 2568+ sel.anchorNode = range[anchorPrefix + "Container"];
 2569+ sel.anchorOffset = range[anchorPrefix + "Offset"];
 2570+ sel.focusNode = range[focusPrefix + "Container"];
 2571+ sel.focusOffset = range[focusPrefix + "Offset"];
 2572+ }
 2573+
 2574+ function updateAnchorAndFocusFromNativeSelection(sel) {
 2575+ var nativeSel = sel.nativeSelection;
 2576+ sel.anchorNode = nativeSel.anchorNode;
 2577+ sel.anchorOffset = nativeSel.anchorOffset;
 2578+ sel.focusNode = nativeSel.focusNode;
 2579+ sel.focusOffset = nativeSel.focusOffset;
 2580+ }
 2581+
 2582+ function updateEmptySelection(sel) {
 2583+ sel.anchorNode = sel.focusNode = null;
 2584+ sel.anchorOffset = sel.focusOffset = 0;
 2585+ sel.rangeCount = 0;
 2586+ sel.isCollapsed = true;
 2587+ sel._ranges.length = 0;
 2588+ }
 2589+
 2590+ function getNativeRange(range) {
 2591+ var nativeRange;
 2592+ if (range instanceof DomRange) {
 2593+ nativeRange = range._selectionNativeRange;
 2594+ if (!nativeRange) {
 2595+ nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
 2596+ nativeRange.setEnd(range.endContainer, range.endOffset);
 2597+ nativeRange.setStart(range.startContainer, range.startOffset);
 2598+ range._selectionNativeRange = nativeRange;
 2599+ range.attachListener("detach", function() {
 2600+
 2601+ this._selectionNativeRange = null;
 2602+ });
 2603+ }
 2604+ } else if (range instanceof WrappedRange) {
 2605+ nativeRange = range.nativeRange;
 2606+ } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
 2607+ nativeRange = range;
 2608+ }
 2609+ return nativeRange;
 2610+ }
 2611+
 2612+ function rangeContainsSingleElement(rangeNodes) {
 2613+ if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
 2614+ return false;
 2615+ }
 2616+ for (var i = 1, len = rangeNodes.length; i < len; ++i) {
 2617+ if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
 2618+ return false;
 2619+ }
 2620+ }
 2621+ return true;
 2622+ }
 2623+
 2624+ function getSingleElementFromRange(range) {
 2625+ var nodes = range.getNodes();
 2626+ if (!rangeContainsSingleElement(nodes)) {
 2627+ throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
 2628+ }
 2629+ return nodes[0];
 2630+ }
 2631+
 2632+ function isTextRange(range) {
 2633+ return !!range && typeof range.text != "undefined";
 2634+ }
 2635+
 2636+ function updateFromTextRange(sel, range) {
 2637+ // Create a Range from the selected TextRange
 2638+ var wrappedRange = new WrappedRange(range);
 2639+ sel._ranges = [wrappedRange];
 2640+
 2641+ updateAnchorAndFocusFromRange(sel, wrappedRange, false);
 2642+ sel.rangeCount = 1;
 2643+ sel.isCollapsed = wrappedRange.collapsed;
 2644+ }
 2645+
 2646+ function updateControlSelection(sel) {
 2647+ // Update the wrapped selection based on what's now in the native selection
 2648+ sel._ranges.length = 0;
 2649+ if (sel.docSelection.type == "None") {
 2650+ updateEmptySelection(sel);
 2651+ } else {
 2652+ var controlRange = sel.docSelection.createRange();
 2653+ if (isTextRange(controlRange)) {
 2654+ // This case (where the selection type is "Control" and calling createRange() on the selection returns
 2655+ // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
 2656+ // ControlRange have been removed from the ControlRange and removed from the document.
 2657+ updateFromTextRange(sel, controlRange);
 2658+ } else {
 2659+ sel.rangeCount = controlRange.length;
 2660+ var range, doc = dom.getDocument(controlRange.item(0));
 2661+ for (var i = 0; i < sel.rangeCount; ++i) {
 2662+ range = api.createRange(doc);
 2663+ range.selectNode(controlRange.item(i));
 2664+ sel._ranges.push(range);
 2665+ }
 2666+ sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
 2667+ updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
 2668+ }
 2669+ }
 2670+ }
 2671+
 2672+ function addRangeToControlSelection(sel, range) {
 2673+ var controlRange = sel.docSelection.createRange();
 2674+ var rangeElement = getSingleElementFromRange(range);
 2675+
 2676+ // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
 2677+ // contained by the supplied range
 2678+ var doc = dom.getDocument(controlRange.item(0));
 2679+ var newControlRange = dom.getBody(doc).createControlRange();
 2680+ for (var i = 0, len = controlRange.length; i < len; ++i) {
 2681+ newControlRange.add(controlRange.item(i));
 2682+ }
 2683+ try {
 2684+ newControlRange.add(rangeElement);
 2685+ } catch (ex) {
 2686+ throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
 2687+ }
 2688+ newControlRange.select();
 2689+
 2690+ // Update the wrapped selection based on what's now in the native selection
 2691+ updateControlSelection(sel);
 2692+ }
 2693+
 2694+ var getSelectionRangeAt;
 2695+
 2696+ if (util.isHostMethod(testSelection, "getRangeAt")) {
 2697+ getSelectionRangeAt = function(sel, index) {
 2698+ try {
 2699+ return sel.getRangeAt(index);
 2700+ } catch(ex) {
 2701+ return null;
 2702+ }
 2703+ };
 2704+ } else if (selectionHasAnchorAndFocus) {
 2705+ getSelectionRangeAt = function(sel) {
 2706+ var doc = dom.getDocument(sel.anchorNode);
 2707+ var range = api.createRange(doc);
 2708+ range.setStart(sel.anchorNode, sel.anchorOffset);
 2709+ range.setEnd(sel.focusNode, sel.focusOffset);
 2710+
 2711+ // Handle the case when the selection was selected backwards (from the end to the start in the
 2712+ // document)
 2713+ if (range.collapsed !== this.isCollapsed) {
 2714+ range.setStart(sel.focusNode, sel.focusOffset);
 2715+ range.setEnd(sel.anchorNode, sel.anchorOffset);
 2716+ }
 2717+
 2718+ return range;
 2719+ };
 2720+ }
 2721+
 2722+ /**
 2723+ * @constructor
 2724+ */
 2725+ function WrappedSelection(selection, docSelection, win) {
 2726+ this.nativeSelection = selection;
 2727+ this.docSelection = docSelection;
 2728+ this._ranges = [];
 2729+ this.win = win;
 2730+ this.refresh();
 2731+ }
 2732+
 2733+ api.getSelection = function(win) {
 2734+ win = win || window;
 2735+ var sel = win[windowPropertyName];
 2736+ var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
 2737+ if (sel) {
 2738+ sel.nativeSelection = nativeSel;
 2739+ sel.docSelection = docSel;
 2740+ sel.refresh(win);
 2741+ } else {
 2742+ sel = new WrappedSelection(nativeSel, docSel, win);
 2743+ win[windowPropertyName] = sel;
 2744+ }
 2745+ return sel;
 2746+ };
 2747+
 2748+ api.getIframeSelection = function(iframeEl) {
 2749+ return api.getSelection(dom.getIframeWindow(iframeEl));
 2750+ };
 2751+
 2752+ var selProto = WrappedSelection.prototype;
 2753+
 2754+ function createControlSelection(sel, ranges) {
 2755+ // Ensure that the selection becomes of type "Control"
 2756+ var doc = dom.getDocument(ranges[0].startContainer);
 2757+ var controlRange = dom.getBody(doc).createControlRange();
 2758+ for (var i = 0, el; i < rangeCount; ++i) {
 2759+ el = getSingleElementFromRange(ranges[i]);
 2760+ try {
 2761+ controlRange.add(el);
 2762+ } catch (ex) {
 2763+ throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
 2764+ }
 2765+ }
 2766+ controlRange.select();
 2767+
 2768+ // Update the wrapped selection based on what's now in the native selection
 2769+ updateControlSelection(sel);
 2770+ }
 2771+
 2772+ // Selecting a range
 2773+ if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
 2774+ selProto.removeAllRanges = function() {
 2775+ this.nativeSelection.removeAllRanges();
 2776+ updateEmptySelection(this);
 2777+ };
 2778+
 2779+ var addRangeBackwards = function(sel, range) {
 2780+ var doc = DomRange.getRangeDocument(range);
 2781+ var endRange = api.createRange(doc);
 2782+ endRange.collapseToPoint(range.endContainer, range.endOffset);
 2783+ sel.nativeSelection.addRange(getNativeRange(endRange));
 2784+ sel.nativeSelection.extend(range.startContainer, range.startOffset);
 2785+ sel.refresh();
 2786+ };
 2787+
 2788+ if (selectionHasRangeCount) {
 2789+ selProto.addRange = function(range, backwards) {
 2790+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
 2791+ addRangeToControlSelection(this, range);
 2792+ } else {
 2793+ if (backwards && selectionHasExtend) {
 2794+ addRangeBackwards(this, range);
 2795+ } else {
 2796+ var previousRangeCount;
 2797+ if (selectionSupportsMultipleRanges) {
 2798+ previousRangeCount = this.rangeCount;
 2799+ } else {
 2800+ this.removeAllRanges();
 2801+ previousRangeCount = 0;
 2802+ }
 2803+ this.nativeSelection.addRange(getNativeRange(range));
 2804+
 2805+ // Check whether adding the range was successful
 2806+ this.rangeCount = this.nativeSelection.rangeCount;
 2807+
 2808+ if (this.rangeCount == previousRangeCount + 1) {
 2809+ // The range was added successfully
 2810+
 2811+ // Check whether the range that we added to the selection is reflected in the last range extracted from
 2812+ // the selection
 2813+ if (api.config.checkSelectionRanges) {
 2814+ var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
 2815+ if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
 2816+ // Happens in WebKit with, for example, a selection placed at the start of a text node
 2817+ range = new WrappedRange(nativeRange);
 2818+ }
 2819+ }
 2820+ this._ranges[this.rangeCount - 1] = range;
 2821+ updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
 2822+ this.isCollapsed = selectionIsCollapsed(this);
 2823+ } else {
 2824+ // The range was not added successfully. The simplest thing is to refresh
 2825+ this.refresh();
 2826+ }
 2827+ }
 2828+ }
 2829+ };
 2830+ } else {
 2831+ selProto.addRange = function(range, backwards) {
 2832+ if (backwards && selectionHasExtend) {
 2833+ addRangeBackwards(this, range);
 2834+ } else {
 2835+ this.nativeSelection.addRange(getNativeRange(range));
 2836+ this.refresh();
 2837+ }
 2838+ };
 2839+ }
 2840+
 2841+ selProto.setRanges = function(ranges) {
 2842+ if (implementsControlRange && ranges.length > 1) {
 2843+ createControlSelection(this, ranges);
 2844+ } else {
 2845+ this.removeAllRanges();
 2846+ for (var i = 0, len = ranges.length; i < len; ++i) {
 2847+ this.addRange(ranges[i]);
 2848+ }
 2849+ }
 2850+ };
 2851+ } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
 2852+ implementsControlRange && useDocumentSelection) {
 2853+
 2854+ selProto.removeAllRanges = function() {
 2855+ // Added try/catch as fix for issue #21
 2856+ try {
 2857+ this.docSelection.empty();
 2858+
 2859+ // Check for empty() not working (issue #24)
 2860+ if (this.docSelection.type != "None") {
 2861+ // Work around failure to empty a control selection by instead selecting a TextRange and then
 2862+ // calling empty()
 2863+ var doc;
 2864+ if (this.anchorNode) {
 2865+ doc = dom.getDocument(this.anchorNode);
 2866+ } else if (this.docSelection.type == CONTROL) {
 2867+ var controlRange = this.docSelection.createRange();
 2868+ if (controlRange.length) {
 2869+ doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
 2870+ }
 2871+ }
 2872+ if (doc) {
 2873+ var textRange = doc.body.createTextRange();
 2874+ textRange.select();
 2875+ this.docSelection.empty();
 2876+ }
 2877+ }
 2878+ } catch(ex) {}
 2879+ updateEmptySelection(this);
 2880+ };
 2881+
 2882+ selProto.addRange = function(range) {
 2883+ if (this.docSelection.type == CONTROL) {
 2884+ addRangeToControlSelection(this, range);
 2885+ } else {
 2886+ WrappedRange.rangeToTextRange(range).select();
 2887+ this._ranges[0] = range;
 2888+ this.rangeCount = 1;
 2889+ this.isCollapsed = this._ranges[0].collapsed;
 2890+ updateAnchorAndFocusFromRange(this, range, false);
 2891+ }
 2892+ };
 2893+
 2894+ selProto.setRanges = function(ranges) {
 2895+ this.removeAllRanges();
 2896+ var rangeCount = ranges.length;
 2897+ if (rangeCount > 1) {
 2898+ createControlSelection(this, ranges);
 2899+ } else if (rangeCount) {
 2900+ this.addRange(ranges[0]);
 2901+ }
 2902+ };
 2903+ } else {
 2904+ module.fail("No means of selecting a Range or TextRange was found");
 2905+ return false;
 2906+ }
 2907+
 2908+ selProto.getRangeAt = function(index) {
 2909+ if (index < 0 || index >= this.rangeCount) {
 2910+ throw new DOMException("INDEX_SIZE_ERR");
 2911+ } else {
 2912+ return this._ranges[index];
 2913+ }
 2914+ };
 2915+
 2916+ var refreshSelection;
 2917+
 2918+ if (useDocumentSelection) {
 2919+ refreshSelection = function(sel) {
 2920+ var range;
 2921+ if (api.isSelectionValid(sel.win)) {
 2922+ range = sel.docSelection.createRange();
 2923+ } else {
 2924+ range = dom.getBody(sel.win.document).createTextRange();
 2925+ range.collapse(true);
 2926+ }
 2927+
 2928+
 2929+ if (sel.docSelection.type == CONTROL) {
 2930+ updateControlSelection(sel);
 2931+ } else if (isTextRange(range)) {
 2932+ updateFromTextRange(sel, range);
 2933+ } else {
 2934+ updateEmptySelection(sel);
 2935+ }
 2936+ };
 2937+ } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
 2938+ refreshSelection = function(sel) {
 2939+ if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
 2940+ updateControlSelection(sel);
 2941+ } else {
 2942+ sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
 2943+ if (sel.rangeCount) {
 2944+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
 2945+ sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
 2946+ }
 2947+ updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
 2948+ sel.isCollapsed = selectionIsCollapsed(sel);
 2949+ } else {
 2950+ updateEmptySelection(sel);
 2951+ }
 2952+ }
 2953+ };
 2954+ } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
 2955+ refreshSelection = function(sel) {
 2956+ var range, nativeSel = sel.nativeSelection;
 2957+ if (nativeSel.anchorNode) {
 2958+ range = getSelectionRangeAt(nativeSel, 0);
 2959+ sel._ranges = [range];
 2960+ sel.rangeCount = 1;
 2961+ updateAnchorAndFocusFromNativeSelection(sel);
 2962+ sel.isCollapsed = selectionIsCollapsed(sel);
 2963+ } else {
 2964+ updateEmptySelection(sel);
 2965+ }
 2966+ };
 2967+ } else {
 2968+ module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
 2969+ return false;
 2970+ }
 2971+
 2972+ selProto.refresh = function(checkForChanges) {
 2973+ var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
 2974+ refreshSelection(this);
 2975+ if (checkForChanges) {
 2976+ var i = oldRanges.length;
 2977+ if (i != this._ranges.length) {
 2978+ return false;
 2979+ }
 2980+ while (i--) {
 2981+ if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
 2982+ return false;
 2983+ }
 2984+ }
 2985+ return true;
 2986+ }
 2987+ };
 2988+
 2989+ // Removal of a single range
 2990+ var removeRangeManually = function(sel, range) {
 2991+ var ranges = sel.getAllRanges(), removed = false;
 2992+ sel.removeAllRanges();
 2993+ for (var i = 0, len = ranges.length; i < len; ++i) {
 2994+ if (removed || range !== ranges[i]) {
 2995+ sel.addRange(ranges[i]);
 2996+ } else {
 2997+ // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
 2998+ // times. removeRange should only remove the first instance, so the following ensures only the first
 2999+ // instance is removed
 3000+ removed = true;
 3001+ }
 3002+ }
 3003+ if (!sel.rangeCount) {
 3004+ updateEmptySelection(sel);
 3005+ }
 3006+ };
 3007+
 3008+ if (implementsControlRange) {
 3009+ selProto.removeRange = function(range) {
 3010+ if (this.docSelection.type == CONTROL) {
 3011+ var controlRange = this.docSelection.createRange();
 3012+ var rangeElement = getSingleElementFromRange(range);
 3013+
 3014+ // Create a new ControlRange containing all the elements in the selected ControlRange minus the
 3015+ // element contained by the supplied range
 3016+ var doc = dom.getDocument(controlRange.item(0));
 3017+ var newControlRange = dom.getBody(doc).createControlRange();
 3018+ var el, removed = false;
 3019+ for (var i = 0, len = controlRange.length; i < len; ++i) {
 3020+ el = controlRange.item(i);
 3021+ if (el !== rangeElement || removed) {
 3022+ newControlRange.add(controlRange.item(i));
 3023+ } else {
 3024+ removed = true;
 3025+ }
 3026+ }
 3027+ newControlRange.select();
 3028+
 3029+ // Update the wrapped selection based on what's now in the native selection
 3030+ updateControlSelection(this);
 3031+ } else {
 3032+ removeRangeManually(this, range);
 3033+ }
 3034+ };
 3035+ } else {
 3036+ selProto.removeRange = function(range) {
 3037+ removeRangeManually(this, range);
 3038+ };
 3039+ }
 3040+
 3041+ // Detecting if a selection is backwards
 3042+ var selectionIsBackwards;
 3043+ if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
 3044+ selectionIsBackwards = function(sel) {
 3045+ var backwards = false;
 3046+ if (sel.anchorNode) {
 3047+ backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
 3048+ }
 3049+ return backwards;
 3050+ };
 3051+
 3052+ selProto.isBackwards = function() {
 3053+ return selectionIsBackwards(this);
 3054+ };
 3055+ } else {
 3056+ selectionIsBackwards = selProto.isBackwards = function() {
 3057+ return false;
 3058+ };
 3059+ }
 3060+
 3061+ // Selection text
 3062+ // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
 3063+ selProto.toString = function() {
 3064+
 3065+ var rangeTexts = [];
 3066+ for (var i = 0, len = this.rangeCount; i < len; ++i) {
 3067+ rangeTexts[i] = "" + this._ranges[i];
 3068+ }
 3069+ return rangeTexts.join("");
 3070+ };
 3071+
 3072+ function assertNodeInSameDocument(sel, node) {
 3073+ if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
 3074+ throw new DOMException("WRONG_DOCUMENT_ERR");
 3075+ }
 3076+ }
 3077+
 3078+ // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
 3079+ selProto.collapse = function(node, offset) {
 3080+ assertNodeInSameDocument(this, node);
 3081+ var range = api.createRange(dom.getDocument(node));
 3082+ range.collapseToPoint(node, offset);
 3083+ this.removeAllRanges();
 3084+ this.addRange(range);
 3085+ this.isCollapsed = true;
 3086+ };
 3087+
 3088+ selProto.collapseToStart = function() {
 3089+ if (this.rangeCount) {
 3090+ var range = this._ranges[0];
 3091+ this.collapse(range.startContainer, range.startOffset);
 3092+ } else {
 3093+ throw new DOMException("INVALID_STATE_ERR");
 3094+ }
 3095+ };
 3096+
 3097+ selProto.collapseToEnd = function() {
 3098+ if (this.rangeCount) {
 3099+ var range = this._ranges[this.rangeCount - 1];
 3100+ this.collapse(range.endContainer, range.endOffset);
 3101+ } else {
 3102+ throw new DOMException("INVALID_STATE_ERR");
 3103+ }
 3104+ };
 3105+
 3106+ // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
 3107+ // never used by Rangy.
 3108+ selProto.selectAllChildren = function(node) {
 3109+ assertNodeInSameDocument(this, node);
 3110+ var range = api.createRange(dom.getDocument(node));
 3111+ range.selectNodeContents(node);
 3112+ this.removeAllRanges();
 3113+ this.addRange(range);
 3114+ };
 3115+
 3116+ selProto.deleteFromDocument = function() {
 3117+ // Sepcial behaviour required for Control selections
 3118+ if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
 3119+ var controlRange = this.docSelection.createRange();
 3120+ var element;
 3121+ while (controlRange.length) {
 3122+ element = controlRange.item(0);
 3123+ controlRange.remove(element);
 3124+ element.parentNode.removeChild(element);
 3125+ }
 3126+ this.refresh();
 3127+ } else if (this.rangeCount) {
 3128+ var ranges = this.getAllRanges();
 3129+ this.removeAllRanges();
 3130+ for (var i = 0, len = ranges.length; i < len; ++i) {
 3131+ ranges[i].deleteContents();
 3132+ }
 3133+ // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
 3134+ // range. Firefox moves the selection to where the final selected range was, so we emulate that
 3135+ this.addRange(ranges[len - 1]);
 3136+ }
 3137+ };
 3138+
 3139+ // The following are non-standard extensions
 3140+ selProto.getAllRanges = function() {
 3141+ return this._ranges.slice(0);
 3142+ };
 3143+
 3144+ selProto.setSingleRange = function(range) {
 3145+ this.setRanges( [range] );
 3146+ };
 3147+
 3148+ selProto.containsNode = function(node, allowPartial) {
 3149+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
 3150+ if (this._ranges[i].containsNode(node, allowPartial)) {
 3151+ return true;
 3152+ }
 3153+ }
 3154+ return false;
 3155+ };
 3156+
 3157+ selProto.toHtml = function() {
 3158+ var html = "";
 3159+ if (this.rangeCount) {
 3160+ var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
 3161+ for (var i = 0, len = this._ranges.length; i < len; ++i) {
 3162+ container.appendChild(this._ranges[i].cloneContents());
 3163+ }
 3164+ html = container.innerHTML;
 3165+ }
 3166+ return html;
 3167+ };
 3168+
 3169+ function inspect(sel) {
 3170+ var rangeInspects = [];
 3171+ var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
 3172+ var focus = new DomPosition(sel.focusNode, sel.focusOffset);
 3173+ var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
 3174+
 3175+ if (typeof sel.rangeCount != "undefined") {
 3176+ for (var i = 0, len = sel.rangeCount; i < len; ++i) {
 3177+ rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
 3178+ }
 3179+ }
 3180+ return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
 3181+ ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
 3182+
 3183+ }
 3184+
 3185+ selProto.getName = function() {
 3186+ return "WrappedSelection";
 3187+ };
 3188+
 3189+ selProto.inspect = function() {
 3190+ return inspect(this);
 3191+ };
 3192+
 3193+ selProto.detach = function() {
 3194+ this.win[windowPropertyName] = null;
 3195+ this.win = this.anchorNode = this.focusNode = null;
 3196+ };
 3197+
 3198+ WrappedSelection.inspect = inspect;
 3199+
 3200+ api.Selection = WrappedSelection;
 3201+
 3202+ api.selectionPrototype = selProto;
 3203+
 3204+ api.addCreateMissingNativeApiListener(function(win) {
 3205+ if (typeof win.getSelection == "undefined") {
 3206+ win.getSelection = function() {
 3207+ return api.getSelection(this);
 3208+ };
 3209+ }
 3210+ win = null;
 3211+ });
 3212+});
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-selectionsaverestore.js
@@ -0,0 +1,195 @@
 2+/**
 3+ * @license Selection save and restore module for Rangy.
 4+ * Saves and restores user selections using marker invisible elements in the DOM.
 5+ *
 6+ * Part of Rangy, a cross-browser JavaScript range and selection library
 7+ * http://code.google.com/p/rangy/
 8+ *
 9+ * Depends on Rangy core.
 10+ *
 11+ * Copyright 2011, Tim Down
 12+ * Licensed under the MIT license.
 13+ * Version: 1.2.2
 14+ * Build date: 13 November 2011
 15+ */
 16+rangy.createModule("SaveRestore", function(api, module) {
 17+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
 18+
 19+ var dom = api.dom;
 20+
 21+ var markerTextChar = "\ufeff";
 22+
 23+ function gEBI(id, doc) {
 24+ return (doc || document).getElementById(id);
 25+ }
 26+
 27+ function insertRangeBoundaryMarker(range, atStart) {
 28+ var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
 29+ var markerEl;
 30+ var doc = dom.getDocument(range.startContainer);
 31+
 32+ // Clone the Range and collapse to the appropriate boundary point
 33+ var boundaryRange = range.cloneRange();
 34+ boundaryRange.collapse(atStart);
 35+
 36+ // Create the marker element containing a single invisible character using DOM methods and insert it
 37+ markerEl = doc.createElement("span");
 38+ markerEl.id = markerId;
 39+ markerEl.style.lineHeight = "0";
 40+ markerEl.style.display = "none";
 41+ markerEl.className = "rangySelectionBoundary";
 42+ markerEl.appendChild(doc.createTextNode(markerTextChar));
 43+
 44+ boundaryRange.insertNode(markerEl);
 45+ boundaryRange.detach();
 46+ return markerEl;
 47+ }
 48+
 49+ function setRangeBoundary(doc, range, markerId, atStart) {
 50+ var markerEl = gEBI(markerId, doc);
 51+ if (markerEl) {
 52+ range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
 53+ markerEl.parentNode.removeChild(markerEl);
 54+ } else {
 55+ module.warn("Marker element has been removed. Cannot restore selection.");
 56+ }
 57+ }
 58+
 59+ function compareRanges(r1, r2) {
 60+ return r2.compareBoundaryPoints(r1.START_TO_START, r1);
 61+ }
 62+
 63+ function saveSelection(win) {
 64+ win = win || window;
 65+ var doc = win.document;
 66+ if (!api.isSelectionValid(win)) {
 67+ module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
 68+ return;
 69+ }
 70+ var sel = api.getSelection(win);
 71+ var ranges = sel.getAllRanges();
 72+ var rangeInfos = [], startEl, endEl, range;
 73+
 74+ // Order the ranges by position within the DOM, latest first
 75+ ranges.sort(compareRanges);
 76+
 77+ for (var i = 0, len = ranges.length; i < len; ++i) {
 78+ range = ranges[i];
 79+ if (range.collapsed) {
 80+ endEl = insertRangeBoundaryMarker(range, false);
 81+ rangeInfos.push({
 82+ markerId: endEl.id,
 83+ collapsed: true
 84+ });
 85+ } else {
 86+ endEl = insertRangeBoundaryMarker(range, false);
 87+ startEl = insertRangeBoundaryMarker(range, true);
 88+
 89+ rangeInfos[i] = {
 90+ startMarkerId: startEl.id,
 91+ endMarkerId: endEl.id,
 92+ collapsed: false,
 93+ backwards: ranges.length == 1 && sel.isBackwards()
 94+ };
 95+ }
 96+ }
 97+
 98+ // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
 99+ // between its markers
 100+ for (i = len - 1; i >= 0; --i) {
 101+ range = ranges[i];
 102+ if (range.collapsed) {
 103+ range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
 104+ } else {
 105+ range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
 106+ range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
 107+ }
 108+ }
 109+
 110+ // Ensure current selection is unaffected
 111+ sel.setRanges(ranges);
 112+ return {
 113+ win: win,
 114+ doc: doc,
 115+ rangeInfos: rangeInfos,
 116+ restored: false
 117+ };
 118+ }
 119+
 120+ function restoreSelection(savedSelection, preserveDirection) {
 121+ if (!savedSelection.restored) {
 122+ var rangeInfos = savedSelection.rangeInfos;
 123+ var sel = api.getSelection(savedSelection.win);
 124+ var ranges = [];
 125+
 126+ // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
 127+ // normalization affecting previously restored ranges.
 128+ for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
 129+ rangeInfo = rangeInfos[i];
 130+ range = api.createRange(savedSelection.doc);
 131+ if (rangeInfo.collapsed) {
 132+ var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
 133+ if (markerEl) {
 134+ markerEl.style.display = "inline";
 135+ var previousNode = markerEl.previousSibling;
 136+
 137+ // Workaround for issue 17
 138+ if (previousNode && previousNode.nodeType == 3) {
 139+ markerEl.parentNode.removeChild(markerEl);
 140+ range.collapseToPoint(previousNode, previousNode.length);
 141+ } else {
 142+ range.collapseBefore(markerEl);
 143+ markerEl.parentNode.removeChild(markerEl);
 144+ }
 145+ } else {
 146+ module.warn("Marker element has been removed. Cannot restore selection.");
 147+ }
 148+ } else {
 149+ setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
 150+ setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
 151+ }
 152+
 153+ // Normalizing range boundaries is only viable if the selection contains only one range. For example,
 154+ // if the selection contained two ranges that were both contained within the same single text node,
 155+ // both would alter the same text node when restoring and break the other range.
 156+ if (len == 1) {
 157+ range.normalizeBoundaries();
 158+ }
 159+ ranges[i] = range;
 160+ }
 161+ if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
 162+ sel.removeAllRanges();
 163+ sel.addRange(ranges[0], true);
 164+ } else {
 165+ sel.setRanges(ranges);
 166+ }
 167+
 168+ savedSelection.restored = true;
 169+ }
 170+ }
 171+
 172+ function removeMarkerElement(doc, markerId) {
 173+ var markerEl = gEBI(markerId, doc);
 174+ if (markerEl) {
 175+ markerEl.parentNode.removeChild(markerEl);
 176+ }
 177+ }
 178+
 179+ function removeMarkers(savedSelection) {
 180+ var rangeInfos = savedSelection.rangeInfos;
 181+ for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
 182+ rangeInfo = rangeInfos[i];
 183+ if (rangeInfo.collapsed) {
 184+ removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
 185+ } else {
 186+ removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
 187+ removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
 188+ }
 189+ }
 190+ }
 191+
 192+ api.saveSelection = saveSelection;
 193+ api.restoreSelection = restoreSelection;
 194+ api.removeMarkerElement = removeMarkerElement;
 195+ api.removeMarkers = removeMarkers;
 196+});
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-serializer.js
@@ -0,0 +1,300 @@
 2+/**
 3+ * @license Serializer module for Rangy.
 4+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
 5+ * cookie or local storage and restore it on the user's next visit to the same page.
 6+ *
 7+ * Part of Rangy, a cross-browser JavaScript range and selection library
 8+ * http://code.google.com/p/rangy/
 9+ *
 10+ * Depends on Rangy core.
 11+ *
 12+ * Copyright 2011, Tim Down
 13+ * Licensed under the MIT license.
 14+ * Version: 1.2.2
 15+ * Build date: 13 November 2011
 16+ */
 17+rangy.createModule("Serializer", function(api, module) {
 18+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
 19+ var UNDEF = "undefined";
 20+
 21+ // encodeURIComponent and decodeURIComponent are required for cookie handling
 22+ if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
 23+ module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
 24+ }
 25+
 26+ // Checksum for checking whether range can be serialized
 27+ var crc32 = (function() {
 28+ function utf8encode(str) {
 29+ var utf8CharCodes = [];
 30+
 31+ for (var i = 0, len = str.length, c; i < len; ++i) {
 32+ c = str.charCodeAt(i);
 33+ if (c < 128) {
 34+ utf8CharCodes.push(c);
 35+ } else if (c < 2048) {
 36+ utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
 37+ } else {
 38+ utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
 39+ }
 40+ }
 41+ return utf8CharCodes;
 42+ }
 43+
 44+ var cachedCrcTable = null;
 45+
 46+ function buildCRCTable() {
 47+ var table = [];
 48+ for (var i = 0, j, crc; i < 256; ++i) {
 49+ crc = i;
 50+ j = 8;
 51+ while (j--) {
 52+ if ((crc & 1) == 1) {
 53+ crc = (crc >>> 1) ^ 0xEDB88320;
 54+ } else {
 55+ crc >>>= 1;
 56+ }
 57+ }
 58+ table[i] = crc >>> 0;
 59+ }
 60+ return table;
 61+ }
 62+
 63+ function getCrcTable() {
 64+ if (!cachedCrcTable) {
 65+ cachedCrcTable = buildCRCTable();
 66+ }
 67+ return cachedCrcTable;
 68+ }
 69+
 70+ return function(str) {
 71+ var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
 72+ for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
 73+ y = (crc ^ utf8CharCodes[i]) & 0xFF;
 74+ crc = (crc >>> 8) ^ crcTable[y];
 75+ }
 76+ return (crc ^ -1) >>> 0;
 77+ };
 78+ })();
 79+
 80+ var dom = api.dom;
 81+
 82+ function escapeTextForHtml(str) {
 83+ return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
 84+ }
 85+
 86+ function nodeToInfoString(node, infoParts) {
 87+ infoParts = infoParts || [];
 88+ var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
 89+ var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
 90+ var start = "", end = "";
 91+ switch (nodeType) {
 92+ case 3: // Text node
 93+ start = escapeTextForHtml(node.nodeValue);
 94+ break;
 95+ case 8: // Comment
 96+ start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
 97+ break;
 98+ default:
 99+ start = "<" + nodeInfo + ">";
 100+ end = "</>";
 101+ break;
 102+ }
 103+ if (start) {
 104+ infoParts.push(start);
 105+ }
 106+ for (var i = 0; i < childCount; ++i) {
 107+ nodeToInfoString(children[i], infoParts);
 108+ }
 109+ if (end) {
 110+ infoParts.push(end);
 111+ }
 112+ return infoParts;
 113+ }
 114+
 115+ // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
 116+ // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
 117+ // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
 118+ // innerHTML whenever the user changes an input within the element.
 119+ function getElementChecksum(el) {
 120+ var info = nodeToInfoString(el).join("");
 121+ return crc32(info).toString(16);
 122+ }
 123+
 124+ function serializePosition(node, offset, rootNode) {
 125+ var pathBits = [], n = node;
 126+ rootNode = rootNode || dom.getDocument(node).documentElement;
 127+ while (n && n != rootNode) {
 128+ pathBits.push(dom.getNodeIndex(n, true));
 129+ n = n.parentNode;
 130+ }
 131+ return pathBits.join("/") + ":" + offset;
 132+ }
 133+
 134+ function deserializePosition(serialized, rootNode, doc) {
 135+ if (rootNode) {
 136+ doc = doc || dom.getDocument(rootNode);
 137+ } else {
 138+ doc = doc || document;
 139+ rootNode = doc.documentElement;
 140+ }
 141+ var bits = serialized.split(":");
 142+ var node = rootNode;
 143+ var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
 144+
 145+ while (i--) {
 146+ nodeIndex = parseInt(nodeIndices[i], 10);
 147+ if (nodeIndex < node.childNodes.length) {
 148+ node = node.childNodes[parseInt(nodeIndices[i], 10)];
 149+ } else {
 150+ throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
 151+ " has no child with index " + nodeIndex + ", " + i);
 152+ }
 153+ }
 154+
 155+ return new dom.DomPosition(node, parseInt(bits[1], 10));
 156+ }
 157+
 158+ function serializeRange(range, omitChecksum, rootNode) {
 159+ rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
 160+ if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
 161+ throw new Error("serializeRange: range is not wholly contained within specified root node");
 162+ }
 163+ var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
 164+ serializePosition(range.endContainer, range.endOffset, rootNode);
 165+ if (!omitChecksum) {
 166+ serialized += "{" + getElementChecksum(rootNode) + "}";
 167+ }
 168+ return serialized;
 169+ }
 170+
 171+ function deserializeRange(serialized, rootNode, doc) {
 172+ if (rootNode) {
 173+ doc = doc || dom.getDocument(rootNode);
 174+ } else {
 175+ doc = doc || document;
 176+ rootNode = doc.documentElement;
 177+ }
 178+ var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
 179+ var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
 180+ if (checksum && checksum !== getElementChecksum(rootNode)) {
 181+ throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
 182+ ") and target root node (" + rootNodeChecksum + ") do not match");
 183+ }
 184+ var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
 185+ var range = api.createRange(doc);
 186+ range.setStart(start.node, start.offset);
 187+ range.setEnd(end.node, end.offset);
 188+ return range;
 189+ }
 190+
 191+ function canDeserializeRange(serialized, rootNode, doc) {
 192+ if (rootNode) {
 193+ doc = doc || dom.getDocument(rootNode);
 194+ } else {
 195+ doc = doc || document;
 196+ rootNode = doc.documentElement;
 197+ }
 198+ var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
 199+ var checksum = result[3];
 200+ return !checksum || checksum === getElementChecksum(rootNode);
 201+ }
 202+
 203+ function serializeSelection(selection, omitChecksum, rootNode) {
 204+ selection = selection || api.getSelection();
 205+ var ranges = selection.getAllRanges(), serializedRanges = [];
 206+ for (var i = 0, len = ranges.length; i < len; ++i) {
 207+ serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
 208+ }
 209+ return serializedRanges.join("|");
 210+ }
 211+
 212+ function deserializeSelection(serialized, rootNode, win) {
 213+ if (rootNode) {
 214+ win = win || dom.getWindow(rootNode);
 215+ } else {
 216+ win = win || window;
 217+ rootNode = win.document.documentElement;
 218+ }
 219+ var serializedRanges = serialized.split("|");
 220+ var sel = api.getSelection(win);
 221+ var ranges = [];
 222+
 223+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
 224+ ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
 225+ }
 226+ sel.setRanges(ranges);
 227+
 228+ return sel;
 229+ }
 230+
 231+ function canDeserializeSelection(serialized, rootNode, win) {
 232+ var doc;
 233+ if (rootNode) {
 234+ doc = win ? win.document : dom.getDocument(rootNode);
 235+ } else {
 236+ win = win || window;
 237+ rootNode = win.document.documentElement;
 238+ }
 239+ var serializedRanges = serialized.split("|");
 240+
 241+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
 242+ if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
 243+ return false;
 244+ }
 245+ }
 246+
 247+ return true;
 248+ }
 249+
 250+
 251+ var cookieName = "rangySerializedSelection";
 252+
 253+ function getSerializedSelectionFromCookie(cookie) {
 254+ var parts = cookie.split(/[;,]/);
 255+ for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
 256+ nameVal = parts[i].split("=");
 257+ if (nameVal[0].replace(/^\s+/, "") == cookieName) {
 258+ val = nameVal[1];
 259+ if (val) {
 260+ return decodeURIComponent(val.replace(/\s+$/, ""));
 261+ }
 262+ }
 263+ }
 264+ return null;
 265+ }
 266+
 267+ function restoreSelectionFromCookie(win) {
 268+ win = win || window;
 269+ var serialized = getSerializedSelectionFromCookie(win.document.cookie);
 270+ if (serialized) {
 271+ deserializeSelection(serialized, win.doc)
 272+ }
 273+ }
 274+
 275+ function saveSelectionCookie(win, props) {
 276+ win = win || window;
 277+ props = (typeof props == "object") ? props : {};
 278+ var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
 279+ var path = props.path ? ";path=" + props.path : "";
 280+ var domain = props.domain ? ";domain=" + props.domain : "";
 281+ var secure = props.secure ? ";secure" : "";
 282+ var serialized = serializeSelection(api.getSelection(win));
 283+ win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
 284+ }
 285+
 286+ api.serializePosition = serializePosition;
 287+ api.deserializePosition = deserializePosition;
 288+
 289+ api.serializeRange = serializeRange;
 290+ api.deserializeRange = deserializeRange;
 291+ api.canDeserializeRange = canDeserializeRange;
 292+
 293+ api.serializeSelection = serializeSelection;
 294+ api.deserializeSelection = deserializeSelection;
 295+ api.canDeserializeSelection = canDeserializeSelection;
 296+
 297+ api.restoreSelectionFromCookie = restoreSelectionFromCookie;
 298+ api.saveSelectionCookie = saveSelectionCookie;
 299+
 300+ api.getElementChecksum = getElementChecksum;
 301+});
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-cssclassapplier.js
@@ -0,0 +1,713 @@
 2+/**
 3+ * @license CSS Class Applier module for Rangy.
 4+ * Adds, removes and toggles CSS classes on Ranges and Selections
 5+ *
 6+ * Part of Rangy, a cross-browser JavaScript range and selection library
 7+ * http://code.google.com/p/rangy/
 8+ *
 9+ * Depends on Rangy core.
 10+ *
 11+ * Copyright 2011, Tim Down
 12+ * Licensed under the MIT license.
 13+ * Version: 1.2.2
 14+ * Build date: 13 November 2011
 15+ */
 16+rangy.createModule("CssClassApplier", function(api, module) {
 17+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
 18+
 19+ var dom = api.dom;
 20+
 21+
 22+
 23+ var defaultTagName = "span";
 24+
 25+ function trim(str) {
 26+ return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
 27+ }
 28+
 29+ function hasClass(el, cssClass) {
 30+ return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
 31+ }
 32+
 33+ function addClass(el, cssClass) {
 34+ if (el.className) {
 35+ if (!hasClass(el, cssClass)) {
 36+ el.className += " " + cssClass;
 37+ }
 38+ } else {
 39+ el.className = cssClass;
 40+ }
 41+ }
 42+
 43+ var removeClass = (function() {
 44+ function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
 45+ return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
 46+ }
 47+
 48+ return function(el, cssClass) {
 49+ if (el.className) {
 50+ el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
 51+ }
 52+ };
 53+ })();
 54+
 55+ function sortClassName(className) {
 56+ return className.split(/\s+/).sort().join(" ");
 57+ }
 58+
 59+ function getSortedClassName(el) {
 60+ return sortClassName(el.className);
 61+ }
 62+
 63+ function haveSameClasses(el1, el2) {
 64+ return getSortedClassName(el1) == getSortedClassName(el2);
 65+ }
 66+
 67+ function replaceWithOwnChildren(el) {
 68+
 69+ var parent = el.parentNode;
 70+ while (el.hasChildNodes()) {
 71+ parent.insertBefore(el.firstChild, el);
 72+ }
 73+ parent.removeChild(el);
 74+ }
 75+
 76+ function rangeSelectsAnyText(range, textNode) {
 77+ var textRange = range.cloneRange();
 78+ textRange.selectNodeContents(textNode);
 79+
 80+ var intersectionRange = textRange.intersection(range);
 81+ var text = intersectionRange ? intersectionRange.toString() : "";
 82+ textRange.detach();
 83+
 84+ return text != "";
 85+ }
 86+
 87+ function getEffectiveTextNodes(range) {
 88+ return range.getNodes([3], function(textNode) {
 89+ return rangeSelectsAnyText(range, textNode);
 90+ });
 91+ }
 92+
 93+ function elementsHaveSameNonClassAttributes(el1, el2) {
 94+ if (el1.attributes.length != el2.attributes.length) return false;
 95+ for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
 96+ attr1 = el1.attributes[i];
 97+ name = attr1.name;
 98+ if (name != "class") {
 99+ attr2 = el2.attributes.getNamedItem(name);
 100+ if (attr1.specified != attr2.specified) return false;
 101+ if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
 102+ }
 103+ }
 104+ return true;
 105+ }
 106+
 107+ function elementHasNonClassAttributes(el, exceptions) {
 108+ for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
 109+ attrName = el.attributes[i].name;
 110+ if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
 111+ return true;
 112+ }
 113+ }
 114+ return false;
 115+ }
 116+
 117+ function elementHasProps(el, props) {
 118+ for (var p in props) {
 119+ if (props.hasOwnProperty(p) && el[p] !== props[p]) {
 120+ return false;
 121+ }
 122+ }
 123+ return true;
 124+ }
 125+
 126+ var getComputedStyleProperty;
 127+
 128+ if (typeof window.getComputedStyle != "undefined") {
 129+ getComputedStyleProperty = function(el, propName) {
 130+ return dom.getWindow(el).getComputedStyle(el, null)[propName];
 131+ };
 132+ } else if (typeof document.documentElement.currentStyle != "undefined") {
 133+ getComputedStyleProperty = function(el, propName) {
 134+ return el.currentStyle[propName];
 135+ };
 136+ } else {
 137+ module.fail("No means of obtaining computed style properties found");
 138+ }
 139+
 140+ var isEditableElement;
 141+
 142+ (function() {
 143+ var testEl = document.createElement("div");
 144+ if (typeof testEl.isContentEditable == "boolean") {
 145+ isEditableElement = function(node) {
 146+ return node && node.nodeType == 1 && node.isContentEditable;
 147+ };
 148+ } else {
 149+ isEditableElement = function(node) {
 150+ if (!node || node.nodeType != 1 || node.contentEditable == "false") {
 151+ return false;
 152+ }
 153+ return node.contentEditable == "true" || isEditableElement(node.parentNode);
 154+ };
 155+ }
 156+ })();
 157+
 158+ function isEditingHost(node) {
 159+ var parent;
 160+ return node && node.nodeType == 1
 161+ && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
 162+ || (isEditableElement(node) && !isEditableElement(node.parentNode)));
 163+ }
 164+
 165+ function isEditable(node) {
 166+ return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
 167+ }
 168+
 169+ var inlineDisplayRegex = /^inline(-block|-table)?$/i;
 170+
 171+ function isNonInlineElement(node) {
 172+ return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
 173+ }
 174+
 175+ // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
 176+ var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
 177+
 178+ function isUnrenderedWhiteSpaceNode(node) {
 179+ if (node.data.length == 0) {
 180+ return true;
 181+ }
 182+ if (htmlNonWhiteSpaceRegex.test(node.data)) {
 183+ return false;
 184+ }
 185+ var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
 186+ switch (cssWhiteSpace) {
 187+ case "pre":
 188+ case "pre-wrap":
 189+ case "-moz-pre-wrap":
 190+ return false;
 191+ case "pre-line":
 192+ if (/[\r\n]/.test(node.data)) {
 193+ return false;
 194+ }
 195+ }
 196+
 197+ // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
 198+ // non-inline element, it will not be rendered. This seems to be a good enough definition.
 199+ return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
 200+ }
 201+
 202+ function isSplitPoint(node, offset) {
 203+ if (dom.isCharacterDataNode(node)) {
 204+ if (offset == 0) {
 205+ return !!node.previousSibling;
 206+ } else if (offset == node.length) {
 207+ return !!node.nextSibling;
 208+ } else {
 209+ return true;
 210+ }
 211+ }
 212+
 213+ return offset > 0 && offset < node.childNodes.length;
 214+ }
 215+
 216+ function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) {
 217+ var newNode;
 218+ var splitAtStart = (descendantOffset == 0);
 219+
 220+ if (dom.isAncestorOf(descendantNode, node)) {
 221+
 222+ return node;
 223+ }
 224+
 225+ if (dom.isCharacterDataNode(descendantNode)) {
 226+ if (descendantOffset == 0) {
 227+ descendantOffset = dom.getNodeIndex(descendantNode);
 228+ descendantNode = descendantNode.parentNode;
 229+ } else if (descendantOffset == descendantNode.length) {
 230+ descendantOffset = dom.getNodeIndex(descendantNode) + 1;
 231+ descendantNode = descendantNode.parentNode;
 232+ } else {
 233+ throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
 234+ + descendantOffset + " in " + descendantNode.data);
 235+ }
 236+ }
 237+
 238+ if (isSplitPoint(descendantNode, descendantOffset)) {
 239+ if (!newNode) {
 240+ newNode = descendantNode.cloneNode(false);
 241+ if (newNode.id) {
 242+ newNode.removeAttribute("id");
 243+ }
 244+ var child;
 245+ while ((child = descendantNode.childNodes[descendantOffset])) {
 246+ newNode.appendChild(child);
 247+ }
 248+ dom.insertAfter(newNode, descendantNode);
 249+ }
 250+ return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
 251+ } else if (node != descendantNode) {
 252+ newNode = descendantNode.parentNode;
 253+
 254+ // Work out a new split point in the parent node
 255+ var newNodeIndex = dom.getNodeIndex(descendantNode);
 256+
 257+ if (!splitAtStart) {
 258+ newNodeIndex++;
 259+ }
 260+ return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve);
 261+ }
 262+ return node;
 263+ }
 264+
 265+ function areElementsMergeable(el1, el2) {
 266+ return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
 267+ }
 268+
 269+ function createAdjacentMergeableTextNodeGetter(forward) {
 270+ var propName = forward ? "nextSibling" : "previousSibling";
 271+
 272+ return function(textNode, checkParentElement) {
 273+ var el = textNode.parentNode;
 274+ var adjacentNode = textNode[propName];
 275+ if (adjacentNode) {
 276+ // Can merge if the node's previous/next sibling is a text node
 277+ if (adjacentNode && adjacentNode.nodeType == 3) {
 278+ return adjacentNode;
 279+ }
 280+ } else if (checkParentElement) {
 281+ // Compare text node parent element with its sibling
 282+ adjacentNode = el[propName];
 283+
 284+ if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
 285+ return adjacentNode[forward ? "firstChild" : "lastChild"];
 286+ }
 287+ }
 288+ return null;
 289+ }
 290+ }
 291+
 292+ var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
 293+ getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
 294+
 295+
 296+ function Merge(firstNode) {
 297+ this.isElementMerge = (firstNode.nodeType == 1);
 298+ this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
 299+ this.textNodes = [this.firstTextNode];
 300+ }
 301+
 302+ Merge.prototype = {
 303+ doMerge: function() {
 304+ var textBits = [], textNode, parent, text;
 305+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
 306+ textNode = this.textNodes[i];
 307+ parent = textNode.parentNode;
 308+ textBits[i] = textNode.data;
 309+ if (i) {
 310+ parent.removeChild(textNode);
 311+ if (!parent.hasChildNodes()) {
 312+ parent.parentNode.removeChild(parent);
 313+ }
 314+ }
 315+ }
 316+ this.firstTextNode.data = text = textBits.join("");
 317+ return text;
 318+ },
 319+
 320+ getLength: function() {
 321+ var i = this.textNodes.length, len = 0;
 322+ while (i--) {
 323+ len += this.textNodes[i].length;
 324+ }
 325+ return len;
 326+ },
 327+
 328+ toString: function() {
 329+ var textBits = [];
 330+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
 331+ textBits[i] = "'" + this.textNodes[i].data + "'";
 332+ }
 333+ return "[Merge(" + textBits.join(",") + ")]";
 334+ }
 335+ };
 336+
 337+ var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
 338+
 339+ // Allow "class" as a property name in object properties
 340+ var mappedPropertyNames = {"class" : "className"};
 341+
 342+ function CssClassApplier(cssClass, options, tagNames) {
 343+ this.cssClass = cssClass;
 344+ var normalize, i, len, propName;
 345+
 346+ var elementPropertiesFromOptions = null;
 347+
 348+ // Initialize from options object
 349+ if (typeof options == "object" && options !== null) {
 350+ tagNames = options.tagNames;
 351+ elementPropertiesFromOptions = options.elementProperties;
 352+
 353+ for (i = 0; propName = optionProperties[i++]; ) {
 354+ if (options.hasOwnProperty(propName)) {
 355+ this[propName] = options[propName];
 356+ }
 357+ }
 358+ normalize = options.normalize;
 359+ } else {
 360+ normalize = options;
 361+ }
 362+
 363+ // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
 364+ this.normalize = (typeof normalize == "undefined") ? true : normalize;
 365+
 366+ // Initialize element properties and attribute exceptions
 367+ this.attrExceptions = [];
 368+ var el = document.createElement(this.elementTagName);
 369+ this.elementProperties = {};
 370+ for (var p in elementPropertiesFromOptions) {
 371+ if (elementPropertiesFromOptions.hasOwnProperty(p)) {
 372+ // Map "class" to "className"
 373+ if (mappedPropertyNames.hasOwnProperty(p)) {
 374+ p = mappedPropertyNames[p];
 375+ }
 376+ el[p] = elementPropertiesFromOptions[p];
 377+
 378+ // Copy the property back from the dummy element so that later comparisons to check whether elements
 379+ // may be removed are checking against the right value. For example, the href property of an element
 380+ // returns a fully qualified URL even if it was previously assigned a relative URL.
 381+ this.elementProperties[p] = el[p];
 382+ this.attrExceptions.push(p);
 383+ }
 384+ }
 385+
 386+ this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
 387+ sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
 388+
 389+ // Initialize tag names
 390+ this.applyToAnyTagName = false;
 391+ var type = typeof tagNames;
 392+ if (type == "string") {
 393+ if (tagNames == "*") {
 394+ this.applyToAnyTagName = true;
 395+ } else {
 396+ this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
 397+ }
 398+ } else if (type == "object" && typeof tagNames.length == "number") {
 399+ this.tagNames = [];
 400+ for (i = 0, len = tagNames.length; i < len; ++i) {
 401+ if (tagNames[i] == "*") {
 402+ this.applyToAnyTagName = true;
 403+ } else {
 404+ this.tagNames.push(tagNames[i].toLowerCase());
 405+ }
 406+ }
 407+ } else {
 408+ this.tagNames = [this.elementTagName];
 409+ }
 410+ }
 411+
 412+ CssClassApplier.prototype = {
 413+ elementTagName: defaultTagName,
 414+ elementProperties: {},
 415+ ignoreWhiteSpace: true,
 416+ applyToEditableOnly: false,
 417+
 418+ hasClass: function(node) {
 419+ return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
 420+ },
 421+
 422+ getSelfOrAncestorWithClass: function(node) {
 423+ while (node) {
 424+ if (this.hasClass(node, this.cssClass)) {
 425+ return node;
 426+ }
 427+ node = node.parentNode;
 428+ }
 429+ return null;
 430+ },
 431+
 432+ isModifiable: function(node) {
 433+ return !this.applyToEditableOnly || isEditable(node);
 434+ },
 435+
 436+ // White space adjacent to an unwrappable node can be ignored for wrapping
 437+ isIgnorableWhiteSpaceNode: function(node) {
 438+ return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
 439+ },
 440+
 441+ // Normalizes nodes after applying a CSS class to a Range.
 442+ postApply: function(textNodes, range, isUndo) {
 443+
 444+ var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
 445+
 446+ var merges = [], currentMerge;
 447+
 448+ var rangeStartNode = firstNode, rangeEndNode = lastNode;
 449+ var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
 450+
 451+ var textNode, precedingTextNode;
 452+
 453+ for (var i = 0, len = textNodes.length; i < len; ++i) {
 454+ textNode = textNodes[i];
 455+ precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
 456+
 457+ if (precedingTextNode) {
 458+ if (!currentMerge) {
 459+ currentMerge = new Merge(precedingTextNode);
 460+ merges.push(currentMerge);
 461+ }
 462+ currentMerge.textNodes.push(textNode);
 463+ if (textNode === firstNode) {
 464+ rangeStartNode = currentMerge.firstTextNode;
 465+ rangeStartOffset = rangeStartNode.length;
 466+ }
 467+ if (textNode === lastNode) {
 468+ rangeEndNode = currentMerge.firstTextNode;
 469+ rangeEndOffset = currentMerge.getLength();
 470+ }
 471+ } else {
 472+ currentMerge = null;
 473+ }
 474+ }
 475+
 476+ // Test whether the first node after the range needs merging
 477+ var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
 478+
 479+ if (nextTextNode) {
 480+ if (!currentMerge) {
 481+ currentMerge = new Merge(lastNode);
 482+ merges.push(currentMerge);
 483+ }
 484+ currentMerge.textNodes.push(nextTextNode);
 485+ }
 486+
 487+ // Do the merges
 488+ if (merges.length) {
 489+
 490+ for (i = 0, len = merges.length; i < len; ++i) {
 491+ merges[i].doMerge();
 492+ }
 493+
 494+
 495+ // Set the range boundaries
 496+ range.setStart(rangeStartNode, rangeStartOffset);
 497+ range.setEnd(rangeEndNode, rangeEndOffset);
 498+ }
 499+
 500+ },
 501+
 502+ createContainer: function(doc) {
 503+ var el = doc.createElement(this.elementTagName);
 504+ api.util.extend(el, this.elementProperties);
 505+ addClass(el, this.cssClass);
 506+ return el;
 507+ },
 508+
 509+ applyToTextNode: function(textNode) {
 510+
 511+
 512+ var parent = textNode.parentNode;
 513+ if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
 514+ addClass(parent, this.cssClass);
 515+ } else {
 516+ var el = this.createContainer(dom.getDocument(textNode));
 517+ textNode.parentNode.insertBefore(el, textNode);
 518+ el.appendChild(textNode);
 519+ }
 520+
 521+ },
 522+
 523+ isRemovable: function(el) {
 524+ return el.tagName.toLowerCase() == this.elementTagName
 525+ && getSortedClassName(el) == this.elementSortedClassName
 526+ && elementHasProps(el, this.elementProperties)
 527+ && !elementHasNonClassAttributes(el, this.attrExceptions)
 528+ && this.isModifiable(el);
 529+ },
 530+
 531+ undoToTextNode: function(textNode, range, ancestorWithClass) {
 532+
 533+ if (!range.containsNode(ancestorWithClass)) {
 534+ // Split out the portion of the ancestor from which we can remove the CSS class
 535+ //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
 536+ var ancestorRange = range.cloneRange();
 537+ ancestorRange.selectNode(ancestorWithClass);
 538+
 539+ if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
 540+ splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
 541+ range.setEndAfter(ancestorWithClass);
 542+ }
 543+ if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
 544+ ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
 545+ }
 546+ }
 547+
 548+ if (this.isRemovable(ancestorWithClass)) {
 549+ replaceWithOwnChildren(ancestorWithClass);
 550+ } else {
 551+ removeClass(ancestorWithClass, this.cssClass);
 552+ }
 553+ },
 554+
 555+ applyToRange: function(range) {
 556+ range.splitBoundaries();
 557+ var textNodes = getEffectiveTextNodes(range);
 558+
 559+ if (textNodes.length) {
 560+ var textNode;
 561+
 562+ for (var i = 0, len = textNodes.length; i < len; ++i) {
 563+ textNode = textNodes[i];
 564+
 565+ if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
 566+ && this.isModifiable(textNode)) {
 567+ this.applyToTextNode(textNode);
 568+ }
 569+ }
 570+ range.setStart(textNodes[0], 0);
 571+ textNode = textNodes[textNodes.length - 1];
 572+ range.setEnd(textNode, textNode.length);
 573+ if (this.normalize) {
 574+ this.postApply(textNodes, range, false);
 575+ }
 576+ }
 577+ },
 578+
 579+ applyToSelection: function(win) {
 580+
 581+ win = win || window;
 582+ var sel = api.getSelection(win);
 583+
 584+ var range, ranges = sel.getAllRanges();
 585+ sel.removeAllRanges();
 586+ var i = ranges.length;
 587+ while (i--) {
 588+ range = ranges[i];
 589+ this.applyToRange(range);
 590+ sel.addRange(range);
 591+ }
 592+
 593+ },
 594+
 595+ undoToRange: function(range) {
 596+
 597+ range.splitBoundaries();
 598+ var textNodes = getEffectiveTextNodes(range);
 599+ var textNode, ancestorWithClass;
 600+ var lastTextNode = textNodes[textNodes.length - 1];
 601+
 602+ if (textNodes.length) {
 603+ for (var i = 0, len = textNodes.length; i < len; ++i) {
 604+ textNode = textNodes[i];
 605+ ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
 606+ if (ancestorWithClass && this.isModifiable(textNode)) {
 607+ this.undoToTextNode(textNode, range, ancestorWithClass);
 608+ }
 609+
 610+ // Ensure the range is still valid
 611+ range.setStart(textNodes[0], 0);
 612+ range.setEnd(lastTextNode, lastTextNode.length);
 613+ }
 614+
 615+
 616+
 617+ if (this.normalize) {
 618+ this.postApply(textNodes, range, true);
 619+ }
 620+ }
 621+ },
 622+
 623+ undoToSelection: function(win) {
 624+ win = win || window;
 625+ var sel = api.getSelection(win);
 626+ var ranges = sel.getAllRanges(), range;
 627+ sel.removeAllRanges();
 628+ for (var i = 0, len = ranges.length; i < len; ++i) {
 629+ range = ranges[i];
 630+ this.undoToRange(range);
 631+ sel.addRange(range);
 632+ }
 633+ },
 634+
 635+ getTextSelectedByRange: function(textNode, range) {
 636+ var textRange = range.cloneRange();
 637+ textRange.selectNodeContents(textNode);
 638+
 639+ var intersectionRange = textRange.intersection(range);
 640+ var text = intersectionRange ? intersectionRange.toString() : "";
 641+ textRange.detach();
 642+
 643+ return text;
 644+ },
 645+
 646+ isAppliedToRange: function(range) {
 647+ if (range.collapsed) {
 648+ return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
 649+ } else {
 650+ var textNodes = range.getNodes( [3] );
 651+ for (var i = 0, textNode; textNode = textNodes[i++]; ) {
 652+ if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
 653+ && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
 654+ return false;
 655+ }
 656+ }
 657+ return true;
 658+ }
 659+ },
 660+
 661+ isAppliedToSelection: function(win) {
 662+ win = win || window;
 663+ var sel = api.getSelection(win);
 664+ var ranges = sel.getAllRanges();
 665+ var i = ranges.length;
 666+ while (i--) {
 667+ if (!this.isAppliedToRange(ranges[i])) {
 668+ return false;
 669+ }
 670+ }
 671+
 672+ return true;
 673+ },
 674+
 675+ toggleRange: function(range) {
 676+ if (this.isAppliedToRange(range)) {
 677+ this.undoToRange(range);
 678+ } else {
 679+ this.applyToRange(range);
 680+ }
 681+ },
 682+
 683+ toggleSelection: function(win) {
 684+ if (this.isAppliedToSelection(win)) {
 685+ this.undoToSelection(win);
 686+ } else {
 687+ this.applyToSelection(win);
 688+ }
 689+ },
 690+
 691+ detach: function() {}
 692+ };
 693+
 694+ function createCssClassApplier(cssClass, options, tagNames) {
 695+ return new CssClassApplier(cssClass, options, tagNames);
 696+ }
 697+
 698+ CssClassApplier.util = {
 699+ hasClass: hasClass,
 700+ addClass: addClass,
 701+ removeClass: removeClass,
 702+ hasSameClasses: haveSameClasses,
 703+ replaceWithOwnChildren: replaceWithOwnChildren,
 704+ elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
 705+ elementHasNonClassAttributes: elementHasNonClassAttributes,
 706+ splitNodeAt: splitNodeAt,
 707+ isEditableElement: isEditableElement,
 708+ isEditingHost: isEditingHost,
 709+ isEditable: isEditable
 710+ };
 711+
 712+ api.CssClassApplier = CssClassApplier;
 713+ api.createCssClassApplier = createCssClassApplier;
 714+});
Index: trunk/extensions/VisualEditor/contentEditable/index.php
@@ -0,0 +1,134 @@
 2+<!DOCTYPE html>
 3+
 4+<html>
 5+ <head>
 6+ <title>EditSurface + ContentEditable Demo</title>
 7+ <style>
 8+.es-contentView-ruler {
 9+ position: absolute;
 10+ top: 0;
 11+ left: 0;
 12+ display: inline-block;
 13+ z-index: -1000;
 14+}
 15+.es-paragraphView {
 16+ margin-bottom: 20px;
 17+}
 18+#es-preview {
 19+ font-family: monospace,"Courier New";
 20+ white-space: pre-wrap;
 21+}
 22+.es-contentView-format-textStyle-italic,
 23+.es-contentView-format-textStyle-emphasize {
 24+ font-style: italic;
 25+}
 26+
 27+.es-contentView-format-textStyle-bold,
 28+.es-contentView-format-textStyle-strong {
 29+ font-weight: bold;
 30+}
 31+ </style>
 32+ </head>
 33+ <body>
 34+ <script src="diff_match_patch.js"></script>
 35+
 36+ <!-- Rangy -->
 37+ <script src="rangy/rangy-core.js"></script>
 38+ <script src="rangy/rangy-cssclassapplier.js"></script>
 39+ <script src="rangy/rangy-selectionsaverestore.js"></script>
 40+ <script src="rangy/rangy-serializer.js"></script>
 41+
 42+ <!-- EditSurface -->
 43+ <script src="../modules/jquery/jquery.js"></script>
 44+ <script src="../modules/es/es.js"></script>
 45+ <script src="../modules/es/es.Html.js"></script>
 46+ <script src="../modules/es/es.Position.js"></script>
 47+ <script src="../modules/es/es.Range.js"></script>
 48+ <script src="../modules/es/es.TransactionProcessor.js"></script>
 49+
 50+ <!-- Serializers -->
 51+ <script src="../modules/es/serializers/es.AnnotationSerializer.js"></script>
 52+ <script src="../modules/es/serializers/es.HtmlSerializer.js"></script>
 53+ <script src="../modules/es/serializers/es.JsonSerializer.js"></script>
 54+ <script src="../modules/es/serializers/es.WikitextSerializer.js"></script>
 55+
 56+ <!-- Bases -->
 57+ <script src="../modules/es/bases/es.EventEmitter.js"></script>
 58+ <script src="../modules/es/bases/es.DocumentNode.js"></script>
 59+ <script src="../modules/es/bases/es.DocumentModelNode.js"></script>
 60+ <script src="../modules/es/bases/es.DocumentBranchNode.js"></script>
 61+ <script src="../modules/es/bases/es.DocumentLeafNode.js"></script>
 62+ <script src="../modules/es/bases/es.DocumentModelBranchNode.js"></script>
 63+ <script src="../modules/es/bases/es.DocumentModelLeafNode.js"></script>
 64+ <script src="../modules/es/bases/es.DocumentViewNode.js"></script>
 65+ <script src="../modules/es/bases/es.DocumentViewBranchNode.js"></script>
 66+ <script src="views/es.DocumentViewLeafNode.js"></script>
 67+ <script src="../modules/es/bases/es.Inspector.js"></script>
 68+ <script src="../modules/es/bases/es.Tool.js"></script>
 69+
 70+ <!-- Models -->
 71+ <script src="../modules/es/models/es.SurfaceModel.js"></script>
 72+ <script src="../modules/es/models/es.DocumentModel.js"></script>
 73+ <script src="../modules/es/models/es.ParagraphModel.js"></script>
 74+ <script src="../modules/es/models/es.PreModel.js"></script>
 75+ <script src="../modules/es/models/es.ListModel.js"></script>
 76+ <script src="../modules/es/models/es.ListItemModel.js"></script>
 77+ <script src="../modules/es/models/es.TableModel.js"></script>
 78+ <script src="../modules/es/models/es.TableRowModel.js"></script>
 79+ <script src="../modules/es/models/es.TableCellModel.js"></script>
 80+ <script src="../modules/es/models/es.HeadingModel.js"></script>
 81+ <script src="../modules/es/models/es.TransactionModel.js"></script>
 82+
 83+ <!-- Inspectors -->
 84+ <script src="../modules/es/inspectors/es.LinkInspector.js"></script>
 85+
 86+ <!-- Tools -->
 87+ <script src="../modules/es/tools/es.ButtonTool.js"></script>
 88+ <script src="../modules/es/tools/es.AnnotationButtonTool.js"></script>
 89+ <script src="../modules/es/tools/es.ClearButtonTool.js"></script>
 90+ <script src="../modules/es/tools/es.HistoryButtonTool.js"></script>
 91+ <script src="../modules/es/tools/es.ListButtonTool.js"></script>
 92+ <script src="../modules/es/tools/es.IndentationButtonTool.js"></script>
 93+ <script src="../modules/es/tools/es.DropdownTool.js"></script>
 94+ <script src="../modules/es/tools/es.FormatDropdownTool.js"></script>
 95+
 96+ <!-- Views -->
 97+ <!--
 98+ <script src="../modules/es/views/es.SurfaceView.js"></script>
 99+ <script src="../modules/es/views/es.ToolbarView.js"></script>
 100+ <script src="../modules/es/views/es.ContentView.js"></script>
 101+ <script src="../modules/es/views/es.ContextView.js"></script>
 102+ <script src="../modules/es/views/es.DocumentView.js"></script>
 103+ <script src="../modules/es/views/es.ParagraphView.js"></script>
 104+ <script src="../modules/es/views/es.PreView.js"></script>
 105+ <script src="../modules/es/views/es.ListView.js"></script>
 106+ <script src="../modules/es/views/es.MenuView.js"></script>
 107+ <script src="../modules/es/views/es.ListItemView.js"></script>
 108+ <script src="../modules/es/views/es.TableView.js"></script>
 109+ <script src="../modules/es/views/es.TableRowView.js"></script>
 110+ <script src="../modules/es/views/es.TableCellView.js"></script>
 111+ <script src="../modules/es/views/es.HeadingView.js"></script>
 112+ -->
 113+ <script src="views/es.SurfaceView.js"></script>
 114+ <script src="views/es.ContentView.js"></script>
 115+ <script src="views/es.DocumentView.js"></script>
 116+ <script src="views/es.ParagraphView.js"></script>
 117+
 118+
 119+ <!-- Demo -->
 120+ <script src="main.js"></script>
 121+
 122+ <table style="margin: auto; width: 1000px; border: solid 1px;">
 123+ <tr>
 124+ <td style="width: 500px; vertical-align: top; padding: 10px;">
 125+ <div id="es-editor"></div>
 126+ </td>
 127+ <td style="width: 500px; vertical-align: top; padding: 10px;">
 128+ <div id="es-preview"></div>
 129+ </td>
 130+ </tr>
 131+ </table>
 132+
 133+
 134+ </body>
 135+</html>
Index: trunk/extensions/VisualEditor/contentEditable/views/es.ParagraphView.js
@@ -0,0 +1,26 @@
 2+/**
 3+ * Creates an es.ParagraphView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewLeafNode}
 8+ * @param {es.ParagraphModel} model Paragraph model to view
 9+ */
 10+es.ParagraphView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewLeafNode.call( this, model );
 13+
 14+ // DOM Changes
 15+ this.$.addClass( 'es-paragraphView' );
 16+};
 17+
 18+/* Registration */
 19+
 20+es.DocumentView.splitRules.paragraph = {
 21+ 'self': true,
 22+ 'children': null
 23+};
 24+
 25+/* Inheritance */
 26+
 27+es.extendClass( es.ParagraphView, es.DocumentViewLeafNode );
Index: trunk/extensions/VisualEditor/contentEditable/views/es.SurfaceView.js
@@ -0,0 +1,48 @@
 2+es.SurfaceView = function( $container, model ) {
 3+ // Inheritance
 4+ es.EventEmitter.call( this );
 5+
 6+ // References for use in closures
 7+ var _this = this;
 8+
 9+ // Properties
 10+ this.model = model;
 11+ this.documentView = new es.DocumentView( this.model.getDocument(), this );
 12+ this.$ = $container.append( this.documentView.$ );
 13+
 14+ this.$.keydown( function(e) {
 15+ return _this.onKeyDown( e );
 16+ } );
 17+
 18+ this.documentView.renderContent();
 19+};
 20+
 21+es.SurfaceView.prototype.onKeyDown = function( e ) {
 22+ if ( e.which == 13 ) {
 23+ e.preventDefault();
 24+
 25+ console.log(this.getSelection());
 26+ }
 27+};
 28+
 29+es.SurfaceView.prototype.getSelection = function() {
 30+ var selection = rangy.getSelection();
 31+
 32+ var node = selection.anchorNode;
 33+ var $node = $( node );
 34+ while( !$node.hasClass( 'es-paragraphView' ) ) {
 35+ $node = $node.parent();
 36+ }
 37+ var $contents = $node.contents();
 38+ for( var i = 0; i < $contents.length; i++ ) {
 39+ if ( $contents[i] == node ) {
 40+ console.log(node);
 41+ }
 42+ }
 43+
 44+ return 0;
 45+};
 46+
 47+/* Inheritance */
 48+
 49+es.extendClass( es.SurfaceView, es.EventEmitter );
\ No newline at end of file
Index: trunk/extensions/VisualEditor/contentEditable/views/es.ContentView.js
@@ -0,0 +1,236 @@
 2+es.ContentView = function( $container, model ) {
 3+ // Inheritance
 4+ es.EventEmitter.call( this );
 5+
 6+ // Properties
 7+ this.$ = $container;
 8+ this.model = model;
 9+}
 10+
 11+
 12+/* Static Members */
 13+
 14+/**
 15+ * List of annotation rendering implementations.
 16+ *
 17+ * Each supported annotation renderer must have an open and close property, each either a string or
 18+ * a function which accepts a data argument.
 19+ *
 20+ * @static
 21+ * @member
 22+ */
 23+es.ContentView.annotationRenderers = {
 24+ 'object/template': {
 25+ 'open': function( data ) {
 26+ return '<span class="es-contentView-format-object" contentEditable="false">' + data.html;
 27+ },
 28+ 'close': '</span>'
 29+ },
 30+ 'object/hook': {
 31+ 'open': function( data ) {
 32+ return '<span class="es-contentView-format-object">' + data.html;
 33+ },
 34+ 'close': '</span>'
 35+ },
 36+ 'textStyle/bold': {
 37+ //'open': '<span class="es-contentView-format-textStyle-bold">',
 38+ //'close': '</span>'
 39+ 'open': '<b>',
 40+ 'close': '</b>'
 41+ },
 42+ 'textStyle/italic': {
 43+ 'open': '<span class="es-contentView-format-textStyle-italic">',
 44+ 'close': '</span>'
 45+ },
 46+ 'textStyle/strong': {
 47+ 'open': '<span class="es-contentView-format-textStyle-strong">',
 48+ 'close': '</span>'
 49+ },
 50+ 'textStyle/emphasize': {
 51+ 'open': '<span class="es-contentView-format-textStyle-emphasize">',
 52+ 'close': '</span>'
 53+ },
 54+ 'textStyle/big': {
 55+ 'open': '<span class="es-contentView-format-textStyle-big">',
 56+ 'close': '</span>'
 57+ },
 58+ 'textStyle/small': {
 59+ 'open': '<span class="es-contentView-format-textStyle-small">',
 60+ 'close': '</span>'
 61+ },
 62+ 'textStyle/superScript': {
 63+ 'open': '<span class="es-contentView-format-textStyle-superScript">',
 64+ 'close': '</span>'
 65+ },
 66+ 'textStyle/subScript': {
 67+ 'open': '<span class="es-contentView-format-textStyle-subScript">',
 68+ 'close': '</span>'
 69+ },
 70+ 'link/external': {
 71+ 'open': function( data ) {
 72+ return '<span class="es-contentView-format-link" data-href="' + data.href + '">';
 73+ },
 74+ 'close': '</span>'
 75+ },
 76+ 'link/internal': {
 77+ 'open': function( data ) {
 78+ return '<span class="es-contentView-format-link" data-title="wiki/' + data.title + '">';
 79+ },
 80+ 'close': '</span>'
 81+ }
 82+};
 83+
 84+/**
 85+ * Mapping of character and HTML entities or renderings.
 86+ *
 87+ * @static
 88+ * @member
 89+ */
 90+es.ContentView.htmlCharacters = {
 91+ '&': '&amp;',
 92+ '<': '&lt;',
 93+ '>': '&gt;',
 94+ '\'': '&#039;',
 95+ '"': '&quot;',
 96+ '\n': '<span class="es-contentView-whitespace">&#182;</span>',
 97+ '\t': '<span class="es-contentView-whitespace">&#8702;</span>',
 98+ ' ': '&nbsp;'
 99+};
 100+
 101+/* Static Methods */
 102+
 103+/**
 104+ * Gets a rendered opening or closing of an annotation.
 105+ *
 106+ * Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack
 107+ * argument should be used while rendering content.
 108+ *
 109+ * @static
 110+ * @method
 111+ * @param {String} bias Which side of the annotation to render, either "open" or "close"
 112+ * @param {Object} annotation Annotation to render
 113+ * @param {Array} stack List of currently open annotations
 114+ * @returns {String} Rendered annotation
 115+ */
 116+es.ContentView.renderAnnotation = function( bias, annotation, stack ) {
 117+ var renderers = es.ContentView.annotationRenderers,
 118+ type = annotation.type,
 119+ out = '';
 120+ if ( type in renderers ) {
 121+ if ( bias === 'open' ) {
 122+ // Add annotation to the top of the stack
 123+ stack.push( annotation );
 124+ // Open annotation
 125+ out += typeof renderers[type].open === 'function' ?
 126+ renderers[type].open( annotation.data ) : renderers[type].open;
 127+ } else {
 128+ if ( stack[stack.length - 1] === annotation ) {
 129+ // Remove annotation from top of the stack
 130+ stack.pop();
 131+ // Close annotation
 132+ out += typeof renderers[type].close === 'function' ?
 133+ renderers[type].close( annotation.data ) : renderers[type].close;
 134+ } else {
 135+ // Find the annotation in the stack
 136+ var depth = es.inArray( annotation, stack ),
 137+ i;
 138+ if ( depth === -1 ) {
 139+ throw 'Invalid stack error. An element is missing from the stack.';
 140+ }
 141+ // Close each already opened annotation
 142+ for ( i = stack.length - 1; i >= depth + 1; i-- ) {
 143+ out += typeof renderers[stack[i].type].close === 'function' ?
 144+ renderers[stack[i].type].close( stack[i].data ) :
 145+ renderers[stack[i].type].close;
 146+ }
 147+ // Close the buried annotation
 148+ out += typeof renderers[type].close === 'function' ?
 149+ renderers[type].close( annotation.data ) : renderers[type].close;
 150+ // Re-open each previously opened annotation
 151+ for ( i = depth + 1; i < stack.length; i++ ) {
 152+ out += typeof renderers[stack[i].type].open === 'function' ?
 153+ renderers[stack[i].type].open( stack[i].data ) :
 154+ renderers[stack[i].type].open;
 155+ }
 156+ // Remove the annotation from the middle of the stack
 157+ stack.splice( depth, 1 );
 158+ }
 159+ }
 160+ }
 161+ return out;
 162+};
 163+
 164+/* Methods */
 165+
 166+es.ContentView.prototype.render = function( offset ) {
 167+ this.$.html(this.getHtml(0, this.model.getContentLength()));
 168+};
 169+
 170+/**
 171+ * Gets an HTML rendering of a range of data within content model.
 172+ *
 173+ * @method
 174+ * @param {es.Range} range Range of content to render
 175+ * @param {String} Rendered HTML of data within content model
 176+ */
 177+es.ContentView.prototype.getHtml = function( range, options ) {
 178+ if ( range ) {
 179+ range.normalize();
 180+ } else {
 181+ range = { 'start': 0, 'end': undefined };
 182+ }
 183+ var data = this.model.getContentData(),
 184+ render = es.ContentView.renderAnnotation,
 185+ htmlChars = es.ContentView.htmlCharacters;
 186+ var out = '',
 187+ left = '',
 188+ right,
 189+ leftPlain,
 190+ rightPlain,
 191+ stack = [],
 192+ chr,
 193+ i,
 194+ j;
 195+ for ( i = 0; i < data.length; i++ ) {
 196+ right = data[i];
 197+ leftPlain = typeof left === 'string';
 198+ rightPlain = typeof right === 'string';
 199+ if ( !leftPlain && rightPlain ) {
 200+ // [formatted][plain] pair, close any annotations for left
 201+ for ( j = 1; j < left.length; j++ ) {
 202+ out += render( 'close', left[j], stack );
 203+ }
 204+ } else if ( leftPlain && !rightPlain ) {
 205+ // [plain][formatted] pair, open any annotations for right
 206+ for ( j = 1; j < right.length; j++ ) {
 207+ out += render( 'open', right[j], stack );
 208+ }
 209+ } else if ( !leftPlain && !rightPlain ) {
 210+ // [formatted][formatted] pair, open/close any differences
 211+ for ( j = 1; j < left.length; j++ ) {
 212+ if ( es.inArray( left[j], right ) === -1 ) {
 213+ out += render( 'close', left[j], stack );
 214+ }
 215+ }
 216+ for ( j = 1; j < right.length; j++ ) {
 217+ if ( es.inArray( right[j], left ) === -1 ) {
 218+ out += render( 'open', right[j], stack );
 219+ }
 220+ }
 221+ }
 222+ chr = rightPlain ? right : right[0];
 223+ out += chr in htmlChars ? htmlChars[chr] : chr;
 224+ left = right;
 225+ }
 226+ // Close all remaining tags at the end of the content
 227+ if ( !rightPlain && right ) {
 228+ for ( j = 1; j < right.length; j++ ) {
 229+ out += render( 'close', right[j], stack );
 230+ }
 231+ }
 232+ return out;
 233+};
 234+
 235+/* Inheritance */
 236+
 237+es.extendClass( es.ContentView, es.EventEmitter );
Index: trunk/extensions/VisualEditor/contentEditable/views/es.DocumentViewLeafNode.js
@@ -0,0 +1,91 @@
 2+/**
 3+ * Creates an es.DocumentViewLeafNode object.
 4+ *
 5+ * @class
 6+ * @abstract
 7+ * @constructor
 8+ * @extends {es.DocumentLeafNode}
 9+ * @extends {es.DocumentViewNode}
 10+ * @param model {es.ModelNode} Model to observe
 11+ * @param {jQuery} [$element] Element to use as a container
 12+ */
 13+es.DocumentViewLeafNode = function( model, $element ) {
 14+ // Inheritance
 15+ es.DocumentLeafNode.call( this );
 16+ es.DocumentViewNode.call( this, model, $element );
 17+
 18+ this.$.data('view', this);
 19+
 20+ // Properties
 21+ this.$content = this.$;
 22+ this.contentView = new es.ContentView( this.$content, model );
 23+
 24+ // Events
 25+ this.contentView.on( 'update', this.emitUpdate );
 26+};
 27+
 28+/* Methods */
 29+
 30+/**
 31+ * Render content.
 32+ *
 33+ * @method
 34+ */
 35+es.DocumentViewLeafNode.prototype.renderContent = function() {
 36+ this.contentView.render();
 37+};
 38+
 39+/**
 40+ * Draw selection around a given range.
 41+ *
 42+ * @method
 43+ * @param {es.Range} range Range of content to draw selection around
 44+ */
 45+es.DocumentViewLeafNode.prototype.drawSelection = function( range ) {
 46+ this.contentView.drawSelection( range );
 47+};
 48+
 49+/**
 50+ * Clear selection.
 51+ *
 52+ * @method
 53+ */
 54+es.DocumentViewLeafNode.prototype.clearSelection = function() {
 55+ this.contentView.clearSelection();
 56+};
 57+
 58+/**
 59+ * Gets the nearest offset of a rendered position.
 60+ *
 61+ * @method
 62+ * @param {es.Position} position Position to get offset for
 63+ * @returns {Integer} Offset of position
 64+ */
 65+es.DocumentViewLeafNode.prototype.getOffsetFromRenderedPosition = function( position ) {
 66+ return this.contentView.getOffsetFromRenderedPosition( position );
 67+};
 68+
 69+/**
 70+ * Gets rendered position of offset within content.
 71+ *
 72+ * @method
 73+ * @param {Integer} offset Offset to get position for
 74+ * @returns {es.Position} Position of offset
 75+ */
 76+es.DocumentViewLeafNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
 77+ var position = this.contentView.getRenderedPositionFromOffset( offset, leftBias ),
 78+ contentPosition = this.$content.offset();
 79+ position.top += contentPosition.top;
 80+ position.left += contentPosition.left;
 81+ position.bottom += contentPosition.top;
 82+ return position;
 83+};
 84+
 85+es.DocumentViewLeafNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
 86+ return this.contentView.getRenderedLineRangeFromOffset( offset );
 87+};
 88+
 89+/* Inheritance */
 90+
 91+es.extendClass( es.DocumentViewLeafNode, es.DocumentLeafNode );
 92+es.extendClass( es.DocumentViewLeafNode, es.DocumentViewNode );
Index: trunk/extensions/VisualEditor/contentEditable/views/es.DocumentView.js
@@ -0,0 +1,72 @@
 2+/**
 3+ * Creates an es.DocumentView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewBranchNode}
 8+ * @param {es.DocumentModel} documentModel Document model to view
 9+ * @param {es.SurfaceView} surfaceView Surface view this view is a child of
 10+ */
 11+es.DocumentView = function( model, surfaceView ) {
 12+ // Inheritance
 13+ es.DocumentViewBranchNode.call( this, model );
 14+
 15+ // Properties
 16+ this.surfaceView = surfaceView;
 17+
 18+ // DOM Changes
 19+ this.$.addClass( 'es-documentView' );
 20+ this.$.attr('contentEditable', 'true');
 21+};
 22+
 23+/* Static Members */
 24+
 25+
 26+/**
 27+ * Mapping of symbolic names and splitting rules.
 28+ *
 29+ * Each rule is an object with a self and children property. Each of these properties may contain
 30+ * one of two possible values:
 31+ * Boolean - Whether a split is allowed
 32+ * Null - Node is a leaf, so there's nothing to split
 33+ *
 34+ * @example Paragraph rules
 35+ * {
 36+ * 'self': true
 37+ * 'children': null
 38+ * }
 39+ * @example List rules
 40+ * {
 41+ * 'self': false,
 42+ * 'children': true
 43+ * }
 44+ * @example ListItem rules
 45+ * {
 46+ * 'self': true,
 47+ * 'children': false
 48+ * }
 49+ */
 50+es.DocumentView.splitRules = {};
 51+
 52+/* Methods */
 53+
 54+/**
 55+ * Get the document offset of a position created from passed DOM event
 56+ *
 57+ * @method
 58+ * @param e {Event} Event to create es.Position from
 59+ * @returns {Integer} Document offset
 60+ */
 61+es.DocumentView.prototype.getOffsetFromEvent = function( e ) {
 62+ var position = es.Position.newFromEventPagePosition( e );
 63+ return this.getOffsetFromRenderedPosition( position );
 64+};
 65+
 66+es.DocumentView.splitRules.document = {
 67+ 'self': false,
 68+ 'children': true
 69+};
 70+
 71+/* Inheritance */
 72+
 73+es.extendClass( es.DocumentView, es.DocumentViewBranchNode );
Index: trunk/extensions/VisualEditor/contentEditable/diff_match_patch.js
@@ -0,0 +1,49 @@
 2+(function(){function diff_match_patch(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=0.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=0.5;this.Patch_Margin=4;this.Match_MaxBits=32}
 3+diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[[0,a]]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b),c=a.substring(0,f),a=a.substring(f),b=b.substring(f),f=this.diff_commonSuffix(a,b),g=a.substring(a.length-f),a=a.substring(0,a.length-f),b=b.substring(0,b.length-f),a=this.diff_compute_(a,
 4+b,e,d);c&&a.unshift([0,c]);g&&a.push([0,g]);this.diff_cleanupMerge(a);return a};
 5+diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[1,b]];if(!b)return[[-1,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);if(-1!=g)return c=[[1,e.substring(0,g)],[0,f],[1,e.substring(g+f.length)]],a.length>b.length&&(c[0][0]=c[2][0]=-1),c;if(1==f.length)return[[-1,a],[1,b]];return(e=this.diff_halfMatch_(a,b))?(f=e[0],a=e[1],g=e[2],b=e[3],e=e[4],f=this.diff_main(f,g,c,d),c=this.diff_main(a,b,c,d),f.concat([[0,e]],c)):c&&100<a.length&&100<b.length?this.diff_lineMode_(a,
 6+b,d):this.diff_bisect_(a,b,d)};
 7+diff_match_patch.prototype.diff_lineMode_=function(a,b,c){var d=this.diff_linesToChars_(a,b),a=d.chars1,b=d.chars2,d=d.lineArray,a=this.diff_main(a,b,!1,c);this.diff_charsToLines_(a,d);this.diff_cleanupSemantic(a);a.push([0,""]);for(var e=d=b=0,f="",g="";b<a.length;){switch(a[b][0]){case 1:e++;g+=a[b][1];break;case -1:d++;f+=a[b][1];break;case 0:if(1<=d&&1<=e){a.splice(b-d-e,d+e);b=b-d-e;d=this.diff_main(f,g,!1,c);for(e=d.length-1;0<=e;e--)a.splice(b,0,d[e]);b+=d.length}d=e=0;g=f=""}b++}a.pop();return a};
 8+diff_match_patch.prototype.diff_bisect_=function(a,b,c){for(var d=a.length,e=b.length,f=Math.ceil((d+e)/2),g=f,h=2*f,j=Array(h),i=Array(h),k=0;k<h;k++)j[k]=-1,i[k]=-1;j[g+1]=0;i[g+1]=0;for(var k=d-e,p=0!=k%2,q=0,s=0,o=0,v=0,u=0;u<f&&!((new Date).getTime()>c);u++){for(var n=-u+q;n<=u-s;n+=2){var l=g+n,m;m=n==-u||n!=u&&j[l-1]<j[l+1]?j[l+1]:j[l-1]+1;for(var r=m-n;m<d&&r<e&&a.charAt(m)==b.charAt(r);)m++,r++;j[l]=m;if(m>d)s+=2;else if(r>e)q+=2;else if(p&&(l=g+k-n,0<=l&&l<h&&-1!=i[l])){var t=d-i[l];if(m>=
 9+t)return this.diff_bisectSplit_(a,b,m,r,c)}}for(n=-u+o;n<=u-v;n+=2){l=g+n;t=n==-u||n!=u&&i[l-1]<i[l+1]?i[l+1]:i[l-1]+1;for(m=t-n;t<d&&m<e&&a.charAt(d-t-1)==b.charAt(e-m-1);)t++,m++;i[l]=t;if(t>d)v+=2;else if(m>e)o+=2;else if(!p&&(l=g+k-n,0<=l&&l<h&&-1!=j[l]&&(m=j[l],r=g+m-l,t=d-t,m>=t)))return this.diff_bisectSplit_(a,b,m,r,c)}}return[[-1,a],[1,b]]};
 10+diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d),a=a.substring(c),b=b.substring(d),f=this.diff_main(f,g,!1,e),e=this.diff_main(a,b,!1,e);return f.concat(e)};
 11+diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,f=-1,g=d.length;f<a.length-1;){f=a.indexOf("\n",c);-1==f&&(f=a.length-1);var q=a.substring(c,f+1),c=f+1;(e.hasOwnProperty?e.hasOwnProperty(q):void 0!==e[q])?b+=String.fromCharCode(e[q]):(b+=String.fromCharCode(g),e[q]=g,d[g++]=q)}return b}var d=[],e={};d[0]="";var f=c(a),g=c(b);return{chars1:f,chars2:g,lineArray:d}};
 12+diff_match_patch.prototype.diff_charsToLines_=function(a,b){for(var c=0;c<a.length;c++){for(var d=a[c][1],e=[],f=0;f<d.length;f++)e[f]=b[d.charCodeAt(f)];a[c][1]=e.join("")}};diff_match_patch.prototype.diff_commonPrefix=function(a,b){if(!a||!b||a.charAt(0)!=b.charAt(0))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;)a.substring(f,e)==b.substring(f,e)?f=c=e:d=e,e=Math.floor((d-c)/2+c);return e};
 13+diff_match_patch.prototype.diff_commonSuffix=function(a,b){if(!a||!b||a.charAt(a.length-1)!=b.charAt(b.length-1))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;)a.substring(a.length-e,a.length-f)==b.substring(b.length-e,b.length-f)?f=c=e:d=e,e=Math.floor((d-c)/2+c);return e};
 14+diff_match_patch.prototype.diff_commonOverlap_=function(a,b){var c=a.length,d=b.length;if(0==c||0==d)return 0;c>d?a=a.substring(c-d):c<d&&(b=b.substring(0,c));c=Math.min(c,d);if(a==b)return c;for(var d=0,e=1;;){var f=a.substring(c-e),f=b.indexOf(f);if(-1==f)return d;e+=f;if(0==f||a.substring(c-e)==b.substring(0,e))d=e,e++}};
 15+diff_match_patch.prototype.diff_halfMatch_=function(a,b){function c(a,b,c){for(var d=a.substring(c,c+Math.floor(a.length/4)),e=-1,g="",h,j,n,l;-1!=(e=b.indexOf(d,e+1));){var m=f.diff_commonPrefix(a.substring(c),b.substring(e)),r=f.diff_commonSuffix(a.substring(0,c),b.substring(0,e));g.length<r+m&&(g=b.substring(e-r,e)+b.substring(e,e+m),h=a.substring(0,c-r),j=a.substring(c+m),n=b.substring(0,e-r),l=b.substring(e+m))}return 2*g.length>=a.length?[h,j,n,l,g]:null}if(0>=this.Diff_Timeout)return null;
 16+var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.length<d.length)return null;var f=this,g=c(d,e,Math.ceil(d.length/4)),d=c(d,e,Math.ceil(d.length/2)),h;if(!g&&!d)return null;h=d?g?g[4].length>d[4].length?g:d:d:g;var j;a.length>b.length?(g=h[0],d=h[1],e=h[2],j=h[3]):(e=h[0],j=h[1],g=h[2],d=h[3]);h=h[4];return[g,d,e,j,h]};
 17+diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,j=0,i=0;f<a.length;)0==a[f][0]?(c[d++]=f,g=j,h=i,i=j=0,e=a[f][1]):(1==a[f][0]?j+=a[f][1].length:i+=a[f][1].length,e&&e.length<=Math.max(g,h)&&e.length<=Math.max(j,i)&&(a.splice(c[d-1],0,[-1,e]),a[c[d-1]+1][0]=1,d--,d--,f=0<d?c[d-1]:-1,i=j=h=g=0,e=null,b=!0)),f++;b&&this.diff_cleanupMerge(a);this.diff_cleanupSemanticLossless(a);for(f=1;f<a.length;){if(-1==a[f-1][0]&&1==a[f][0]){b=a[f-1][1];c=a[f][1];
 18+d=this.diff_commonOverlap_(b,c);e=this.diff_commonOverlap_(c,b);if(d>=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,[0,c.substring(0,d)]),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,[0,b.substring(0,e)]),a[f-1][0]=1,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=-1,a[f+1][1]=b.substring(e),f++;f++}f++}};
 19+diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_),c=g&&c.match(diff_match_patch.linebreakRegex_),d=h&&d.match(diff_match_patch.linebreakRegex_),i=c&&a.match(diff_match_patch.blanklineEndRegex_),j=d&&b.match(diff_match_patch.blanklineStartRegex_);
 20+return i||j?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c<a.length-1;){if(0==a[c-1][0]&&0==a[c+1][0]){var d=a[c-1][1],e=a[c][1],f=a[c+1][1],g=this.diff_commonSuffix(d,e);if(g)var h=e.substring(e.length-g),d=d.substring(0,d.length-g),e=h+e.substring(0,e.length-g),f=h+f;for(var g=d,h=e,j=f,i=b(d,e)+b(e,f);e.charAt(0)===f.charAt(0);){var d=d+e.charAt(0),e=e.substring(1)+f.charAt(0),f=f.substring(1),k=b(d,e)+b(e,f);k>=i&&(i=k,g=d,h=e,j=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-1,1),c--),a[c][1]=
 21+h,j?a[c+1][1]=j:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/;
 22+diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,j=!1,i=!1;f<a.length;){if(0==a[f][0])a[f][1].length<this.Diff_EditCost&&(j||i)?(c[d++]=f,g=j,h=i,e=a[f][1]):(d=0,e=null),j=i=!1;else if(-1==a[f][0]?i=!0:j=!0,e&&(g&&h&&j&&i||e.length<this.Diff_EditCost/2&&3==g+h+j+i))a.splice(c[d-1],0,[-1,e]),a[c[d-1]+1][0]=1,d--,e=null,g&&h?(j=i=!0,d=0):(d--,f=0<d?c[d-1]:-1,j=i=!1),b=!0;f++}b&&this.diff_cleanupMerge(a)};
 23+diff_match_patch.prototype.diff_cleanupMerge=function(a){a.push([0,""]);for(var b=0,c=0,d=0,e="",f="",g;b<a.length;)switch(a[b][0]){case 1:d++;f+=a[b][1];b++;break;case -1:c++;e+=a[b][1];b++;break;case 0:1<c+d?(0!==c&&0!==d&&(g=this.diff_commonPrefix(f,e),0!==g&&(0<b-c-d&&0==a[b-c-d-1][0]?a[b-c-d-1][1]+=f.substring(0,g):(a.splice(0,0,[0,f.substring(0,g)]),b++),f=f.substring(g),e=e.substring(g)),g=this.diff_commonSuffix(f,e),0!==g&&(a[b][1]=f.substring(f.length-g)+a[b][1],f=f.substring(0,f.length-
 24+g),e=e.substring(0,e.length-g))),0===c?a.splice(b-d,c+d,[1,f]):0===d?a.splice(b-c,c+d,[-1,e]):a.splice(b-c-d,c+d,[-1,e],[1,f]),b=b-c-d+(c?1:0)+(d?1:0)+1):0!==b&&0==a[b-1][0]?(a[b-1][1]+=a[b][1],a.splice(b,1)):b++,c=d=0,f=e=""}""===a[a.length-1][1]&&a.pop();c=!1;for(b=1;b<a.length-1;)0==a[b-1][0]&&0==a[b+1][0]&&(a[b][1].substring(a[b][1].length-a[b-1][1].length)==a[b-1][1]?(a[b][1]=a[b-1][1]+a[b][1].substring(0,a[b][1].length-a[b-1][1].length),a[b+1][1]=a[b-1][1]+a[b+1][1],a.splice(b-1,1),c=!0):a[b][1].substring(0,
 25+a[b+1][1].length)==a[b+1][1]&&(a[b-1][1]+=a[b+1][1],a[b][1]=a[b][1].substring(a[b+1][1].length)+a[b+1][1],a.splice(b+1,1),c=!0)),b++;c&&this.diff_cleanupMerge(a)};diff_match_patch.prototype.diff_xIndex=function(a,b){var c=0,d=0,e=0,f=0,g;for(g=0;g<a.length;g++){1!==a[g][0]&&(c+=a[g][1].length);-1!==a[g][0]&&(d+=a[g][1].length);if(c>b)break;e=c;f=d}return a.length!=g&&-1===a[g][0]?f:f+(b-e)};
 26+diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=/</g,e=/>/g,f=/\n/g,g=0;g<a.length;g++){var h=a[g][0],j=a[g][1],j=j.replace(c,"&amp;").replace(d,"&lt;").replace(e,"&gt;").replace(f,"&para;<br>");switch(h){case 1:b[g]='<ins style="background:#e6ffe6;">'+j+"</ins>";break;case -1:b[g]='<del style="background:#ffe6e6;">'+j+"</del>";break;case 0:b[g]="<span>"+j+"</span>"}}return b.join("")};
 27+diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;c<a.length;c++)1!==a[c][0]&&(b[c]=a[c][1]);return b.join("")};diff_match_patch.prototype.diff_text2=function(a){for(var b=[],c=0;c<a.length;c++)-1!==a[c][0]&&(b[c]=a[c][1]);return b.join("")};diff_match_patch.prototype.diff_levenshtein=function(a){for(var b=0,c=0,d=0,e=0;e<a.length;e++){var f=a[e][0],g=a[e][1];switch(f){case 1:c+=g.length;break;case -1:d+=g.length;break;case 0:b+=Math.max(c,d),d=c=0}}return b+=Math.max(c,d)};
 28+diff_match_patch.prototype.diff_toDelta=function(a){for(var b=[],c=0;c<a.length;c++)switch(a[c][0]){case 1:b[c]="+"+encodeURI(a[c][1]);break;case -1:b[c]="-"+a[c][1].length;break;case 0:b[c]="="+a[c][1].length}return b.join("\t").replace(/%20/g," ")};
 29+diff_match_patch.prototype.diff_fromDelta=function(a,b){for(var c=[],d=0,e=0,f=b.split(/\t/g),g=0;g<f.length;g++){var h=f[g].substring(1);switch(f[g].charAt(0)){case "+":try{c[d++]=[1,decodeURI(h)]}catch(j){throw Error("Illegal escape in diff_fromDelta: "+h);}break;case "-":case "=":var i=parseInt(h,10);if(isNaN(i)||0>i)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=i);"="==f[g].charAt(0)?c[d++]=[0,h]:c[d++]=[-1,h];break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+
 30+f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
 31+diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return!f.Match_Distance?g?1:e:e+g/f.Match_Distance}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));for(var j=1<<b.length-1,h=-1,i,k,p=b.length+a.length,q,s=0;s<b.length;s++){i=0;for(k=p;i<k;)d(s,c+
 32+k)<=g?i=k:p=k,k=Math.floor((p-i)/2+i);p=k;i=Math.max(1,c-k+1);var o=Math.min(c+k,a.length)+b.length;k=Array(o+2);for(k[o+1]=(1<<s)-1;o>=i;o--){var v=e[a.charAt(o-1)];k[o]=0===s?(k[o+1]<<1|1)&v:(k[o+1]<<1|1)&v|(q[o+1]|q[o])<<1|1|q[o+1];if(k[o]&j&&(v=d(s,o-1),v<=g))if(g=v,h=o-1,h>c)i=Math.max(1,2*c-h);else break}if(d(s+1,c)>g)break;q=k}return h};
 33+diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c<a.length;c++)b[a.charAt(c)]=0;for(c=0;c<a.length;c++)b[a.charAt(c)]|=1<<a.length-c-1;return b};
 34+diff_match_patch.prototype.patch_addContext_=function(a,b){if(0!=b.length){for(var c=b.substring(a.start2,a.start2+a.length1),d=0;b.indexOf(c)!=b.lastIndexOf(c)&&c.length<this.Match_MaxBits-this.Patch_Margin-this.Patch_Margin;)d+=this.Patch_Margin,c=b.substring(a.start2-d,a.start2+a.length1+d);d+=this.Patch_Margin;(c=b.substring(a.start2-d,a.start2))&&a.diffs.unshift([0,c]);(d=b.substring(a.start2+a.length1,a.start2+a.length1+d))&&a.diffs.push([0,d]);a.start1-=c.length;a.start2-=c.length;a.length1+=
 35+c.length+d.length;a.length2+=c.length+d.length}};
 36+diff_match_patch.prototype.patch_make=function(a,b,c){var d;if("string"==typeof a&&"string"==typeof b&&"undefined"==typeof c)d=a,b=this.diff_main(d,b,!0),2<b.length&&(this.diff_cleanupSemantic(b),this.diff_cleanupEfficiency(b));else if(a&&"object"==typeof a&&"undefined"==typeof b&&"undefined"==typeof c)b=a,d=this.diff_text1(b);else if("string"==typeof a&&b&&"object"==typeof b&&"undefined"==typeof c)d=a;else if("string"==typeof a&&"string"==typeof b&&c&&"object"==typeof c)d=a,b=c;else throw Error("Unknown call format to patch_make.");
 37+if(0===b.length)return[];for(var c=[],a=new diff_match_patch.patch_obj,e=0,f=0,g=0,h=d,j=0;j<b.length;j++){var i=b[j][0],k=b[j][1];!e&&0!==i&&(a.start1=f,a.start2=g);switch(i){case 1:a.diffs[e++]=b[j];a.length2+=k.length;d=d.substring(0,g)+k+d.substring(g);break;case -1:a.length1+=k.length;a.diffs[e++]=b[j];d=d.substring(0,g)+d.substring(g+k.length);break;case 0:k.length<=2*this.Patch_Margin&&e&&b.length!=j+1?(a.diffs[e++]=b[j],a.length1+=k.length,a.length2+=k.length):k.length>=2*this.Patch_Margin&&
 38+e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}1!==i&&(f+=k.length);-1!==i&&(g+=k.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;c<a.length;c++){var d=a[c],e=new diff_match_patch.patch_obj;e.diffs=[];for(var f=0;f<d.diffs.length;f++)e.diffs[f]=d.diffs[f].slice();e.start1=d.start1;e.start2=d.start2;e.length1=d.length1;e.length2=d.length2;b[c]=e}return b};
 39+diff_match_patch.prototype.patch_apply=function(a,b){if(0==a.length)return[b,[]];var a=this.patch_deepCopy(a),c=this.patch_addPadding(a),b=c+b+c;this.patch_splitMax(a);for(var d=0,e=[],f=0;f<a.length;f++){var g=a[f].start2+d,h=this.diff_text1(a[f].diffs),j,i=-1;if(h.length>this.Match_MaxBits){if(j=this.match_main(b,h.substring(0,this.Match_MaxBits),g),-1!=j&&(i=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==i||j>=i))j=-1}else j=this.match_main(b,h,g);
 40+if(-1==j)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=j-g,g=-1==i?b.substring(j,j+h.length):b.substring(j,i+this.Match_MaxBits),h==g)b=b.substring(0,j)+this.diff_text2(a[f].diffs)+b.substring(j+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);for(var h=0,k,i=0;i<a[f].diffs.length;i++){var p=a[f].diffs[i];0!==p[0]&&(k=this.diff_xIndex(g,h));1===p[0]?b=b.substring(0,
 41+j+k)+p[1]+b.substring(j+k):-1===p[0]&&(b=b.substring(0,j+k)+b.substring(j+this.diff_xIndex(g,h+p[1].length)));-1!==p[0]&&(h+=p[1].length)}}}b=b.substring(c.length,b.length-c.length);return[b,e]};
 42+diff_match_patch.prototype.patch_addPadding=function(a){for(var b=this.Patch_Margin,c="",d=1;d<=b;d++)c+=String.fromCharCode(d);for(d=0;d<a.length;d++)a[d].start1+=b,a[d].start2+=b;var d=a[0],e=d.diffs;if(0==e.length||0!=e[0][0])e.unshift([0,c]),d.start1-=b,d.start2-=b,d.length1+=b,d.length2+=b;else if(b>e[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;0==e.length||0!=e[e.length-1][0]?(e.push([0,
 43+c]),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c};
 44+diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c<a.length;c++)if(!(a[c].length1<=b)){var d=a[c];a.splice(c--,1);for(var e=d.start1,f=d.start2,g="";0!==d.diffs.length;){var h=new diff_match_patch.patch_obj,j=!0;h.start1=e-g.length;h.start2=f-g.length;""!==g&&(h.length1=h.length2=g.length,h.diffs.push([0,g]));for(;0!==d.diffs.length&&h.length1<b-this.Patch_Margin;){var g=d.diffs[0][0],i=d.diffs[0][1];1===g?(h.length2+=i.length,f+=i.length,h.diffs.push(d.diffs.shift()),
 45+j=!1):-1===g&&1==h.diffs.length&&0==h.diffs[0][0]&&i.length>2*b?(h.length1+=i.length,e+=i.length,j=!1,h.diffs.push([g,i]),d.diffs.shift()):(i=i.substring(0,b-h.length1-this.Patch_Margin),h.length1+=i.length,e+=i.length,0===g?(h.length2+=i.length,f+=i.length):j=!1,h.diffs.push([g,i]),i==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(i.length))}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);i=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==i&&
 46+(h.length1+=i.length,h.length2+=i.length,0!==h.diffs.length&&0===h.diffs[h.diffs.length-1][0]?h.diffs[h.diffs.length-1][1]+=i:h.diffs.push([0,i]));j||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c<a.length;c++)b[c]=a[c];return b.join("")};
 47+diff_match_patch.prototype.patch_fromText=function(a){var b=[];if(!a)return b;for(var a=a.split("\n"),c=0,d=/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/;c<a.length;){var e=a[c].match(d);if(!e)throw Error("Invalid patch string: "+a[c]);var f=new diff_match_patch.patch_obj;b.push(f);f.start1=parseInt(e[1],10);""===e[2]?(f.start1--,f.length1=1):"0"==e[2]?f.length1=0:(f.start1--,f.length1=parseInt(e[2],10));f.start2=parseInt(e[3],10);""===e[4]?(f.start2--,f.length2=1):"0"==e[4]?f.length2=0:(f.start2--,f.length2=
 48+parseInt(e[4],10));for(c++;c<a.length;){e=a[c].charAt(0);try{var g=decodeURI(a[c].substring(1))}catch(h){throw Error("Illegal escape in patch_fromText: "+g);}if("-"==e)f.diffs.push([-1,g]);else if("+"==e)f.diffs.push([1,g]);else if(" "==e)f.diffs.push([0,g]);else if("@"==e)break;else if(""!==e)throw Error('Invalid patch mode "'+e+'" in: '+g);c++}}return b};diff_match_patch.patch_obj=function(){this.diffs=[];this.start2=this.start1=null;this.length2=this.length1=0};
 49+diff_match_patch.patch_obj.prototype.toString=function(){var a,b;a=0===this.length1?this.start1+",0":1==this.length1?this.start1+1:this.start1+1+","+this.length1;b=0===this.length2?this.start2+",0":1==this.length2?this.start2+1:this.start2+1+","+this.length2;a=["@@ -"+a+" +"+b+" @@\n"];var c;for(b=0;b<this.diffs.length;b++){switch(this.diffs[b][0]){case 1:c="+";break;case -1:c="-";break;case 0:c=" "}a[b+1]=c+encodeURI(this.diffs[b][1])+"\n"}return a.join("").replace(/%20/g," ")};
 50+this.diff_match_patch=diff_match_patch;this.DIFF_DELETE=-1;this.DIFF_INSERT=1;this.DIFF_EQUAL=0;})()

Comments

#Comment by Johnduhart (talk | contribs)   05:06, 26 January 2012
#Comment by 😂 (talk | contribs)   22:16, 26 January 2012

Status & tagging log