dom/imptests/editing/implementation.js

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 "use strict";
     3 var htmlNamespace = "http://www.w3.org/1999/xhtml";
     5 var cssStylingFlag = false;
     7 var defaultSingleLineContainerName = "p";
     9 // This is bad :(
    10 var globalRange = null;
    12 // Commands are stored in a dictionary where we call their actions and such
    13 var commands = {};
    15 ///////////////////////////////////////////////////////////////////////////////
    16 ////////////////////////////// Utility functions //////////////////////////////
    17 ///////////////////////////////////////////////////////////////////////////////
    18 //@{
    20 function nextNode(node) {
    21 	if (node.hasChildNodes()) {
    22 		return node.firstChild;
    23 	}
    24 	return nextNodeDescendants(node);
    25 }
    27 function previousNode(node) {
    28 	if (node.previousSibling) {
    29 		node = node.previousSibling;
    30 		while (node.hasChildNodes()) {
    31 			node = node.lastChild;
    32 		}
    33 		return node;
    34 	}
    35 	if (node.parentNode
    36 	&& node.parentNode.nodeType == Node.ELEMENT_NODE) {
    37 		return node.parentNode;
    38 	}
    39 	return null;
    40 }
    42 function nextNodeDescendants(node) {
    43 	while (node && !node.nextSibling) {
    44 		node = node.parentNode;
    45 	}
    46 	if (!node) {
    47 		return null;
    48 	}
    49 	return node.nextSibling;
    50 }
    52 /**
    53  * Returns true if ancestor is an ancestor of descendant, false otherwise.
    54  */
    55 function isAncestor(ancestor, descendant) {
    56 	return ancestor
    57 		&& descendant
    58 		&& Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
    59 }
    61 /**
    62  * Returns true if ancestor is an ancestor of or equal to descendant, false
    63  * otherwise.
    64  */
    65 function isAncestorContainer(ancestor, descendant) {
    66 	return (ancestor || descendant)
    67 		&& (ancestor == descendant || isAncestor(ancestor, descendant));
    68 }
    70 /**
    71  * Returns true if descendant is a descendant of ancestor, false otherwise.
    72  */
    73 function isDescendant(descendant, ancestor) {
    74 	return ancestor
    75 		&& descendant
    76 		&& Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY);
    77 }
    79 /**
    80  * Returns true if node1 is before node2 in tree order, false otherwise.
    81  */
    82 function isBefore(node1, node2) {
    83 	return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING);
    84 }
    86 /**
    87  * Returns true if node1 is after node2 in tree order, false otherwise.
    88  */
    89 function isAfter(node1, node2) {
    90 	return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING);
    91 }
    93 function getAncestors(node) {
    94 	var ancestors = [];
    95 	while (node.parentNode) {
    96 		ancestors.unshift(node.parentNode);
    97 		node = node.parentNode;
    98 	}
    99 	return ancestors;
   100 }
   102 function getInclusiveAncestors(node) {
   103 	return getAncestors(node).concat(node);
   104 }
   106 function getDescendants(node) {
   107 	var descendants = [];
   108 	var stop = nextNodeDescendants(node);
   109 	while ((node = nextNode(node))
   110 	&& node != stop) {
   111 		descendants.push(node);
   112 	}
   113 	return descendants;
   114 }
   116 function getInclusiveDescendants(node) {
   117 	return [node].concat(getDescendants(node));
   118 }
   120 function convertProperty(property) {
   121 	// Special-case for now
   122 	var map = {
   123 		"fontFamily": "font-family",
   124 		"fontSize": "font-size",
   125 		"fontStyle": "font-style",
   126 		"fontWeight": "font-weight",
   127 		"textDecoration": "text-decoration",
   128 	};
   129 	if (typeof map[property] != "undefined") {
   130 		return map[property];
   131 	}
   133 	return property;
   134 }
   136 // Return the <font size=X> value for the given CSS size, or undefined if there
   137 // is none.
   138 function cssSizeToLegacy(cssVal) {
   139 	return {
   140 		"x-small": 1,
   141 		"small": 2,
   142 		"medium": 3,
   143 		"large": 4,
   144 		"x-large": 5,
   145 		"xx-large": 6,
   146 		"xxx-large": 7
   147 	}[cssVal];
   148 }
   150 // Return the CSS size given a legacy size.
   151 function legacySizeToCss(legacyVal) {
   152 	return {
   153 		1: "x-small",
   154 		2: "small",
   155 		3: "medium",
   156 		4: "large",
   157 		5: "x-large",
   158 		6: "xx-large",
   159 		7: "xxx-large",
   160 	}[legacyVal];
   161 }
   163 // Opera 11 puts HTML elements in the null namespace, it seems.
   164 function isHtmlNamespace(ns) {
   165 	return ns === null
   166 		|| ns === htmlNamespace;
   167 }
   169 // "the directionality" from HTML.  I don't bother caring about non-HTML
   170 // elements.
   171 //
   172 // "The directionality of an element is either 'ltr' or 'rtl', and is
   173 // determined as per the first appropriate set of steps from the following
   174 // list:"
   175 function getDirectionality(element) {
   176 	// "If the element's dir attribute is in the ltr state
   177 	//     The directionality of the element is 'ltr'."
   178 	if (element.dir == "ltr") {
   179 		return "ltr";
   180 	}
   182 	// "If the element's dir attribute is in the rtl state
   183 	//     The directionality of the element is 'rtl'."
   184 	if (element.dir == "rtl") {
   185 		return "rtl";
   186 	}
   188 	// "If the element's dir attribute is in the auto state
   189 	// "If the element is a bdi element and the dir attribute is not in a
   190 	// defined state (i.e. it is not present or has an invalid value)
   191 	//     [lots of complicated stuff]
   192 	//
   193 	// Skip this, since no browser implements it anyway.
   195 	// "If the element is a root element and the dir attribute is not in a
   196 	// defined state (i.e. it is not present or has an invalid value)
   197 	//     The directionality of the element is 'ltr'."
   198 	if (!isHtmlElement(element.parentNode)) {
   199 		return "ltr";
   200 	}
   202 	// "If the element has a parent element and the dir attribute is not in a
   203 	// defined state (i.e. it is not present or has an invalid value)
   204 	//     The directionality of the element is the same as the element's
   205 	//     parent element's directionality."
   206 	return getDirectionality(element.parentNode);
   207 }
   209 //@}
   211 ///////////////////////////////////////////////////////////////////////////////
   212 ///////////////////////////// DOM Range functions /////////////////////////////
   213 ///////////////////////////////////////////////////////////////////////////////
   214 //@{
   216 function getNodeIndex(node) {
   217 	var ret = 0;
   218 	while (node.previousSibling) {
   219 		ret++;
   220 		node = node.previousSibling;
   221 	}
   222 	return ret;
   223 }
   225 // "The length of a Node node is the following, depending on node:
   226 //
   227 // ProcessingInstruction
   228 // DocumentType
   229 //   Always 0.
   230 // Text
   231 // Comment
   232 //   node's length.
   233 // Any other node
   234 //   node's childNodes's length."
   235 function getNodeLength(node) {
   236 	switch (node.nodeType) {
   237 		case Node.PROCESSING_INSTRUCTION_NODE:
   238 		case Node.DOCUMENT_TYPE_NODE:
   239 			return 0;
   241 		case Node.TEXT_NODE:
   242 		case Node.COMMENT_NODE:
   243 			return node.length;
   245 		default:
   246 			return node.childNodes.length;
   247 	}
   248 }
   250 /**
   251  * The position of two boundary points relative to one another, as defined by
   252  * DOM Range.
   253  */
   254 function getPosition(nodeA, offsetA, nodeB, offsetB) {
   255 	// "If node A is the same as node B, return equal if offset A equals offset
   256 	// B, before if offset A is less than offset B, and after if offset A is
   257 	// greater than offset B."
   258 	if (nodeA == nodeB) {
   259 		if (offsetA == offsetB) {
   260 			return "equal";
   261 		}
   262 		if (offsetA < offsetB) {
   263 			return "before";
   264 		}
   265 		if (offsetA > offsetB) {
   266 			return "after";
   267 		}
   268 	}
   270 	// "If node A is after node B in tree order, compute the position of (node
   271 	// B, offset B) relative to (node A, offset A). If it is before, return
   272 	// after. If it is after, return before."
   273 	if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
   274 		var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
   275 		if (pos == "before") {
   276 			return "after";
   277 		}
   278 		if (pos == "after") {
   279 			return "before";
   280 		}
   281 	}
   283 	// "If node A is an ancestor of node B:"
   284 	if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
   285 		// "Let child equal node B."
   286 		var child = nodeB;
   288 		// "While child is not a child of node A, set child to its parent."
   289 		while (child.parentNode != nodeA) {
   290 			child = child.parentNode;
   291 		}
   293 		// "If the index of child is less than offset A, return after."
   294 		if (getNodeIndex(child) < offsetA) {
   295 			return "after";
   296 		}
   297 	}
   299 	// "Return before."
   300 	return "before";
   301 }
   303 /**
   304  * Returns the furthest ancestor of a Node as defined by DOM Range.
   305  */
   306 function getFurthestAncestor(node) {
   307 	var root = node;
   308 	while (root.parentNode != null) {
   309 		root = root.parentNode;
   310 	}
   311 	return root;
   312 }
   314 /**
   315  * "contained" as defined by DOM Range: "A Node node is contained in a range
   316  * range if node's furthest ancestor is the same as range's root, and (node, 0)
   317  * is after range's start, and (node, length of node) is before range's end."
   318  */
   319 function isContained(node, range) {
   320 	var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
   321 	var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
   323 	return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer)
   324 		&& pos1 == "after"
   325 		&& pos2 == "before";
   326 }
   328 /**
   329  * Return all nodes contained in range that the provided function returns true
   330  * for, omitting any with an ancestor already being returned.
   331  */
   332 function getContainedNodes(range, condition) {
   333 	if (typeof condition == "undefined") {
   334 		condition = function() { return true };
   335 	}
   336 	var node = range.startContainer;
   337 	if (node.hasChildNodes()
   338 	&& range.startOffset < node.childNodes.length) {
   339 		// A child is contained
   340 		node = node.childNodes[range.startOffset];
   341 	} else if (range.startOffset == getNodeLength(node)) {
   342 		// No descendant can be contained
   343 		node = nextNodeDescendants(node);
   344 	} else {
   345 		// No children; this node at least can't be contained
   346 		node = nextNode(node);
   347 	}
   349 	var stop = range.endContainer;
   350 	if (stop.hasChildNodes()
   351 	&& range.endOffset < stop.childNodes.length) {
   352 		// The node after the last contained node is a child
   353 		stop = stop.childNodes[range.endOffset];
   354 	} else {
   355 		// This node and/or some of its children might be contained
   356 		stop = nextNodeDescendants(stop);
   357 	}
   359 	var nodeList = [];
   360 	while (isBefore(node, stop)) {
   361 		if (isContained(node, range)
   362 		&& condition(node)) {
   363 			nodeList.push(node);
   364 			node = nextNodeDescendants(node);
   365 			continue;
   366 		}
   367 		node = nextNode(node);
   368 	}
   369 	return nodeList;
   370 }
   372 /**
   373  * As above, but includes nodes with an ancestor that's already been returned.
   374  */
   375 function getAllContainedNodes(range, condition) {
   376 	if (typeof condition == "undefined") {
   377 		condition = function() { return true };
   378 	}
   379 	var node = range.startContainer;
   380 	if (node.hasChildNodes()
   381 	&& range.startOffset < node.childNodes.length) {
   382 		// A child is contained
   383 		node = node.childNodes[range.startOffset];
   384 	} else if (range.startOffset == getNodeLength(node)) {
   385 		// No descendant can be contained
   386 		node = nextNodeDescendants(node);
   387 	} else {
   388 		// No children; this node at least can't be contained
   389 		node = nextNode(node);
   390 	}
   392 	var stop = range.endContainer;
   393 	if (stop.hasChildNodes()
   394 	&& range.endOffset < stop.childNodes.length) {
   395 		// The node after the last contained node is a child
   396 		stop = stop.childNodes[range.endOffset];
   397 	} else {
   398 		// This node and/or some of its children might be contained
   399 		stop = nextNodeDescendants(stop);
   400 	}
   402 	var nodeList = [];
   403 	while (isBefore(node, stop)) {
   404 		if (isContained(node, range)
   405 		&& condition(node)) {
   406 			nodeList.push(node);
   407 		}
   408 		node = nextNode(node);
   409 	}
   410 	return nodeList;
   411 }
   413 // Returns either null, or something of the form rgb(x, y, z), or something of
   414 // the form rgb(x, y, z, w) with w != 0.
   415 function normalizeColor(color) {
   416 	if (color.toLowerCase() == "currentcolor") {
   417 		return null;
   418 	}
   420 	if (normalizeColor.resultCache === undefined) {
   421 		normalizeColor.resultCache = {};
   422 	}
   424 	if (normalizeColor.resultCache[color] !== undefined) {
   425 		return normalizeColor.resultCache[color];
   426 	}
   428 	var originalColor = color;
   430 	var outerSpan = document.createElement("span");
   431 	document.body.appendChild(outerSpan);
   432 	outerSpan.style.color = "black";
   434 	var innerSpan = document.createElement("span");
   435 	outerSpan.appendChild(innerSpan);
   436 	innerSpan.style.color = color;
   437 	color = getComputedStyle(innerSpan).color;
   439 	if (color == "rgb(0, 0, 0)") {
   440 		// Maybe it's really black, maybe it's invalid.
   441 		outerSpan.color = "white";
   442 		color = getComputedStyle(innerSpan).color;
   443 		if (color != "rgb(0, 0, 0)") {
   444 			return normalizeColor.resultCache[originalColor] = null;
   445 		}
   446 	}
   448 	document.body.removeChild(outerSpan);
   450 	// I rely on the fact that browsers generally provide consistent syntax for
   451 	// getComputedStyle(), although it's not standardized.  There are only
   452 	// three exceptions I found:
   453 	if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
   454 		// IE10PP2 seems to do this sometimes.
   455 		return normalizeColor.resultCache[originalColor] =
   456 			color.replace("rgba", "rgb").replace(", 1)", ")");
   457 	}
   458 	if (color == "transparent") {
   459 		// IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
   460 		// the specified value is "transparent".
   461 		return normalizeColor.resultCache[originalColor] =
   462 			"rgba(0, 0, 0, 0)";
   463 	}
   464 	// Chrome 15 dev adds way too many significant figures.  This isn't a full
   465 	// fix, it just fixes one case that comes up in tests.
   466 	color = color.replace(/, 0.496094\)$/, ", 0.5)");
   467 	return normalizeColor.resultCache[originalColor] = color;
   468 }
   470 // Returns either null, or something of the form #xxxxxx.
   471 function parseSimpleColor(color) {
   472 	color = normalizeColor(color);
   473 	var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
   474 	if (matches) {
   475 		return "#"
   476 			+ parseInt(matches[1]).toString(16).replace(/^.$/, "0$&")
   477 			+ parseInt(matches[2]).toString(16).replace(/^.$/, "0$&")
   478 			+ parseInt(matches[3]).toString(16).replace(/^.$/, "0$&");
   479 	}
   480 	return null;
   481 }
   483 //@}
   485 //////////////////////////////////////////////////////////////////////////////
   486 /////////////////////////// Edit command functions ///////////////////////////
   487 //////////////////////////////////////////////////////////////////////////////
   489 /////////////////////////////////////////////////
   490 ///// Methods of the HTMLDocument interface /////
   491 /////////////////////////////////////////////////
   492 //@{
   494 var executionStackDepth = 0;
   496 // Helper function for common behavior.
   497 function editCommandMethod(command, range, callback) {
   498 	// Set up our global range magic, but only if we're the outermost function
   499 	if (executionStackDepth == 0 && typeof range != "undefined") {
   500 		globalRange = range;
   501 	} else if (executionStackDepth == 0) {
   502 		globalRange = null;
   503 		globalRange = getActiveRange();
   504 	}
   506 	executionStackDepth++;
   507 	try {
   508 		var ret = callback();
   509 	} catch(e) {
   510 		executionStackDepth--;
   511 		throw e;
   512 	}
   513 	executionStackDepth--;
   514 	return ret;
   515 }
   517 function myExecCommand(command, showUi, value, range) {
   518 	// "All of these methods must treat their command argument ASCII
   519 	// case-insensitively."
   520 	command = command.toLowerCase();
   522 	// "If only one argument was provided, let show UI be false."
   523 	//
   524 	// If range was passed, I can't actually detect how many args were passed
   525 	// . . .
   526 	if (arguments.length == 1
   527 	|| (arguments.length >=4 && typeof showUi == "undefined")) {
   528 		showUi = false;
   529 	}
   531 	// "If only one or two arguments were provided, let value be the empty
   532 	// string."
   533 	if (arguments.length <= 2
   534 	|| (arguments.length >=4 && typeof value == "undefined")) {
   535 		value = "";
   536 	}
   538 	return editCommandMethod(command, range, (function(command, showUi, value) { return function() {
   539 		// "If command is not supported or not enabled, return false."
   540 		if (!(command in commands) || !myQueryCommandEnabled(command)) {
   541 			return false;
   542 		}
   544 		// "Take the action for command, passing value to the instructions as an
   545 		// argument."
   546 		var ret = commands[command].action(value);
   548 		// Check for bugs
   549 		if (ret !== true && ret !== false) {
   550 			throw "execCommand() didn't return true or false: " + ret;
   551 		}
   553 		// "If the previous step returned false, return false."
   554 		if (ret === false) {
   555 			return false;
   556 		}
   558 		// "Return true."
   559 		return true;
   560 	}})(command, showUi, value));
   561 }
   563 function myQueryCommandEnabled(command, range) {
   564 	// "All of these methods must treat their command argument ASCII
   565 	// case-insensitively."
   566 	command = command.toLowerCase();
   568 	return editCommandMethod(command, range, (function(command) { return function() {
   569 		// "Return true if command is both supported and enabled, false
   570 		// otherwise."
   571 		if (!(command in commands)) {
   572 			return false;
   573 		}
   575 		// "Among commands defined in this specification, those listed in
   576 		// Miscellaneous commands are always enabled, except for the cut
   577 		// command and the paste command. The other commands defined here are
   578 		// enabled if the active range is not null, its start node is either
   579 		// editable or an editing host, its end node is either editable or an
   580 		// editing host, and there is some editing host that is an inclusive
   581 		// ancestor of both its start node and its end node."
   582 		return ["copy", "defaultparagraphseparator", "selectall", "stylewithcss",
   583 		"usecss"].indexOf(command) != -1
   584 			|| (
   585 				getActiveRange() !== null
   586 				&& (isEditable(getActiveRange().startContainer) || isEditingHost(getActiveRange().startContainer))
   587 				&& (isEditable(getActiveRange().endContainer) || isEditingHost(getActiveRange().endContainer))
   588 				&& (getInclusiveAncestors(getActiveRange().commonAncestorContainer).some(isEditingHost))
   589 			);
   590 	}})(command));
   591 }
   593 function myQueryCommandIndeterm(command, range) {
   594 	// "All of these methods must treat their command argument ASCII
   595 	// case-insensitively."
   596 	command = command.toLowerCase();
   598 	return editCommandMethod(command, range, (function(command) { return function() {
   599 		// "If command is not supported or has no indeterminacy, return false."
   600 		if (!(command in commands) || !("indeterm" in commands[command])) {
   601 			return false;
   602 		}
   604 		// "Return true if command is indeterminate, otherwise false."
   605 		return commands[command].indeterm();
   606 	}})(command));
   607 }
   609 function myQueryCommandState(command, range) {
   610 	// "All of these methods must treat their command argument ASCII
   611 	// case-insensitively."
   612 	command = command.toLowerCase();
   614 	return editCommandMethod(command, range, (function(command) { return function() {
   615 		// "If command is not supported or has no state, return false."
   616 		if (!(command in commands) || !("state" in commands[command])) {
   617 			return false;
   618 		}
   620 		// "If the state override for command is set, return it."
   621 		if (typeof getStateOverride(command) != "undefined") {
   622 			return getStateOverride(command);
   623 		}
   625 		// "Return true if command's state is true, otherwise false."
   626 		return commands[command].state();
   627 	}})(command));
   628 }
   630 // "When the queryCommandSupported(command) method on the HTMLDocument
   631 // interface is invoked, the user agent must return true if command is
   632 // supported, and false otherwise."
   633 function myQueryCommandSupported(command) {
   634 	// "All of these methods must treat their command argument ASCII
   635 	// case-insensitively."
   636 	command = command.toLowerCase();
   638 	return command in commands;
   639 }
   641 function myQueryCommandValue(command, range) {
   642 	// "All of these methods must treat their command argument ASCII
   643 	// case-insensitively."
   644 	command = command.toLowerCase();
   646 	return editCommandMethod(command, range, function() {
   647 		// "If command is not supported or has no value, return the empty string."
   648 		if (!(command in commands) || !("value" in commands[command])) {
   649 			return "";
   650 		}
   652 		// "If command is "fontSize" and its value override is set, convert the
   653 		// value override to an integer number of pixels and return the legacy
   654 		// font size for the result."
   655 		if (command == "fontsize"
   656 		&& getValueOverride("fontsize") !== undefined) {
   657 			return getLegacyFontSize(getValueOverride("fontsize"));
   658 		}
   660 		// "If the value override for command is set, return it."
   661 		if (typeof getValueOverride(command) != "undefined") {
   662 			return getValueOverride(command);
   663 		}
   665 		// "Return command's value."
   666 		return commands[command].value();
   667 	});
   668 }
   669 //@}
   671 //////////////////////////////
   672 ///// Common definitions /////
   673 //////////////////////////////
   674 //@{
   676 // "An HTML element is an Element whose namespace is the HTML namespace."
   677 //
   678 // I allow an extra argument to more easily check whether something is a
   679 // particular HTML element, like isHtmlElement(node, "OL").  It accepts arrays
   680 // too, like isHtmlElement(node, ["OL", "UL"]) to check if it's an ol or ul.
   681 function isHtmlElement(node, tags) {
   682 	if (typeof tags == "string") {
   683 		tags = [tags];
   684 	}
   685 	if (typeof tags == "object") {
   686 		tags = tags.map(function(tag) { return tag.toUpperCase() });
   687 	}
   688 	return node
   689 		&& node.nodeType == Node.ELEMENT_NODE
   690 		&& isHtmlNamespace(node.namespaceURI)
   691 		&& (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1);
   692 }
   694 // "A prohibited paragraph child name is "address", "article", "aside",
   695 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
   696 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
   697 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
   698 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
   699 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or
   700 // "xmp"."
   701 var prohibitedParagraphChildNames = ["address", "article", "aside",
   702 	"blockquote", "caption", "center", "col", "colgroup", "dd", "details",
   703 	"dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
   704 	"form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
   705 	"listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
   706 	"summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul",
   707 	"xmp"];
   709 // "A prohibited paragraph child is an HTML element whose local name is a
   710 // prohibited paragraph child name."
   711 function isProhibitedParagraphChild(node) {
   712 	return isHtmlElement(node, prohibitedParagraphChildNames);
   713 }
   715 // "A block node is either an Element whose "display" property does not have
   716 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
   717 // Document, or a DocumentFragment."
   718 function isBlockNode(node) {
   719 	return node
   720 		&& ((node.nodeType == Node.ELEMENT_NODE && ["inline", "inline-block", "inline-table", "none"].indexOf(getComputedStyle(node).display) == -1)
   721 		|| node.nodeType == Node.DOCUMENT_NODE
   722 		|| node.nodeType == Node.DOCUMENT_FRAGMENT_NODE);
   723 }
   725 // "An inline node is a node that is not a block node."
   726 function isInlineNode(node) {
   727 	return node && !isBlockNode(node);
   728 }
   730 // "An editing host is a node that is either an HTML element with a
   731 // contenteditable attribute set to the true state, or the HTML element child
   732 // of a Document whose designMode is enabled."
   733 function isEditingHost(node) {
   734 	return node
   735 		&& isHtmlElement(node)
   736 		&& (node.contentEditable == "true"
   737 		|| (node.parentNode
   738 		&& node.parentNode.nodeType == Node.DOCUMENT_NODE
   739 		&& node.parentNode.designMode == "on"));
   740 }
   742 // "Something is editable if it is a node; it is not an editing host; it does
   743 // not have a contenteditable attribute set to the false state; its parent is
   744 // an editing host or editable; and either it is an HTML element, or it is an
   745 // svg or math element, or it is not an Element and its parent is an HTML
   746 // element."
   747 function isEditable(node) {
   748 	return node
   749 		&& !isEditingHost(node)
   750 		&& (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false")
   751 		&& (isEditingHost(node.parentNode) || isEditable(node.parentNode))
   752 		&& (isHtmlElement(node)
   753 		|| (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/2000/svg" && node.localName == "svg")
   754 		|| (node.nodeType == Node.ELEMENT_NODE && node.namespaceURI == "http://www.w3.org/1998/Math/MathML" && node.localName == "math")
   755 		|| (node.nodeType != Node.ELEMENT_NODE && isHtmlElement(node.parentNode)));
   756 }
   758 // Helper function, not defined in the spec
   759 function hasEditableDescendants(node) {
   760 	for (var i = 0; i < node.childNodes.length; i++) {
   761 		if (isEditable(node.childNodes[i])
   762 		|| hasEditableDescendants(node.childNodes[i])) {
   763 			return true;
   764 		}
   765 	}
   766 	return false;
   767 }
   769 // "The editing host of node is null if node is neither editable nor an editing
   770 // host; node itself, if node is an editing host; or the nearest ancestor of
   771 // node that is an editing host, if node is editable."
   772 function getEditingHostOf(node) {
   773 	if (isEditingHost(node)) {
   774 		return node;
   775 	} else if (isEditable(node)) {
   776 		var ancestor = node.parentNode;
   777 		while (!isEditingHost(ancestor)) {
   778 			ancestor = ancestor.parentNode;
   779 		}
   780 		return ancestor;
   781 	} else {
   782 		return null;
   783 	}
   784 }
   786 // "Two nodes are in the same editing host if the editing host of the first is
   787 // non-null and the same as the editing host of the second."
   788 function inSameEditingHost(node1, node2) {
   789 	return getEditingHostOf(node1)
   790 		&& getEditingHostOf(node1) == getEditingHostOf(node2);
   791 }
   793 // "A collapsed line break is a br that begins a line box which has nothing
   794 // else in it, and therefore has zero height."
   795 function isCollapsedLineBreak(br) {
   796 	if (!isHtmlElement(br, "br")) {
   797 		return false;
   798 	}
   800 	// Add a zwsp after it and see if that changes the height of the nearest
   801 	// non-inline parent.  Note: this is not actually reliable, because the
   802 	// parent might have a fixed height or something.
   803 	var ref = br.parentNode;
   804 	while (getComputedStyle(ref).display == "inline") {
   805 		ref = ref.parentNode;
   806 	}
   807 	var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
   808 	ref.style.height = "auto";
   809 	ref.style.maxHeight = "none";
   810 	ref.style.minHeight = "0";
   811 	var space = document.createTextNode("\u200b");
   812 	var origHeight = ref.offsetHeight;
   813 	if (origHeight == 0) {
   814 		throw "isCollapsedLineBreak: original height is zero, bug?";
   815 	}
   816 	br.parentNode.insertBefore(space, br.nextSibling);
   817 	var finalHeight = ref.offsetHeight;
   818 	space.parentNode.removeChild(space);
   819 	if (refStyle === null) {
   820 		// Without the setAttribute() line, removeAttribute() doesn't work in
   821 		// Chrome 14 dev.  I have no idea why.
   822 		ref.setAttribute("style", "");
   823 		ref.removeAttribute("style");
   824 	} else {
   825 		ref.setAttribute("style", refStyle);
   826 	}
   828 	// Allow some leeway in case the zwsp didn't create a whole new line, but
   829 	// only made an existing line slightly higher.  Firefox 6.0a2 shows this
   830 	// behavior when the first line is bold.
   831 	return origHeight < finalHeight - 5;
   832 }
   834 // "An extraneous line break is a br that has no visual effect, in that
   835 // removing it from the DOM would not change layout, except that a br that is
   836 // the sole child of an li is not extraneous."
   837 //
   838 // FIXME: This doesn't work in IE, since IE ignores display: none in
   839 // contenteditable.
   840 function isExtraneousLineBreak(br) {
   841 	if (!isHtmlElement(br, "br")) {
   842 		return false;
   843 	}
   845 	if (isHtmlElement(br.parentNode, "li")
   846 	&& br.parentNode.childNodes.length == 1) {
   847 		return false;
   848 	}
   850 	// Make the line break disappear and see if that changes the block's
   851 	// height.  Yes, this is an absurd hack.  We have to reset height etc. on
   852 	// the reference node because otherwise its height won't change if it's not
   853 	// auto.
   854 	var ref = br.parentNode;
   855 	while (getComputedStyle(ref).display == "inline") {
   856 		ref = ref.parentNode;
   857 	}
   858 	var refStyle = ref.hasAttribute("style") ? ref.getAttribute("style") : null;
   859 	ref.style.height = "auto";
   860 	ref.style.maxHeight = "none";
   861 	ref.style.minHeight = "0";
   862 	var brStyle = br.hasAttribute("style") ? br.getAttribute("style") : null;
   863 	var origHeight = ref.offsetHeight;
   864 	if (origHeight == 0) {
   865 		throw "isExtraneousLineBreak: original height is zero, bug?";
   866 	}
   867 	br.setAttribute("style", "display:none");
   868 	var finalHeight = ref.offsetHeight;
   869 	if (refStyle === null) {
   870 		// Without the setAttribute() line, removeAttribute() doesn't work in
   871 		// Chrome 14 dev.  I have no idea why.
   872 		ref.setAttribute("style", "");
   873 		ref.removeAttribute("style");
   874 	} else {
   875 		ref.setAttribute("style", refStyle);
   876 	}
   877 	if (brStyle === null) {
   878 		br.removeAttribute("style");
   879 	} else {
   880 		br.setAttribute("style", brStyle);
   881 	}
   883 	return origHeight == finalHeight;
   884 }
   886 // "A whitespace node is either a Text node whose data is the empty string; or
   887 // a Text node whose data consists only of one or more tabs (0x0009), line
   888 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
   889 // parent is an Element whose resolved value for "white-space" is "normal" or
   890 // "nowrap"; or a Text node whose data consists only of one or more tabs
   891 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
   892 // parent is an Element whose resolved value for "white-space" is "pre-line"."
   893 function isWhitespaceNode(node) {
   894 	return node
   895 		&& node.nodeType == Node.TEXT_NODE
   896 		&& (node.data == ""
   897 		|| (
   898 			/^[\t\n\r ]+$/.test(node.data)
   899 			&& node.parentNode
   900 			&& node.parentNode.nodeType == Node.ELEMENT_NODE
   901 			&& ["normal", "nowrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1
   902 		) || (
   903 			/^[\t\r ]+$/.test(node.data)
   904 			&& node.parentNode
   905 			&& node.parentNode.nodeType == Node.ELEMENT_NODE
   906 			&& getComputedStyle(node.parentNode).whiteSpace == "pre-line"
   907 		));
   908 }
   910 // "node is a collapsed whitespace node if the following algorithm returns
   911 // true:"
   912 function isCollapsedWhitespaceNode(node) {
   913 	// "If node is not a whitespace node, return false."
   914 	if (!isWhitespaceNode(node)) {
   915 		return false;
   916 	}
   918 	// "If node's data is the empty string, return true."
   919 	if (node.data == "") {
   920 		return true;
   921 	}
   923 	// "Let ancestor be node's parent."
   924 	var ancestor = node.parentNode;
   926 	// "If ancestor is null, return true."
   927 	if (!ancestor) {
   928 		return true;
   929 	}
   931 	// "If the "display" property of some ancestor of node has resolved value
   932 	// "none", return true."
   933 	if (getAncestors(node).some(function(ancestor) {
   934 		return ancestor.nodeType == Node.ELEMENT_NODE
   935 			&& getComputedStyle(ancestor).display == "none";
   936 	})) {
   937 		return true;
   938 	}
   940 	// "While ancestor is not a block node and its parent is not null, set
   941 	// ancestor to its parent."
   942 	while (!isBlockNode(ancestor)
   943 	&& ancestor.parentNode) {
   944 		ancestor = ancestor.parentNode;
   945 	}
   947 	// "Let reference be node."
   948 	var reference = node;
   950 	// "While reference is a descendant of ancestor:"
   951 	while (reference != ancestor) {
   952 		// "Let reference be the node before it in tree order."
   953 		reference = previousNode(reference);
   955 		// "If reference is a block node or a br, return true."
   956 		if (isBlockNode(reference)
   957 		|| isHtmlElement(reference, "br")) {
   958 			return true;
   959 		}
   961 		// "If reference is a Text node that is not a whitespace node, or is an
   962 		// img, break from this loop."
   963 		if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
   964 		|| isHtmlElement(reference, "img")) {
   965 			break;
   966 		}
   967 	}
   969 	// "Let reference be node."
   970 	reference = node;
   972 	// "While reference is a descendant of ancestor:"
   973 	var stop = nextNodeDescendants(ancestor);
   974 	while (reference != stop) {
   975 		// "Let reference be the node after it in tree order, or null if there
   976 		// is no such node."
   977 		reference = nextNode(reference);
   979 		// "If reference is a block node or a br, return true."
   980 		if (isBlockNode(reference)
   981 		|| isHtmlElement(reference, "br")) {
   982 			return true;
   983 		}
   985 		// "If reference is a Text node that is not a whitespace node, or is an
   986 		// img, break from this loop."
   987 		if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference))
   988 		|| isHtmlElement(reference, "img")) {
   989 			break;
   990 		}
   991 	}
   993 	// "Return false."
   994 	return false;
   995 }
   997 // "Something is visible if it is a node that either is a block node, or a Text
   998 // node that is not a collapsed whitespace node, or an img, or a br that is not
   999 // an extraneous line break, or any node with a visible descendant; excluding
  1000 // any node with an ancestor container Element whose "display" property has
  1001 // resolved value "none"."
  1002 function isVisible(node) {
  1003 	if (!node) {
  1004 		return false;
  1007 	if (getAncestors(node).concat(node)
  1008 	.filter(function(node) { return node.nodeType == Node.ELEMENT_NODE })
  1009 	.some(function(node) { return getComputedStyle(node).display == "none" })) {
  1010 		return false;
  1013 	if (isBlockNode(node)
  1014 	|| (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node))
  1015 	|| isHtmlElement(node, "img")
  1016 	|| (isHtmlElement(node, "br") && !isExtraneousLineBreak(node))) {
  1017 		return true;
  1020 	for (var i = 0; i < node.childNodes.length; i++) {
  1021 		if (isVisible(node.childNodes[i])) {
  1022 			return true;
  1026 	return false;
  1029 // "Something is invisible if it is a node that is not visible."
  1030 function isInvisible(node) {
  1031 	return node && !isVisible(node);
  1034 // "A collapsed block prop is either a collapsed line break that is not an
  1035 // extraneous line break, or an Element that is an inline node and whose
  1036 // children are all either invisible or collapsed block props and that has at
  1037 // least one child that is a collapsed block prop."
  1038 function isCollapsedBlockProp(node) {
  1039 	if (isCollapsedLineBreak(node)
  1040 	&& !isExtraneousLineBreak(node)) {
  1041 		return true;
  1044 	if (!isInlineNode(node)
  1045 	|| node.nodeType != Node.ELEMENT_NODE) {
  1046 		return false;
  1049 	var hasCollapsedBlockPropChild = false;
  1050 	for (var i = 0; i < node.childNodes.length; i++) {
  1051 		if (!isInvisible(node.childNodes[i])
  1052 		&& !isCollapsedBlockProp(node.childNodes[i])) {
  1053 			return false;
  1055 		if (isCollapsedBlockProp(node.childNodes[i])) {
  1056 			hasCollapsedBlockPropChild = true;
  1060 	return hasCollapsedBlockPropChild;
  1063 // "The active range is the range of the selection given by calling
  1064 // getSelection() on the context object. (Thus the active range may be null.)"
  1065 //
  1066 // We cheat and return globalRange if that's defined.  We also ensure that the
  1067 // active range meets the requirements that selection boundary points are
  1068 // supposed to meet, i.e., that the nodes are both Text or Element nodes that
  1069 // descend from a Document.
  1070 function getActiveRange() {
  1071 	var ret;
  1072 	if (globalRange) {
  1073 		ret = globalRange;
  1074 	} else if (getSelection().rangeCount) {
  1075 		ret = getSelection().getRangeAt(0);
  1076 	} else {
  1077 		return null;
  1079 	if ([Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.startContainer.nodeType) == -1
  1080 	|| [Node.TEXT_NODE, Node.ELEMENT_NODE].indexOf(ret.endContainer.nodeType) == -1
  1081 	|| !ret.startContainer.ownerDocument
  1082 	|| !ret.endContainer.ownerDocument
  1083 	|| !isDescendant(ret.startContainer, ret.startContainer.ownerDocument)
  1084 	|| !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) {
  1085 		throw "Invalid active range; test bug?";
  1087 	return ret;
  1090 // "For some commands, each HTMLDocument must have a boolean state override
  1091 // and/or a string value override. These do not change the command's state or
  1092 // value, but change the way some algorithms behave, as specified in those
  1093 // algorithms' definitions. Initially, both must be unset for every command.
  1094 // Whenever the number of ranges in the Selection changes to something
  1095 // different, and whenever a boundary point of the range at a given index in
  1096 // the Selection changes to something different, the state override and value
  1097 // override must be unset for every command."
  1098 //
  1099 // We implement this crudely by using setters and getters.  To verify that the
  1100 // selection hasn't changed, we copy the active range and just check the
  1101 // endpoints match.  This isn't really correct, but it's good enough for us.
  1102 // Unset state/value overrides are undefined.  We put everything in a function
  1103 // so no one can access anything except via the provided functions, since
  1104 // otherwise callers might mistakenly use outdated overrides (if the selection
  1105 // has changed).
  1106 var getStateOverride, setStateOverride, unsetStateOverride,
  1107 	getValueOverride, setValueOverride, unsetValueOverride;
  1108 (function() {
  1109 	var stateOverrides = {};
  1110 	var valueOverrides = {};
  1111 	var storedRange = null;
  1113 	function resetOverrides() {
  1114 		if (!storedRange
  1115 		|| storedRange.startContainer != getActiveRange().startContainer
  1116 		|| storedRange.endContainer != getActiveRange().endContainer
  1117 		|| storedRange.startOffset != getActiveRange().startOffset
  1118 		|| storedRange.endOffset != getActiveRange().endOffset) {
  1119 			stateOverrides = {};
  1120 			valueOverrides = {};
  1121 			storedRange = getActiveRange().cloneRange();
  1125 	getStateOverride = function(command) {
  1126 		resetOverrides();
  1127 		return stateOverrides[command];
  1128 	};
  1130 	setStateOverride = function(command, newState) {
  1131 		resetOverrides();
  1132 		stateOverrides[command] = newState;
  1133 	};
  1135 	unsetStateOverride = function(command) {
  1136 		resetOverrides();
  1137 		delete stateOverrides[command];
  1140 	getValueOverride = function(command) {
  1141 		resetOverrides();
  1142 		return valueOverrides[command];
  1145 	// "The value override for the backColor command must be the same as the
  1146 	// value override for the hiliteColor command, such that setting one sets
  1147 	// the other to the same thing and unsetting one unsets the other."
  1148 	setValueOverride = function(command, newValue) {
  1149 		resetOverrides();
  1150 		valueOverrides[command] = newValue;
  1151 		if (command == "backcolor") {
  1152 			valueOverrides.hilitecolor = newValue;
  1153 		} else if (command == "hilitecolor") {
  1154 			valueOverrides.backcolor = newValue;
  1158 	unsetValueOverride = function(command) {
  1159 		resetOverrides();
  1160 		delete valueOverrides[command];
  1161 		if (command == "backcolor") {
  1162 			delete valueOverrides.hilitecolor;
  1163 		} else if (command == "hilitecolor") {
  1164 			delete valueOverrides.backcolor;
  1167 })();
  1169 //@}
  1171 /////////////////////////////
  1172 ///// Common algorithms /////
  1173 /////////////////////////////
  1175 ///// Assorted common algorithms /////
  1176 //@{
  1178 // Magic array of extra ranges whose endpoints we want to preserve.
  1179 var extraRanges = [];
  1181 function movePreservingRanges(node, newParent, newIndex) {
  1182 	// For convenience, I allow newIndex to be -1 to mean "insert at the end".
  1183 	if (newIndex == -1) {
  1184 		newIndex = newParent.childNodes.length;
  1187 	// "When the user agent is to move a Node to a new location, preserving
  1188 	// ranges, it must remove the Node from its original parent (if any), then
  1189 	// insert it in the new location. In doing so, however, it must ignore the
  1190 	// regular range mutation rules, and instead follow these rules:"
  1192 	// "Let node be the moved Node, old parent and old index be the old parent
  1193 	// (which may be null) and index, and new parent and new index be the new
  1194 	// parent and index."
  1195 	var oldParent = node.parentNode;
  1196 	var oldIndex = getNodeIndex(node);
  1198 	// We preserve the global range object, the ranges in the selection, and
  1199 	// any range that's in the extraRanges array.  Any other ranges won't get
  1200 	// updated, because we have no references to them.
  1201 	var ranges = [globalRange].concat(extraRanges);
  1202 	for (var i = 0; i < getSelection().rangeCount; i++) {
  1203 		ranges.push(getSelection().getRangeAt(i));
  1205 	var boundaryPoints = [];
  1206 	ranges.forEach(function(range) {
  1207 		boundaryPoints.push([range.startContainer, range.startOffset]);
  1208 		boundaryPoints.push([range.endContainer, range.endOffset]);
  1209 	});
  1211 	boundaryPoints.forEach(function(boundaryPoint) {
  1212 		// "If a boundary point's node is the same as or a descendant of node,
  1213 		// leave it unchanged, so it moves to the new location."
  1214 		//
  1215 		// No modifications necessary.
  1217 		// "If a boundary point's node is new parent and its offset is greater
  1218 		// than new index, add one to its offset."
  1219 		if (boundaryPoint[0] == newParent
  1220 		&& boundaryPoint[1] > newIndex) {
  1221 			boundaryPoint[1]++;
  1224 		// "If a boundary point's node is old parent and its offset is old index or
  1225 		// old index + 1, set its node to new parent and add new index − old index
  1226 		// to its offset."
  1227 		if (boundaryPoint[0] == oldParent
  1228 		&& (boundaryPoint[1] == oldIndex
  1229 		|| boundaryPoint[1] == oldIndex + 1)) {
  1230 			boundaryPoint[0] = newParent;
  1231 			boundaryPoint[1] += newIndex - oldIndex;
  1234 		// "If a boundary point's node is old parent and its offset is greater than
  1235 		// old index + 1, subtract one from its offset."
  1236 		if (boundaryPoint[0] == oldParent
  1237 		&& boundaryPoint[1] > oldIndex + 1) {
  1238 			boundaryPoint[1]--;
  1240 	});
  1242 	// Now actually move it and preserve the ranges.
  1243 	if (newParent.childNodes.length == newIndex) {
  1244 		newParent.appendChild(node);
  1245 	} else {
  1246 		newParent.insertBefore(node, newParent.childNodes[newIndex]);
  1249 	globalRange.setStart(boundaryPoints[0][0], boundaryPoints[0][1]);
  1250 	globalRange.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]);
  1252 	for (var i = 0; i < extraRanges.length; i++) {
  1253 		extraRanges[i].setStart(boundaryPoints[2*i + 2][0], boundaryPoints[2*i + 2][1]);
  1254 		extraRanges[i].setEnd(boundaryPoints[2*i + 3][0], boundaryPoints[2*i + 3][1]);
  1257 	getSelection().removeAllRanges();
  1258 	for (var i = 1 + extraRanges.length; i < ranges.length; i++) {
  1259 		var newRange = document.createRange();
  1260 		newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]);
  1261 		newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]);
  1262 		getSelection().addRange(newRange);
  1266 function setTagName(element, newName) {
  1267 	// "If element is an HTML element with local name equal to new name, return
  1268 	// element."
  1269 	if (isHtmlElement(element, newName.toUpperCase())) {
  1270 		return element;
  1273 	// "If element's parent is null, return element."
  1274 	if (!element.parentNode) {
  1275 		return element;
  1278 	// "Let replacement element be the result of calling createElement(new
  1279 	// name) on the ownerDocument of element."
  1280 	var replacementElement = element.ownerDocument.createElement(newName);
  1282 	// "Insert replacement element into element's parent immediately before
  1283 	// element."
  1284 	element.parentNode.insertBefore(replacementElement, element);
  1286 	// "Copy all attributes of element to replacement element, in order."
  1287 	for (var i = 0; i < element.attributes.length; i++) {
  1288 		replacementElement.setAttributeNS(element.attributes[i].namespaceURI, element.attributes[i].name, element.attributes[i].value);
  1291 	// "While element has children, append the first child of element as the
  1292 	// last child of replacement element, preserving ranges."
  1293 	while (element.childNodes.length) {
  1294 		movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length);
  1297 	// "Remove element from its parent."
  1298 	element.parentNode.removeChild(element);
  1300 	// "Return replacement element."
  1301 	return replacementElement;
  1304 function removeExtraneousLineBreaksBefore(node) {
  1305 	// "Let ref be the previousSibling of node."
  1306 	var ref = node.previousSibling;
  1308 	// "If ref is null, abort these steps."
  1309 	if (!ref) {
  1310 		return;
  1313 	// "While ref has children, set ref to its lastChild."
  1314 	while (ref.hasChildNodes()) {
  1315 		ref = ref.lastChild;
  1318 	// "While ref is invisible but not an extraneous line break, and ref does
  1319 	// not equal node's parent, set ref to the node before it in tree order."
  1320 	while (isInvisible(ref)
  1321 	&& !isExtraneousLineBreak(ref)
  1322 	&& ref != node.parentNode) {
  1323 		ref = previousNode(ref);
  1326 	// "If ref is an editable extraneous line break, remove it from its
  1327 	// parent."
  1328 	if (isEditable(ref)
  1329 	&& isExtraneousLineBreak(ref)) {
  1330 		ref.parentNode.removeChild(ref);
  1334 function removeExtraneousLineBreaksAtTheEndOf(node) {
  1335 	// "Let ref be node."
  1336 	var ref = node;
  1338 	// "While ref has children, set ref to its lastChild."
  1339 	while (ref.hasChildNodes()) {
  1340 		ref = ref.lastChild;
  1343 	// "While ref is invisible but not an extraneous line break, and ref does
  1344 	// not equal node, set ref to the node before it in tree order."
  1345 	while (isInvisible(ref)
  1346 	&& !isExtraneousLineBreak(ref)
  1347 	&& ref != node) {
  1348 		ref = previousNode(ref);
  1351 	// "If ref is an editable extraneous line break:"
  1352 	if (isEditable(ref)
  1353 	&& isExtraneousLineBreak(ref)) {
  1354 		// "While ref's parent is editable and invisible, set ref to its
  1355 		// parent."
  1356 		while (isEditable(ref.parentNode)
  1357 		&& isInvisible(ref.parentNode)) {
  1358 			ref = ref.parentNode;
  1361 		// "Remove ref from its parent."
  1362 		ref.parentNode.removeChild(ref);
  1366 // "To remove extraneous line breaks from a node, first remove extraneous line
  1367 // breaks before it, then remove extraneous line breaks at the end of it."
  1368 function removeExtraneousLineBreaksFrom(node) {
  1369 	removeExtraneousLineBreaksBefore(node);
  1370 	removeExtraneousLineBreaksAtTheEndOf(node);
  1373 //@}
  1374 ///// Wrapping a list of nodes /////
  1375 //@{
  1377 function wrap(nodeList, siblingCriteria, newParentInstructions) {
  1378 	// "If not provided, sibling criteria returns false and new parent
  1379 	// instructions returns null."
  1380 	if (typeof siblingCriteria == "undefined") {
  1381 		siblingCriteria = function() { return false };
  1383 	if (typeof newParentInstructions == "undefined") {
  1384 		newParentInstructions = function() { return null };
  1387 	// "If every member of node list is invisible, and none is a br, return
  1388 	// null and abort these steps."
  1389 	if (nodeList.every(isInvisible)
  1390 	&& !nodeList.some(function(node) { return isHtmlElement(node, "br") })) {
  1391 		return null;
  1394 	// "If node list's first member's parent is null, return null and abort
  1395 	// these steps."
  1396 	if (!nodeList[0].parentNode) {
  1397 		return null;
  1400 	// "If node list's last member is an inline node that's not a br, and node
  1401 	// list's last member's nextSibling is a br, append that br to node list."
  1402 	if (isInlineNode(nodeList[nodeList.length - 1])
  1403 	&& !isHtmlElement(nodeList[nodeList.length - 1], "br")
  1404 	&& isHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) {
  1405 		nodeList.push(nodeList[nodeList.length - 1].nextSibling);
  1408 	// "While node list's first member's previousSibling is invisible, prepend
  1409 	// it to node list."
  1410 	while (isInvisible(nodeList[0].previousSibling)) {
  1411 		nodeList.unshift(nodeList[0].previousSibling);
  1414 	// "While node list's last member's nextSibling is invisible, append it to
  1415 	// node list."
  1416 	while (isInvisible(nodeList[nodeList.length - 1].nextSibling)) {
  1417 		nodeList.push(nodeList[nodeList.length - 1].nextSibling);
  1420 	// "If the previousSibling of the first member of node list is editable and
  1421 	// running sibling criteria on it returns true, let new parent be the
  1422 	// previousSibling of the first member of node list."
  1423 	var newParent;
  1424 	if (isEditable(nodeList[0].previousSibling)
  1425 	&& siblingCriteria(nodeList[0].previousSibling)) {
  1426 		newParent = nodeList[0].previousSibling;
  1428 	// "Otherwise, if the nextSibling of the last member of node list is
  1429 	// editable and running sibling criteria on it returns true, let new parent
  1430 	// be the nextSibling of the last member of node list."
  1431 	} else if (isEditable(nodeList[nodeList.length - 1].nextSibling)
  1432 	&& siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) {
  1433 		newParent = nodeList[nodeList.length - 1].nextSibling;
  1435 	// "Otherwise, run new parent instructions, and let new parent be the
  1436 	// result."
  1437 	} else {
  1438 		newParent = newParentInstructions();
  1441 	// "If new parent is null, abort these steps and return null."
  1442 	if (!newParent) {
  1443 		return null;
  1446 	// "If new parent's parent is null:"
  1447 	if (!newParent.parentNode) {
  1448 		// "Insert new parent into the parent of the first member of node list
  1449 		// immediately before the first member of node list."
  1450 		nodeList[0].parentNode.insertBefore(newParent, nodeList[0]);
  1452 		// "If any range has a boundary point with node equal to the parent of
  1453 		// new parent and offset equal to the index of new parent, add one to
  1454 		// that boundary point's offset."
  1455 		//
  1456 		// Only try to fix the global range.
  1457 		if (globalRange.startContainer == newParent.parentNode
  1458 		&& globalRange.startOffset == getNodeIndex(newParent)) {
  1459 			globalRange.setStart(globalRange.startContainer, globalRange.startOffset + 1);
  1461 		if (globalRange.endContainer == newParent.parentNode
  1462 		&& globalRange.endOffset == getNodeIndex(newParent)) {
  1463 			globalRange.setEnd(globalRange.endContainer, globalRange.endOffset + 1);
  1467 	// "Let original parent be the parent of the first member of node list."
  1468 	var originalParent = nodeList[0].parentNode;
  1470 	// "If new parent is before the first member of node list in tree order:"
  1471 	if (isBefore(newParent, nodeList[0])) {
  1472 		// "If new parent is not an inline node, but the last visible child of
  1473 		// new parent and the first visible member of node list are both inline
  1474 		// nodes, and the last child of new parent is not a br, call
  1475 		// createElement("br") on the ownerDocument of new parent and append
  1476 		// the result as the last child of new parent."
  1477 		if (!isInlineNode(newParent)
  1478 		&& isInlineNode([].filter.call(newParent.childNodes, isVisible).slice(-1)[0])
  1479 		&& isInlineNode(nodeList.filter(isVisible)[0])
  1480 		&& !isHtmlElement(newParent.lastChild, "BR")) {
  1481 			newParent.appendChild(newParent.ownerDocument.createElement("br"));
  1484 		// "For each node in node list, append node as the last child of new
  1485 		// parent, preserving ranges."
  1486 		for (var i = 0; i < nodeList.length; i++) {
  1487 			movePreservingRanges(nodeList[i], newParent, -1);
  1490 	// "Otherwise:"
  1491 	} else {
  1492 		// "If new parent is not an inline node, but the first visible child of
  1493 		// new parent and the last visible member of node list are both inline
  1494 		// nodes, and the last member of node list is not a br, call
  1495 		// createElement("br") on the ownerDocument of new parent and insert
  1496 		// the result as the first child of new parent."
  1497 		if (!isInlineNode(newParent)
  1498 		&& isInlineNode([].filter.call(newParent.childNodes, isVisible)[0])
  1499 		&& isInlineNode(nodeList.filter(isVisible).slice(-1)[0])
  1500 		&& !isHtmlElement(nodeList[nodeList.length - 1], "BR")) {
  1501 			newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild);
  1504 		// "For each node in node list, in reverse order, insert node as the
  1505 		// first child of new parent, preserving ranges."
  1506 		for (var i = nodeList.length - 1; i >= 0; i--) {
  1507 			movePreservingRanges(nodeList[i], newParent, 0);
  1511 	// "If original parent is editable and has no children, remove it from its
  1512 	// parent."
  1513 	if (isEditable(originalParent) && !originalParent.hasChildNodes()) {
  1514 		originalParent.parentNode.removeChild(originalParent);
  1517 	// "If new parent's nextSibling is editable and running sibling criteria on
  1518 	// it returns true:"
  1519 	if (isEditable(newParent.nextSibling)
  1520 	&& siblingCriteria(newParent.nextSibling)) {
  1521 		// "If new parent is not an inline node, but new parent's last child
  1522 		// and new parent's nextSibling's first child are both inline nodes,
  1523 		// and new parent's last child is not a br, call createElement("br") on
  1524 		// the ownerDocument of new parent and append the result as the last
  1525 		// child of new parent."
  1526 		if (!isInlineNode(newParent)
  1527 		&& isInlineNode(newParent.lastChild)
  1528 		&& isInlineNode(newParent.nextSibling.firstChild)
  1529 		&& !isHtmlElement(newParent.lastChild, "BR")) {
  1530 			newParent.appendChild(newParent.ownerDocument.createElement("br"));
  1533 		// "While new parent's nextSibling has children, append its first child
  1534 		// as the last child of new parent, preserving ranges."
  1535 		while (newParent.nextSibling.hasChildNodes()) {
  1536 			movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1);
  1539 		// "Remove new parent's nextSibling from its parent."
  1540 		newParent.parentNode.removeChild(newParent.nextSibling);
  1543 	// "Remove extraneous line breaks from new parent."
  1544 	removeExtraneousLineBreaksFrom(newParent);
  1546 	// "Return new parent."
  1547 	return newParent;
  1551 //@}
  1552 ///// Allowed children /////
  1553 //@{
  1555 // "A name of an element with inline contents is "a", "abbr", "b", "bdi",
  1556 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
  1557 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
  1558 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
  1559 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"."
  1560 var namesOfElementsWithInlineContents = ["a", "abbr", "b", "bdi", "bdo",
  1561 	"cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
  1562 	"kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
  1563 	"span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
  1564 	"xmp", "big", "blink", "font", "marquee", "nobr", "tt"];
  1566 // "An element with inline contents is an HTML element whose local name is a
  1567 // name of an element with inline contents."
  1568 function isElementWithInlineContents(node) {
  1569 	return isHtmlElement(node, namesOfElementsWithInlineContents);
  1572 function isAllowedChild(child, parent_) {
  1573 	// "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or
  1574 	// an HTML element with local name equal to one of those, and child is a
  1575 	// Text node whose data does not consist solely of space characters, return
  1576 	// false."
  1577 	if ((["colgroup", "table", "tbody", "tfoot", "thead", "tr"].indexOf(parent_) != -1
  1578 	|| isHtmlElement(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"]))
  1579 	&& typeof child == "object"
  1580 	&& child.nodeType == Node.TEXT_NODE
  1581 	&& !/^[ \t\n\f\r]*$/.test(child.data)) {
  1582 		return false;
  1585 	// "If parent is "script", "style", "plaintext", or "xmp", or an HTML
  1586 	// element with local name equal to one of those, and child is not a Text
  1587 	// node, return false."
  1588 	if ((["script", "style", "plaintext", "xmp"].indexOf(parent_) != -1
  1589 	|| isHtmlElement(parent_, ["script", "style", "plaintext", "xmp"]))
  1590 	&& (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) {
  1591 		return false;
  1594 	// "If child is a Document, DocumentFragment, or DocumentType, return
  1595 	// false."
  1596 	if (typeof child == "object"
  1597 	&& (child.nodeType == Node.DOCUMENT_NODE
  1598 	|| child.nodeType == Node.DOCUMENT_FRAGMENT_NODE
  1599 	|| child.nodeType == Node.DOCUMENT_TYPE_NODE)) {
  1600 		return false;
  1603 	// "If child is an HTML element, set child to the local name of child."
  1604 	if (isHtmlElement(child)) {
  1605 		child = child.tagName.toLowerCase();
  1608 	// "If child is not a string, return true."
  1609 	if (typeof child != "string") {
  1610 		return true;
  1613 	// "If parent is an HTML element:"
  1614 	if (isHtmlElement(parent_)) {
  1615 		// "If child is "a", and parent or some ancestor of parent is an a,
  1616 		// return false."
  1617 		//
  1618 		// "If child is a prohibited paragraph child name and parent or some
  1619 		// ancestor of parent is an element with inline contents, return
  1620 		// false."
  1621 		//
  1622 		// "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or
  1623 		// some ancestor of parent is an HTML element with local name "h1",
  1624 		// "h2", "h3", "h4", "h5", or "h6", return false."
  1625 		var ancestor = parent_;
  1626 		while (ancestor) {
  1627 			if (child == "a" && isHtmlElement(ancestor, "a")) {
  1628 				return false;
  1630 			if (prohibitedParagraphChildNames.indexOf(child) != -1
  1631 			&& isElementWithInlineContents(ancestor)) {
  1632 				return false;
  1634 			if (/^h[1-6]$/.test(child)
  1635 			&& isHtmlElement(ancestor)
  1636 			&& /^H[1-6]$/.test(ancestor.tagName)) {
  1637 				return false;
  1639 			ancestor = ancestor.parentNode;
  1642 		// "Let parent be the local name of parent."
  1643 		parent_ = parent_.tagName.toLowerCase();
  1646 	// "If parent is an Element or DocumentFragment, return true."
  1647 	if (typeof parent_ == "object"
  1648 	&& (parent_.nodeType == Node.ELEMENT_NODE
  1649 	|| parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) {
  1650 		return true;
  1653 	// "If parent is not a string, return false."
  1654 	if (typeof parent_ != "string") {
  1655 		return false;
  1658 	// "If parent is on the left-hand side of an entry on the following list,
  1659 	// then return true if child is listed on the right-hand side of that
  1660 	// entry, and false otherwise."
  1661 	switch (parent_) {
  1662 		case "colgroup":
  1663 			return child == "col";
  1664 		case "table":
  1665 			return ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1;
  1666 		case "tbody":
  1667 		case "thead":
  1668 		case "tfoot":
  1669 			return ["td", "th", "tr"].indexOf(child) != -1;
  1670 		case "tr":
  1671 			return ["td", "th"].indexOf(child) != -1;
  1672 		case "dl":
  1673 			return ["dt", "dd"].indexOf(child) != -1;
  1674 		case "dir":
  1675 		case "ol":
  1676 		case "ul":
  1677 			return ["dir", "li", "ol", "ul"].indexOf(child) != -1;
  1678 		case "hgroup":
  1679 			return /^h[1-6]$/.test(child);
  1682 	// "If child is "body", "caption", "col", "colgroup", "frame", "frameset",
  1683 	// "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return
  1684 	// false."
  1685 	if (["body", "caption", "col", "colgroup", "frame", "frameset", "head",
  1686 	"html", "tbody", "td", "tfoot", "th", "thead", "tr"].indexOf(child) != -1) {
  1687 		return false;
  1690 	// "If child is "dd" or "dt" and parent is not "dl", return false."
  1691 	if (["dd", "dt"].indexOf(child) != -1
  1692 	&& parent_ != "dl") {
  1693 		return false;
  1696 	// "If child is "li" and parent is not "ol" or "ul", return false."
  1697 	if (child == "li"
  1698 	&& parent_ != "ol"
  1699 	&& parent_ != "ul") {
  1700 		return false;
  1703 	// "If parent is on the left-hand side of an entry on the following list
  1704 	// and child is listed on the right-hand side of that entry, return false."
  1705 	var table = [
  1706 		[["a"], ["a"]],
  1707 		[["dd", "dt"], ["dd", "dt"]],
  1708 		[["h1", "h2", "h3", "h4", "h5", "h6"], ["h1", "h2", "h3", "h4", "h5", "h6"]],
  1709 		[["li"], ["li"]],
  1710 		[["nobr"], ["nobr"]],
  1711 		[namesOfElementsWithInlineContents, prohibitedParagraphChildNames],
  1712 		[["td", "th"], ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]],
  1713 	];
  1714 	for (var i = 0; i < table.length; i++) {
  1715 		if (table[i][0].indexOf(parent_) != -1
  1716 		&& table[i][1].indexOf(child) != -1) {
  1717 			return false;
  1721 	// "Return true."
  1722 	return true;
  1726 //@}
  1728 //////////////////////////////////////
  1729 ///// Inline formatting commands /////
  1730 //////////////////////////////////////
  1732 ///// Inline formatting command definitions /////
  1733 //@{
  1735 // "A node node is effectively contained in a range range if range is not
  1736 // collapsed, and at least one of the following holds:"
  1737 function isEffectivelyContained(node, range) {
  1738 	if (range.collapsed) {
  1739 		return false;
  1742 	// "node is contained in range."
  1743 	if (isContained(node, range)) {
  1744 		return true;
  1747 	// "node is range's start node, it is a Text node, and its length is
  1748 	// different from range's start offset."
  1749 	if (node == range.startContainer
  1750 	&& node.nodeType == Node.TEXT_NODE
  1751 	&& getNodeLength(node) != range.startOffset) {
  1752 		return true;
  1755 	// "node is range's end node, it is a Text node, and range's end offset is
  1756 	// not 0."
  1757 	if (node == range.endContainer
  1758 	&& node.nodeType == Node.TEXT_NODE
  1759 	&& range.endOffset != 0) {
  1760 		return true;
  1763 	// "node has at least one child; and all its children are effectively
  1764 	// contained in range; and either range's start node is not a descendant of
  1765 	// node or is not a Text node or range's start offset is zero; and either
  1766 	// range's end node is not a descendant of node or is not a Text node or
  1767 	// range's end offset is its end node's length."
  1768 	if (node.hasChildNodes()
  1769 	&& [].every.call(node.childNodes, function(child) { return isEffectivelyContained(child, range) })
  1770 	&& (!isDescendant(range.startContainer, node)
  1771 	|| range.startContainer.nodeType != Node.TEXT_NODE
  1772 	|| range.startOffset == 0)
  1773 	&& (!isDescendant(range.endContainer, node)
  1774 	|| range.endContainer.nodeType != Node.TEXT_NODE
  1775 	|| range.endOffset == getNodeLength(range.endContainer))) {
  1776 		return true;
  1779 	return false;
  1782 // Like get(All)ContainedNodes(), but for effectively contained nodes.
  1783 function getEffectivelyContainedNodes(range, condition) {
  1784 	if (typeof condition == "undefined") {
  1785 		condition = function() { return true };
  1787 	var node = range.startContainer;
  1788 	while (isEffectivelyContained(node.parentNode, range)) {
  1789 		node = node.parentNode;
  1792 	var stop = nextNodeDescendants(range.endContainer);
  1794 	var nodeList = [];
  1795 	while (isBefore(node, stop)) {
  1796 		if (isEffectivelyContained(node, range)
  1797 		&& condition(node)) {
  1798 			nodeList.push(node);
  1799 			node = nextNodeDescendants(node);
  1800 			continue;
  1802 		node = nextNode(node);
  1804 	return nodeList;
  1807 function getAllEffectivelyContainedNodes(range, condition) {
  1808 	if (typeof condition == "undefined") {
  1809 		condition = function() { return true };
  1811 	var node = range.startContainer;
  1812 	while (isEffectivelyContained(node.parentNode, range)) {
  1813 		node = node.parentNode;
  1816 	var stop = nextNodeDescendants(range.endContainer);
  1818 	var nodeList = [];
  1819 	while (isBefore(node, stop)) {
  1820 		if (isEffectivelyContained(node, range)
  1821 		&& condition(node)) {
  1822 			nodeList.push(node);
  1824 		node = nextNode(node);
  1826 	return nodeList;
  1829 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element
  1830 // with no attributes except possibly style; or a font element with no
  1831 // attributes except possibly style, color, face, and/or size; or an a element
  1832 // with no attributes except possibly style and/or href."
  1833 function isModifiableElement(node) {
  1834 	if (!isHtmlElement(node)) {
  1835 		return false;
  1838 	if (["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) != -1) {
  1839 		if (node.attributes.length == 0) {
  1840 			return true;
  1843 		if (node.attributes.length == 1
  1844 		&& node.hasAttribute("style")) {
  1845 			return true;
  1849 	if (node.tagName == "FONT" || node.tagName == "A") {
  1850 		var numAttrs = node.attributes.length;
  1852 		if (node.hasAttribute("style")) {
  1853 			numAttrs--;
  1856 		if (node.tagName == "FONT") {
  1857 			if (node.hasAttribute("color")) {
  1858 				numAttrs--;
  1861 			if (node.hasAttribute("face")) {
  1862 				numAttrs--;
  1865 			if (node.hasAttribute("size")) {
  1866 				numAttrs--;
  1870 		if (node.tagName == "A"
  1871 		&& node.hasAttribute("href")) {
  1872 			numAttrs--;
  1875 		if (numAttrs == 0) {
  1876 			return true;
  1880 	return false;
  1883 function isSimpleModifiableElement(node) {
  1884 	// "A simple modifiable element is an HTML element for which at least one
  1885 	// of the following holds:"
  1886 	if (!isHtmlElement(node)) {
  1887 		return false;
  1890 	// Only these elements can possibly be a simple modifiable element.
  1891 	if (["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"].indexOf(node.tagName) == -1) {
  1892 		return false;
  1895 	// "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
  1896 	// element with no attributes."
  1897 	if (node.attributes.length == 0) {
  1898 		return true;
  1901 	// If it's got more than one attribute, everything after this fails.
  1902 	if (node.attributes.length > 1) {
  1903 		return false;
  1906 	// "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
  1907 	// element with exactly one attribute, which is style, which sets no CSS
  1908 	// properties (including invalid or unrecognized properties)."
  1909 	//
  1910 	// Not gonna try for invalid or unrecognized.
  1911 	if (node.hasAttribute("style")
  1912 	&& node.style.length == 0) {
  1913 		return true;
  1916 	// "It is an a element with exactly one attribute, which is href."
  1917 	if (node.tagName == "A"
  1918 	&& node.hasAttribute("href")) {
  1919 		return true;
  1922 	// "It is a font element with exactly one attribute, which is either color,
  1923 	// face, or size."
  1924 	if (node.tagName == "FONT"
  1925 	&& (node.hasAttribute("color")
  1926 	|| node.hasAttribute("face")
  1927 	|| node.hasAttribute("size")
  1928 	)) {
  1929 		return true;
  1932 	// "It is a b or strong element with exactly one attribute, which is style,
  1933 	// and the style attribute sets exactly one CSS property (including invalid
  1934 	// or unrecognized properties), which is "font-weight"."
  1935 	if ((node.tagName == "B" || node.tagName == "STRONG")
  1936 	&& node.hasAttribute("style")
  1937 	&& node.style.length == 1
  1938 	&& node.style.fontWeight != "") {
  1939 		return true;
  1942 	// "It is an i or em element with exactly one attribute, which is style,
  1943 	// and the style attribute sets exactly one CSS property (including invalid
  1944 	// or unrecognized properties), which is "font-style"."
  1945 	if ((node.tagName == "I" || node.tagName == "EM")
  1946 	&& node.hasAttribute("style")
  1947 	&& node.style.length == 1
  1948 	&& node.style.fontStyle != "") {
  1949 		return true;
  1952 	// "It is an a, font, or span element with exactly one attribute, which is
  1953 	// style, and the style attribute sets exactly one CSS property (including
  1954 	// invalid or unrecognized properties), and that property is not
  1955 	// "text-decoration"."
  1956 	if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN")
  1957 	&& node.hasAttribute("style")
  1958 	&& node.style.length == 1
  1959 	&& node.style.textDecoration == "") {
  1960 		return true;
  1963 	// "It is an a, font, s, span, strike, or u element with exactly one
  1964 	// attribute, which is style, and the style attribute sets exactly one CSS
  1965 	// property (including invalid or unrecognized properties), which is
  1966 	// "text-decoration", which is set to "line-through" or "underline" or
  1967 	// "overline" or "none"."
  1968 	//
  1969 	// The weird extra node.style.length check is for Firefox, which as of
  1970 	// 8.0a2 has annoying and weird behavior here.
  1971 	if (["A", "FONT", "S", "SPAN", "STRIKE", "U"].indexOf(node.tagName) != -1
  1972 	&& node.hasAttribute("style")
  1973 	&& (node.style.length == 1
  1974 	|| (node.style.length == 4
  1975 		&& "MozTextBlink" in node.style
  1976 		&& "MozTextDecorationColor" in node.style
  1977 		&& "MozTextDecorationLine" in node.style
  1978 		&& "MozTextDecorationStyle" in node.style)
  1980 	&& (node.style.textDecoration == "line-through"
  1981 	|| node.style.textDecoration == "underline"
  1982 	|| node.style.textDecoration == "overline"
  1983 	|| node.style.textDecoration == "none")) {
  1984 		return true;
  1987 	return false;
  1990 // "A formattable node is an editable visible node that is either a Text node,
  1991 // an img, or a br."
  1992 function isFormattableNode(node) {
  1993 	return isEditable(node)
  1994 		&& isVisible(node)
  1995 		&& (node.nodeType == Node.TEXT_NODE
  1996 		|| isHtmlElement(node, ["img", "br"]));
  1999 // "Two quantities are equivalent values for a command if either both are null,
  2000 // or both are strings and they're equal and the command does not define any
  2001 // equivalent values, or both are strings and the command defines equivalent
  2002 // values and they match the definition."
  2003 function areEquivalentValues(command, val1, val2) {
  2004 	if (val1 === null && val2 === null) {
  2005 		return true;
  2008 	if (typeof val1 == "string"
  2009 	&& typeof val2 == "string"
  2010 	&& val1 == val2
  2011 	&& !("equivalentValues" in commands[command])) {
  2012 		return true;
  2015 	if (typeof val1 == "string"
  2016 	&& typeof val2 == "string"
  2017 	&& "equivalentValues" in commands[command]
  2018 	&& commands[command].equivalentValues(val1, val2)) {
  2019 		return true;
  2022 	return false;
  2025 // "Two quantities are loosely equivalent values for a command if either they
  2026 // are equivalent values for the command, or if the command is the fontSize
  2027 // command; one of the quantities is one of "x-small", "small", "medium",
  2028 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
  2029 // the resolved value of "font-size" on a font element whose size attribute has
  2030 // the corresponding value set ("1" through "7" respectively)."
  2031 function areLooselyEquivalentValues(command, val1, val2) {
  2032 	if (areEquivalentValues(command, val1, val2)) {
  2033 		return true;
  2036 	if (command != "fontsize"
  2037 	|| typeof val1 != "string"
  2038 	|| typeof val2 != "string") {
  2039 		return false;
  2042 	// Static variables in JavaScript?
  2043 	var callee = areLooselyEquivalentValues;
  2044 	if (callee.sizeMap === undefined) {
  2045 		callee.sizeMap = {};
  2046 		var font = document.createElement("font");
  2047 		document.body.appendChild(font);
  2048 		["x-small", "small", "medium", "large", "x-large", "xx-large",
  2049 		"xxx-large"].forEach(function(keyword) {
  2050 			font.size = cssSizeToLegacy(keyword);
  2051 			callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
  2052 		});
  2053 		document.body.removeChild(font);
  2056 	return val1 === callee.sizeMap[val2]
  2057 		|| val2 === callee.sizeMap[val1];
  2060 //@}
  2061 ///// Assorted inline formatting command algorithms /////
  2062 //@{
  2064 function getEffectiveCommandValue(node, command) {
  2065 	// "If neither node nor its parent is an Element, return null."
  2066 	if (node.nodeType != Node.ELEMENT_NODE
  2067 	&& (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) {
  2068 		return null;
  2071 	// "If node is not an Element, return the effective command value of its
  2072 	// parent for command."
  2073 	if (node.nodeType != Node.ELEMENT_NODE) {
  2074 		return getEffectiveCommandValue(node.parentNode, command);
  2077 	// "If command is "createLink" or "unlink":"
  2078 	if (command == "createlink" || command == "unlink") {
  2079 		// "While node is not null, and is not an a element that has an href
  2080 		// attribute, set node to its parent."
  2081 		while (node
  2082 		&& (!isHtmlElement(node)
  2083 		|| node.tagName != "A"
  2084 		|| !node.hasAttribute("href"))) {
  2085 			node = node.parentNode;
  2088 		// "If node is null, return null."
  2089 		if (!node) {
  2090 			return null;
  2093 		// "Return the value of node's href attribute."
  2094 		return node.getAttribute("href");
  2097 	// "If command is "backColor" or "hiliteColor":"
  2098 	if (command == "backcolor"
  2099 	|| command == "hilitecolor") {
  2100 		// "While the resolved value of "background-color" on node is any
  2101 		// fully transparent value, and node's parent is an Element, set
  2102 		// node to its parent."
  2103 		//
  2104 		// Another lame hack to avoid flawed APIs.
  2105 		while ((getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
  2106 		|| getComputedStyle(node).backgroundColor === ""
  2107 		|| getComputedStyle(node).backgroundColor == "transparent")
  2108 		&& node.parentNode
  2109 		&& node.parentNode.nodeType == Node.ELEMENT_NODE) {
  2110 			node = node.parentNode;
  2113 		// "Return the resolved value of "background-color" for node."
  2114 		return getComputedStyle(node).backgroundColor;
  2117 	// "If command is "subscript" or "superscript":"
  2118 	if (command == "subscript" || command == "superscript") {
  2119 		// "Let affected by subscript and affected by superscript be two
  2120 		// boolean variables, both initially false."
  2121 		var affectedBySubscript = false;
  2122 		var affectedBySuperscript = false;
  2124 		// "While node is an inline node:"
  2125 		while (isInlineNode(node)) {
  2126 			var verticalAlign = getComputedStyle(node).verticalAlign;
  2128 			// "If node is a sub, set affected by subscript to true."
  2129 			if (isHtmlElement(node, "sub")) {
  2130 				affectedBySubscript = true;
  2131 			// "Otherwise, if node is a sup, set affected by superscript to
  2132 			// true."
  2133 			} else if (isHtmlElement(node, "sup")) {
  2134 				affectedBySuperscript = true;
  2137 			// "Set node to its parent."
  2138 			node = node.parentNode;
  2141 		// "If affected by subscript and affected by superscript are both true,
  2142 		// return the string "mixed"."
  2143 		if (affectedBySubscript && affectedBySuperscript) {
  2144 			return "mixed";
  2147 		// "If affected by subscript is true, return "subscript"."
  2148 		if (affectedBySubscript) {
  2149 			return "subscript";
  2152 		// "If affected by superscript is true, return "superscript"."
  2153 		if (affectedBySuperscript) {
  2154 			return "superscript";
  2157 		// "Return null."
  2158 		return null;
  2161 	// "If command is "strikethrough", and the "text-decoration" property of
  2162 	// node or any of its ancestors has resolved value containing
  2163 	// "line-through", return "line-through". Otherwise, return null."
  2164 	if (command == "strikethrough") {
  2165 		do {
  2166 			if (getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
  2167 				return "line-through";
  2169 			node = node.parentNode;
  2170 		} while (node && node.nodeType == Node.ELEMENT_NODE);
  2171 		return null;
  2174 	// "If command is "underline", and the "text-decoration" property of node
  2175 	// or any of its ancestors has resolved value containing "underline",
  2176 	// return "underline". Otherwise, return null."
  2177 	if (command == "underline") {
  2178 		do {
  2179 			if (getComputedStyle(node).textDecoration.indexOf("underline") != -1) {
  2180 				return "underline";
  2182 			node = node.parentNode;
  2183 		} while (node && node.nodeType == Node.ELEMENT_NODE);
  2184 		return null;
  2187 	if (!("relevantCssProperty" in commands[command])) {
  2188 		throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue";
  2191 	// "Return the resolved value for node of the relevant CSS property for
  2192 	// command."
  2193 	return getComputedStyle(node)[commands[command].relevantCssProperty];
  2196 function getSpecifiedCommandValue(element, command) {
  2197 	// "If command is "backColor" or "hiliteColor" and element's display
  2198 	// property does not have resolved value "inline", return null."
  2199 	if ((command == "backcolor" || command == "hilitecolor")
  2200 	&& getComputedStyle(element).display != "inline") {
  2201 		return null;
  2204 	// "If command is "createLink" or "unlink":"
  2205 	if (command == "createlink" || command == "unlink") {
  2206 		// "If element is an a element and has an href attribute, return the
  2207 		// value of that attribute."
  2208 		if (isHtmlElement(element)
  2209 		&& element.tagName == "A"
  2210 		&& element.hasAttribute("href")) {
  2211 			return element.getAttribute("href");
  2214 		// "Return null."
  2215 		return null;
  2218 	// "If command is "subscript" or "superscript":"
  2219 	if (command == "subscript" || command == "superscript") {
  2220 		// "If element is a sup, return "superscript"."
  2221 		if (isHtmlElement(element, "sup")) {
  2222 			return "superscript";
  2225 		// "If element is a sub, return "subscript"."
  2226 		if (isHtmlElement(element, "sub")) {
  2227 			return "subscript";
  2230 		// "Return null."
  2231 		return null;
  2234 	// "If command is "strikethrough", and element has a style attribute set,
  2235 	// and that attribute sets "text-decoration":"
  2236 	if (command == "strikethrough"
  2237 	&& element.style.textDecoration != "") {
  2238 		// "If element's style attribute sets "text-decoration" to a value
  2239 		// containing "line-through", return "line-through"."
  2240 		if (element.style.textDecoration.indexOf("line-through") != -1) {
  2241 			return "line-through";
  2244 		// "Return null."
  2245 		return null;
  2248 	// "If command is "strikethrough" and element is a s or strike element,
  2249 	// return "line-through"."
  2250 	if (command == "strikethrough"
  2251 	&& isHtmlElement(element, ["S", "STRIKE"])) {
  2252 		return "line-through";
  2255 	// "If command is "underline", and element has a style attribute set, and
  2256 	// that attribute sets "text-decoration":"
  2257 	if (command == "underline"
  2258 	&& element.style.textDecoration != "") {
  2259 		// "If element's style attribute sets "text-decoration" to a value
  2260 		// containing "underline", return "underline"."
  2261 		if (element.style.textDecoration.indexOf("underline") != -1) {
  2262 			return "underline";
  2265 		// "Return null."
  2266 		return null;
  2269 	// "If command is "underline" and element is a u element, return
  2270 	// "underline"."
  2271 	if (command == "underline"
  2272 	&& isHtmlElement(element, "U")) {
  2273 		return "underline";
  2276 	// "Let property be the relevant CSS property for command."
  2277 	var property = commands[command].relevantCssProperty;
  2279 	// "If property is null, return null."
  2280 	if (property === null) {
  2281 		return null;
  2284 	// "If element has a style attribute set, and that attribute has the
  2285 	// effect of setting property, return the value that it sets property to."
  2286 	if (element.style[property] != "") {
  2287 		return element.style[property];
  2290 	// "If element is a font element that has an attribute whose effect is
  2291 	// to create a presentational hint for property, return the value that the
  2292 	// hint sets property to.  (For a size of 7, this will be the non-CSS value
  2293 	// "xxx-large".)"
  2294 	if (isHtmlNamespace(element.namespaceURI)
  2295 	&& element.tagName == "FONT") {
  2296 		if (property == "color" && element.hasAttribute("color")) {
  2297 			return element.color;
  2299 		if (property == "fontFamily" && element.hasAttribute("face")) {
  2300 			return element.face;
  2302 		if (property == "fontSize" && element.hasAttribute("size")) {
  2303 			// This is not even close to correct in general.
  2304 			var size = parseInt(element.size);
  2305 			if (size < 1) {
  2306 				size = 1;
  2308 			if (size > 7) {
  2309 				size = 7;
  2311 			return {
  2312 				1: "x-small",
  2313 				2: "small",
  2314 				3: "medium",
  2315 				4: "large",
  2316 				5: "x-large",
  2317 				6: "xx-large",
  2318 				7: "xxx-large"
  2319 			}[size];
  2323 	// "If element is in the following list, and property is equal to the
  2324 	// CSS property name listed for it, return the string listed for it."
  2325 	//
  2326 	// A list follows, whose meaning is copied here.
  2327 	if (property == "fontWeight"
  2328 	&& (element.tagName == "B" || element.tagName == "STRONG")) {
  2329 		return "bold";
  2331 	if (property == "fontStyle"
  2332 	&& (element.tagName == "I" || element.tagName == "EM")) {
  2333 		return "italic";
  2336 	// "Return null."
  2337 	return null;
  2340 function reorderModifiableDescendants(node, command, newValue) {
  2341 	// "Let candidate equal node."
  2342 	var candidate = node;
  2344 	// "While candidate is a modifiable element, and candidate has exactly one
  2345 	// child, and that child is also a modifiable element, and candidate is not
  2346 	// a simple modifiable element or candidate's specified command value for
  2347 	// command is not equivalent to new value, set candidate to its child."
  2348 	while (isModifiableElement(candidate)
  2349 	&& candidate.childNodes.length == 1
  2350 	&& isModifiableElement(candidate.firstChild)
  2351 	&& (!isSimpleModifiableElement(candidate)
  2352 	|| !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
  2353 		candidate = candidate.firstChild;
  2356 	// "If candidate is node, or is not a simple modifiable element, or its
  2357 	// specified command value is not equivalent to new value, or its effective
  2358 	// command value is not loosely equivalent to new value, abort these
  2359 	// steps."
  2360 	if (candidate == node
  2361 	|| !isSimpleModifiableElement(candidate)
  2362 	|| !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
  2363 	|| !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
  2364 		return;
  2367 	// "While candidate has children, insert the first child of candidate into
  2368 	// candidate's parent immediately before candidate, preserving ranges."
  2369 	while (candidate.hasChildNodes()) {
  2370 		movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate));
  2373 	// "Insert candidate into node's parent immediately after node."
  2374 	node.parentNode.insertBefore(candidate, node.nextSibling);
  2376 	// "Append the node as the last child of candidate, preserving ranges."
  2377 	movePreservingRanges(node, candidate, -1);
  2380 function recordValues(nodeList) {
  2381 	// "Let values be a list of (node, command, specified command value)
  2382 	// triples, initially empty."
  2383 	var values = [];
  2385 	// "For each node in node list, for each command in the list "subscript",
  2386 	// "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
  2387 	// "strikethrough", and "underline" in that order:"
  2388 	nodeList.forEach(function(node) {
  2389 		["subscript", "bold", "fontname", "fontsize", "forecolor",
  2390 		"hilitecolor", "italic", "strikethrough", "underline"].forEach(function(command) {
  2391 			// "Let ancestor equal node."
  2392 			var ancestor = node;
  2394 			// "If ancestor is not an Element, set it to its parent."
  2395 			if (ancestor.nodeType != Node.ELEMENT_NODE) {
  2396 				ancestor = ancestor.parentNode;
  2399 			// "While ancestor is an Element and its specified command value
  2400 			// for command is null, set it to its parent."
  2401 			while (ancestor
  2402 			&& ancestor.nodeType == Node.ELEMENT_NODE
  2403 			&& getSpecifiedCommandValue(ancestor, command) === null) {
  2404 				ancestor = ancestor.parentNode;
  2407 			// "If ancestor is an Element, add (node, command, ancestor's
  2408 			// specified command value for command) to values. Otherwise add
  2409 			// (node, command, null) to values."
  2410 			if (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
  2411 				values.push([node, command, getSpecifiedCommandValue(ancestor, command)]);
  2412 			} else {
  2413 				values.push([node, command, null]);
  2415 		});
  2416 	});
  2418 	// "Return values."
  2419 	return values;
  2422 function restoreValues(values) {
  2423 	// "For each (node, command, value) triple in values:"
  2424 	values.forEach(function(triple) {
  2425 		var node = triple[0];
  2426 		var command = triple[1];
  2427 		var value = triple[2];
  2429 		// "Let ancestor equal node."
  2430 		var ancestor = node;
  2432 		// "If ancestor is not an Element, set it to its parent."
  2433 		if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) {
  2434 			ancestor = ancestor.parentNode;
  2437 		// "While ancestor is an Element and its specified command value for
  2438 		// command is null, set it to its parent."
  2439 		while (ancestor
  2440 		&& ancestor.nodeType == Node.ELEMENT_NODE
  2441 		&& getSpecifiedCommandValue(ancestor, command) === null) {
  2442 			ancestor = ancestor.parentNode;
  2445 		// "If value is null and ancestor is an Element, push down values on
  2446 		// node for command, with new value null."
  2447 		if (value === null
  2448 		&& ancestor
  2449 		&& ancestor.nodeType == Node.ELEMENT_NODE) {
  2450 			pushDownValues(node, command, null);
  2452 		// "Otherwise, if ancestor is an Element and its specified command
  2453 		// value for command is not equivalent to value, or if ancestor is not
  2454 		// an Element and value is not null, force the value of command to
  2455 		// value on node."
  2456 		} else if ((ancestor
  2457 		&& ancestor.nodeType == Node.ELEMENT_NODE
  2458 		&& !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
  2459 		|| ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
  2460 		&& value !== null)) {
  2461 			forceValue(node, command, value);
  2463 	});
  2467 //@}
  2468 ///// Clearing an element's value /////
  2469 //@{
  2471 function clearValue(element, command) {
  2472 	// "If element is not editable, return the empty list."
  2473 	if (!isEditable(element)) {
  2474 		return [];
  2477 	// "If element's specified command value for command is null, return the
  2478 	// empty list."
  2479 	if (getSpecifiedCommandValue(element, command) === null) {
  2480 		return [];
  2483 	// "If element is a simple modifiable element:"
  2484 	if (isSimpleModifiableElement(element)) {
  2485 		// "Let children be the children of element."
  2486 		var children = Array.prototype.slice.call(element.childNodes);
  2488 		// "For each child in children, insert child into element's parent
  2489 		// immediately before element, preserving ranges."
  2490 		for (var i = 0; i < children.length; i++) {
  2491 			movePreservingRanges(children[i], element.parentNode, getNodeIndex(element));
  2494 		// "Remove element from its parent."
  2495 		element.parentNode.removeChild(element);
  2497 		// "Return children."
  2498 		return children;
  2501 	// "If command is "strikethrough", and element has a style attribute that
  2502 	// sets "text-decoration" to some value containing "line-through", delete
  2503 	// "line-through" from the value."
  2504 	if (command == "strikethrough"
  2505 	&& element.style.textDecoration.indexOf("line-through") != -1) {
  2506 		if (element.style.textDecoration == "line-through") {
  2507 			element.style.textDecoration = "";
  2508 		} else {
  2509 			element.style.textDecoration = element.style.textDecoration.replace("line-through", "");
  2511 		if (element.getAttribute("style") == "") {
  2512 			element.removeAttribute("style");
  2516 	// "If command is "underline", and element has a style attribute that sets
  2517 	// "text-decoration" to some value containing "underline", delete
  2518 	// "underline" from the value."
  2519 	if (command == "underline"
  2520 	&& element.style.textDecoration.indexOf("underline") != -1) {
  2521 		if (element.style.textDecoration == "underline") {
  2522 			element.style.textDecoration = "";
  2523 		} else {
  2524 			element.style.textDecoration = element.style.textDecoration.replace("underline", "");
  2526 		if (element.getAttribute("style") == "") {
  2527 			element.removeAttribute("style");
  2531 	// "If the relevant CSS property for command is not null, unset the CSS
  2532 	// property property of element."
  2533 	if (commands[command].relevantCssProperty !== null) {
  2534 		element.style[commands[command].relevantCssProperty] = '';
  2535 		if (element.getAttribute("style") == "") {
  2536 			element.removeAttribute("style");
  2540 	// "If element is a font element:"
  2541 	if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
  2542 		// "If command is "foreColor", unset element's color attribute, if set."
  2543 		if (command == "forecolor") {
  2544 			element.removeAttribute("color");
  2547 		// "If command is "fontName", unset element's face attribute, if set."
  2548 		if (command == "fontname") {
  2549 			element.removeAttribute("face");
  2552 		// "If command is "fontSize", unset element's size attribute, if set."
  2553 		if (command == "fontsize") {
  2554 			element.removeAttribute("size");
  2558 	// "If element is an a element and command is "createLink" or "unlink",
  2559 	// unset the href property of element."
  2560 	if (isHtmlElement(element, "A")
  2561 	&& (command == "createlink" || command == "unlink")) {
  2562 		element.removeAttribute("href");
  2565 	// "If element's specified command value for command is null, return the
  2566 	// empty list."
  2567 	if (getSpecifiedCommandValue(element, command) === null) {
  2568 		return [];
  2571 	// "Set the tag name of element to "span", and return the one-node list
  2572 	// consisting of the result."
  2573 	return [setTagName(element, "span")];
  2577 //@}
  2578 ///// Pushing down values /////
  2579 //@{
  2581 function pushDownValues(node, command, newValue) {
  2582 	// "If node's parent is not an Element, abort this algorithm."
  2583 	if (!node.parentNode
  2584 	|| node.parentNode.nodeType != Node.ELEMENT_NODE) {
  2585 		return;
  2588 	// "If the effective command value of command is loosely equivalent to new
  2589 	// value on node, abort this algorithm."
  2590 	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
  2591 		return;
  2594 	// "Let current ancestor be node's parent."
  2595 	var currentAncestor = node.parentNode;
  2597 	// "Let ancestor list be a list of Nodes, initially empty."
  2598 	var ancestorList = [];
  2600 	// "While current ancestor is an editable Element and the effective command
  2601 	// value of command is not loosely equivalent to new value on it, append
  2602 	// current ancestor to ancestor list, then set current ancestor to its
  2603 	// parent."
  2604 	while (isEditable(currentAncestor)
  2605 	&& currentAncestor.nodeType == Node.ELEMENT_NODE
  2606 	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
  2607 		ancestorList.push(currentAncestor);
  2608 		currentAncestor = currentAncestor.parentNode;
  2611 	// "If ancestor list is empty, abort this algorithm."
  2612 	if (!ancestorList.length) {
  2613 		return;
  2616 	// "Let propagated value be the specified command value of command on the
  2617 	// last member of ancestor list."
  2618 	var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command);
  2620 	// "If propagated value is null and is not equal to new value, abort this
  2621 	// algorithm."
  2622 	if (propagatedValue === null && propagatedValue != newValue) {
  2623 		return;
  2626 	// "If the effective command value for the parent of the last member of
  2627 	// ancestor list is not loosely equivalent to new value, and new value is
  2628 	// not null, abort this algorithm."
  2629 	if (newValue !== null
  2630 	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
  2631 		return;
  2634 	// "While ancestor list is not empty:"
  2635 	while (ancestorList.length) {
  2636 		// "Let current ancestor be the last member of ancestor list."
  2637 		// "Remove the last member from ancestor list."
  2638 		var currentAncestor = ancestorList.pop();
  2640 		// "If the specified command value of current ancestor for command is
  2641 		// not null, set propagated value to that value."
  2642 		if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
  2643 			propagatedValue = getSpecifiedCommandValue(currentAncestor, command);
  2646 		// "Let children be the children of current ancestor."
  2647 		var children = Array.prototype.slice.call(currentAncestor.childNodes);
  2649 		// "If the specified command value of current ancestor for command is
  2650 		// not null, clear the value of current ancestor."
  2651 		if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
  2652 			clearValue(currentAncestor, command);
  2655 		// "For every child in children:"
  2656 		for (var i = 0; i < children.length; i++) {
  2657 			var child = children[i];
  2659 			// "If child is node, continue with the next child."
  2660 			if (child == node) {
  2661 				continue;
  2664 			// "If child is an Element whose specified command value for
  2665 			// command is neither null nor equivalent to propagated value,
  2666 			// continue with the next child."
  2667 			if (child.nodeType == Node.ELEMENT_NODE
  2668 			&& getSpecifiedCommandValue(child, command) !== null
  2669 			&& !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
  2670 				continue;
  2673 			// "If child is the last member of ancestor list, continue with the
  2674 			// next child."
  2675 			if (child == ancestorList[ancestorList.length - 1]) {
  2676 				continue;
  2679 			// "Force the value of child, with command as in this algorithm
  2680 			// and new value equal to propagated value."
  2681 			forceValue(child, command, propagatedValue);
  2687 //@}
  2688 ///// Forcing the value of a node /////
  2689 //@{
  2691 function forceValue(node, command, newValue) {
  2692 	// "If node's parent is null, abort this algorithm."
  2693 	if (!node.parentNode) {
  2694 		return;
  2697 	// "If new value is null, abort this algorithm."
  2698 	if (newValue === null) {
  2699 		return;
  2702 	// "If node is an allowed child of "span":"
  2703 	if (isAllowedChild(node, "span")) {
  2704 		// "Reorder modifiable descendants of node's previousSibling."
  2705 		reorderModifiableDescendants(node.previousSibling, command, newValue);
  2707 		// "Reorder modifiable descendants of node's nextSibling."
  2708 		reorderModifiableDescendants(node.nextSibling, command, newValue);
  2710 		// "Wrap the one-node list consisting of node, with sibling criteria
  2711 		// returning true for a simple modifiable element whose specified
  2712 		// command value is equivalent to new value and whose effective command
  2713 		// value is loosely equivalent to new value and false otherwise, and
  2714 		// with new parent instructions returning null."
  2715 		wrap([node],
  2716 			function(node) {
  2717 				return isSimpleModifiableElement(node)
  2718 					&& areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
  2719 					&& areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
  2720 			},
  2721 			function() { return null }
  2722 		);
  2725 	// "If node is invisible, abort this algorithm."
  2726 	if (isInvisible(node)) {
  2727 		return;
  2730 	// "If the effective command value of command is loosely equivalent to new
  2731 	// value on node, abort this algorithm."
  2732 	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
  2733 		return;
  2736 	// "If node is not an allowed child of "span":"
  2737 	if (!isAllowedChild(node, "span")) {
  2738 		// "Let children be all children of node, omitting any that are
  2739 		// Elements whose specified command value for command is neither null
  2740 		// nor equivalent to new value."
  2741 		var children = [];
  2742 		for (var i = 0; i < node.childNodes.length; i++) {
  2743 			if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
  2744 				var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
  2746 				if (specifiedValue !== null
  2747 				&& !areEquivalentValues(command, newValue, specifiedValue)) {
  2748 					continue;
  2751 			children.push(node.childNodes[i]);
  2754 		// "Force the value of each Node in children, with command and new
  2755 		// value as in this invocation of the algorithm."
  2756 		for (var i = 0; i < children.length; i++) {
  2757 			forceValue(children[i], command, newValue);
  2760 		// "Abort this algorithm."
  2761 		return;
  2764 	// "If the effective command value of command is loosely equivalent to new
  2765 	// value on node, abort this algorithm."
  2766 	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
  2767 		return;
  2770 	// "Let new parent be null."
  2771 	var newParent = null;
  2773 	// "If the CSS styling flag is false:"
  2774 	if (!cssStylingFlag) {
  2775 		// "If command is "bold" and new value is "bold", let new parent be the
  2776 		// result of calling createElement("b") on the ownerDocument of node."
  2777 		if (command == "bold" && (newValue == "bold" || newValue == "700")) {
  2778 			newParent = node.ownerDocument.createElement("b");
  2781 		// "If command is "italic" and new value is "italic", let new parent be
  2782 		// the result of calling createElement("i") on the ownerDocument of
  2783 		// node."
  2784 		if (command == "italic" && newValue == "italic") {
  2785 			newParent = node.ownerDocument.createElement("i");
  2788 		// "If command is "strikethrough" and new value is "line-through", let
  2789 		// new parent be the result of calling createElement("s") on the
  2790 		// ownerDocument of node."
  2791 		if (command == "strikethrough" && newValue == "line-through") {
  2792 			newParent = node.ownerDocument.createElement("s");
  2795 		// "If command is "underline" and new value is "underline", let new
  2796 		// parent be the result of calling createElement("u") on the
  2797 		// ownerDocument of node."
  2798 		if (command == "underline" && newValue == "underline") {
  2799 			newParent = node.ownerDocument.createElement("u");
  2802 		// "If command is "foreColor", and new value is fully opaque with red,
  2803 		// green, and blue components in the range 0 to 255:"
  2804 		if (command == "forecolor" && parseSimpleColor(newValue)) {
  2805 			// "Let new parent be the result of calling createElement("font")
  2806 			// on the ownerDocument of node."
  2807 			newParent = node.ownerDocument.createElement("font");
  2809 			// "Set the color attribute of new parent to the result of applying
  2810 			// the rules for serializing simple color values to new value
  2811 			// (interpreted as a simple color)."
  2812 			newParent.setAttribute("color", parseSimpleColor(newValue));
  2815 		// "If command is "fontName", let new parent be the result of calling
  2816 		// createElement("font") on the ownerDocument of node, then set the
  2817 		// face attribute of new parent to new value."
  2818 		if (command == "fontname") {
  2819 			newParent = node.ownerDocument.createElement("font");
  2820 			newParent.face = newValue;
  2824 	// "If command is "createLink" or "unlink":"
  2825 	if (command == "createlink" || command == "unlink") {
  2826 		// "Let new parent be the result of calling createElement("a") on the
  2827 		// ownerDocument of node."
  2828 		newParent = node.ownerDocument.createElement("a");
  2830 		// "Set the href attribute of new parent to new value."
  2831 		newParent.setAttribute("href", newValue);
  2833 		// "Let ancestor be node's parent."
  2834 		var ancestor = node.parentNode;
  2836 		// "While ancestor is not null:"
  2837 		while (ancestor) {
  2838 			// "If ancestor is an a, set the tag name of ancestor to "span",
  2839 			// and let ancestor be the result."
  2840 			if (isHtmlElement(ancestor, "A")) {
  2841 				ancestor = setTagName(ancestor, "span");
  2844 			// "Set ancestor to its parent."
  2845 			ancestor = ancestor.parentNode;
  2849 	// "If command is "fontSize"; and new value is one of "x-small", "small",
  2850 	// "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
  2851 	// CSS styling flag is false, or new value is "xxx-large": let new parent
  2852 	// be the result of calling createElement("font") on the ownerDocument of
  2853 	// node, then set the size attribute of new parent to the number from the
  2854 	// following table based on new value: [table omitted]"
  2855 	if (command == "fontsize"
  2856 	&& ["x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1
  2857 	&& (!cssStylingFlag || newValue == "xxx-large")) {
  2858 		newParent = node.ownerDocument.createElement("font");
  2859 		newParent.size = cssSizeToLegacy(newValue);
  2862 	// "If command is "subscript" or "superscript" and new value is
  2863 	// "subscript", let new parent be the result of calling
  2864 	// createElement("sub") on the ownerDocument of node."
  2865 	if ((command == "subscript" || command == "superscript")
  2866 	&& newValue == "subscript") {
  2867 		newParent = node.ownerDocument.createElement("sub");
  2870 	// "If command is "subscript" or "superscript" and new value is
  2871 	// "superscript", let new parent be the result of calling
  2872 	// createElement("sup") on the ownerDocument of node."
  2873 	if ((command == "subscript" || command == "superscript")
  2874 	&& newValue == "superscript") {
  2875 		newParent = node.ownerDocument.createElement("sup");
  2878 	// "If new parent is null, let new parent be the result of calling
  2879 	// createElement("span") on the ownerDocument of node."
  2880 	if (!newParent) {
  2881 		newParent = node.ownerDocument.createElement("span");
  2884 	// "Insert new parent in node's parent before node."
  2885 	node.parentNode.insertBefore(newParent, node);
  2887 	// "If the effective command value of command for new parent is not loosely
  2888 	// equivalent to new value, and the relevant CSS property for command is
  2889 	// not null, set that CSS property of new parent to new value (if the new
  2890 	// value would be valid)."
  2891 	var property = commands[command].relevantCssProperty;
  2892 	if (property !== null
  2893 	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
  2894 		newParent.style[property] = newValue;
  2897 	// "If command is "strikethrough", and new value is "line-through", and the
  2898 	// effective command value of "strikethrough" for new parent is not
  2899 	// "line-through", set the "text-decoration" property of new parent to
  2900 	// "line-through"."
  2901 	if (command == "strikethrough"
  2902 	&& newValue == "line-through"
  2903 	&& getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
  2904 		newParent.style.textDecoration = "line-through";
  2907 	// "If command is "underline", and new value is "underline", and the
  2908 	// effective command value of "underline" for new parent is not
  2909 	// "underline", set the "text-decoration" property of new parent to
  2910 	// "underline"."
  2911 	if (command == "underline"
  2912 	&& newValue == "underline"
  2913 	&& getEffectiveCommandValue(newParent, "underline") != "underline") {
  2914 		newParent.style.textDecoration = "underline";
  2917 	// "Append node to new parent as its last child, preserving ranges."
  2918 	movePreservingRanges(node, newParent, newParent.childNodes.length);
  2920 	// "If node is an Element and the effective command value of command for
  2921 	// node is not loosely equivalent to new value:"
  2922 	if (node.nodeType == Node.ELEMENT_NODE
  2923 	&& !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
  2924 		// "Insert node into the parent of new parent before new parent,
  2925 		// preserving ranges."
  2926 		movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent));
  2928 		// "Remove new parent from its parent."
  2929 		newParent.parentNode.removeChild(newParent);
  2931 		// "Let children be all children of node, omitting any that are
  2932 		// Elements whose specified command value for command is neither null
  2933 		// nor equivalent to new value."
  2934 		var children = [];
  2935 		for (var i = 0; i < node.childNodes.length; i++) {
  2936 			if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
  2937 				var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
  2939 				if (specifiedValue !== null
  2940 				&& !areEquivalentValues(command, newValue, specifiedValue)) {
  2941 					continue;
  2944 			children.push(node.childNodes[i]);
  2947 		// "Force the value of each Node in children, with command and new
  2948 		// value as in this invocation of the algorithm."
  2949 		for (var i = 0; i < children.length; i++) {
  2950 			forceValue(children[i], command, newValue);
  2956 //@}
  2957 ///// Setting the selection's value /////
  2958 //@{
  2960 function setSelectionValue(command, newValue) {
  2961 	// "If there is no formattable node effectively contained in the active
  2962 	// range:"
  2963 	if (!getAllEffectivelyContainedNodes(getActiveRange())
  2964 	.some(isFormattableNode)) {
  2965 		// "If command has inline command activated values, set the state
  2966 		// override to true if new value is among them and false if it's not."
  2967 		if ("inlineCommandActivatedValues" in commands[command]) {
  2968 			setStateOverride(command, commands[command].inlineCommandActivatedValues
  2969 				.indexOf(newValue) != -1);
  2972 		// "If command is "subscript", unset the state override for
  2973 		// "superscript"."
  2974 		if (command == "subscript") {
  2975 			unsetStateOverride("superscript");
  2978 		// "If command is "superscript", unset the state override for
  2979 		// "subscript"."
  2980 		if (command == "superscript") {
  2981 			unsetStateOverride("subscript");
  2984 		// "If new value is null, unset the value override (if any)."
  2985 		if (newValue === null) {
  2986 			unsetValueOverride(command);
  2988 		// "Otherwise, if command is "createLink" or it has a value specified,
  2989 		// set the value override to new value."
  2990 		} else if (command == "createlink" || "value" in commands[command]) {
  2991 			setValueOverride(command, newValue);
  2994 		// "Abort these steps."
  2995 		return;
  2998 	// "If the active range's start node is an editable Text node, and its
  2999 	// start offset is neither zero nor its start node's length, call
  3000 	// splitText() on the active range's start node, with argument equal to the
  3001 	// active range's start offset. Then set the active range's start node to
  3002 	// the result, and its start offset to zero."
  3003 	if (isEditable(getActiveRange().startContainer)
  3004 	&& getActiveRange().startContainer.nodeType == Node.TEXT_NODE
  3005 	&& getActiveRange().startOffset != 0
  3006 	&& getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
  3007 		// Account for browsers not following range mutation rules
  3008 		var newActiveRange = document.createRange();
  3009 		var newNode;
  3010 		if (getActiveRange().startContainer == getActiveRange().endContainer) {
  3011 			var newEndOffset = getActiveRange().endOffset - getActiveRange().startOffset;
  3012 			newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
  3013 			newActiveRange.setEnd(newNode, newEndOffset);
  3014 			getActiveRange().setEnd(newNode, newEndOffset);
  3015 		} else {
  3016 			newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
  3018 		newActiveRange.setStart(newNode, 0);
  3019 		getSelection().removeAllRanges();
  3020 		getSelection().addRange(newActiveRange);
  3022 		getActiveRange().setStart(newNode, 0);
  3025 	// "If the active range's end node is an editable Text node, and its end
  3026 	// offset is neither zero nor its end node's length, call splitText() on
  3027 	// the active range's end node, with argument equal to the active range's
  3028 	// end offset."
  3029 	if (isEditable(getActiveRange().endContainer)
  3030 	&& getActiveRange().endContainer.nodeType == Node.TEXT_NODE
  3031 	&& getActiveRange().endOffset != 0
  3032 	&& getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
  3033 		// IE seems to mutate the range incorrectly here, so we need correction
  3034 		// here as well.  The active range will be temporarily in orphaned
  3035 		// nodes, so calling getActiveRange() after splitText() but before
  3036 		// fixing the range will throw an exception.
  3037 		var activeRange = getActiveRange();
  3038 		var newStart = [activeRange.startContainer, activeRange.startOffset];
  3039 		var newEnd = [activeRange.endContainer, activeRange.endOffset];
  3040 		activeRange.endContainer.splitText(activeRange.endOffset);
  3041 		activeRange.setStart(newStart[0], newStart[1]);
  3042 		activeRange.setEnd(newEnd[0], newEnd[1]);
  3044 		getSelection().removeAllRanges();
  3045 		getSelection().addRange(activeRange);
  3048 	// "Let element list be all editable Elements effectively contained in the
  3049 	// active range.
  3050 	//
  3051 	// "For each element in element list, clear the value of element."
  3052 	getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
  3053 		return isEditable(node) && node.nodeType == Node.ELEMENT_NODE;
  3054 	}).forEach(function(element) {
  3055 		clearValue(element, command);
  3056 	});
  3058 	// "Let node list be all editable nodes effectively contained in the active
  3059 	// range.
  3060 	//
  3061 	// "For each node in node list:"
  3062 	getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
  3063 		// "Push down values on node."
  3064 		pushDownValues(node, command, newValue);
  3066 		// "If node is an allowed child of span, force the value of node."
  3067 		if (isAllowedChild(node, "span")) {
  3068 			forceValue(node, command, newValue);
  3070 	});
  3074 //@}
  3075 ///// The backColor command /////
  3076 //@{
  3077 commands.backcolor = {
  3078 	// Copy-pasted, same as hiliteColor
  3079 	action: function(value) {
  3080 		// Action is further copy-pasted, same as foreColor
  3082 		// "If value is not a valid CSS color, prepend "#" to it."
  3083 		//
  3084 		// "If value is still not a valid CSS color, or if it is currentColor,
  3085 		// return false."
  3086 		//
  3087 		// Cheap hack for testing, no attempt to be comprehensive.
  3088 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
  3089 			value = "#" + value;
  3091 		if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
  3092 		&& !parseSimpleColor(value)
  3093 		&& value.toLowerCase() != "transparent") {
  3094 			return false;
  3097 		// "Set the selection's value to value."
  3098 		setSelectionValue("backcolor", value);
  3100 		// "Return true."
  3101 		return true;
  3102 	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
  3103 	equivalentValues: function(val1, val2) {
  3104 		// "Either both strings are valid CSS colors and have the same red,
  3105 		// green, blue, and alpha components, or neither string is a valid CSS
  3106 		// color."
  3107 		return normalizeColor(val1) === normalizeColor(val2);
  3108 	},
  3109 };
  3111 //@}
  3112 ///// The bold command /////
  3113 //@{
  3114 commands.bold = {
  3115 	action: function() {
  3116 		// "If queryCommandState("bold") returns true, set the selection's
  3117 		// value to "normal". Otherwise set the selection's value to "bold".
  3118 		// Either way, return true."
  3119 		if (myQueryCommandState("bold")) {
  3120 			setSelectionValue("bold", "normal");
  3121 		} else {
  3122 			setSelectionValue("bold", "bold");
  3124 		return true;
  3125 	}, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
  3126 	relevantCssProperty: "fontWeight",
  3127 	equivalentValues: function(val1, val2) {
  3128 		// "Either the two strings are equal, or one is "bold" and the other is
  3129 		// "700", or one is "normal" and the other is "400"."
  3130 		return val1 == val2
  3131 			|| (val1 == "bold" && val2 == "700")
  3132 			|| (val1 == "700" && val2 == "bold")
  3133 			|| (val1 == "normal" && val2 == "400")
  3134 			|| (val1 == "400" && val2 == "normal");
  3135 	},
  3136 };
  3138 //@}
  3139 ///// The createLink command /////
  3140 //@{
  3141 commands.createlink = {
  3142 	action: function(value) {
  3143 		// "If value is the empty string, return false."
  3144 		if (value === "") {
  3145 			return false;
  3148 		// "For each editable a element that has an href attribute and is an
  3149 		// ancestor of some node effectively contained in the active range, set
  3150 		// that a element's href attribute to value."
  3151 		//
  3152 		// TODO: We don't actually do this in tree order, not that it matters
  3153 		// unless you're spying with mutation events.
  3154 		getAllEffectivelyContainedNodes(getActiveRange()).forEach(function(node) {
  3155 			getAncestors(node).forEach(function(ancestor) {
  3156 				if (isEditable(ancestor)
  3157 				&& isHtmlElement(ancestor, "a")
  3158 				&& ancestor.hasAttribute("href")) {
  3159 					ancestor.setAttribute("href", value);
  3161 			});
  3162 		});
  3164 		// "Set the selection's value to value."
  3165 		setSelectionValue("createlink", value);
  3167 		// "Return true."
  3168 		return true;
  3170 };
  3172 //@}
  3173 ///// The fontName command /////
  3174 //@{
  3175 commands.fontname = {
  3176 	action: function(value) {
  3177 		// "Set the selection's value to value, then return true."
  3178 		setSelectionValue("fontname", value);
  3179 		return true;
  3180 	}, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
  3181 };
  3183 //@}
  3184 ///// The fontSize command /////
  3185 //@{
  3187 // Helper function for fontSize's action plus queryOutputHelper.  It's just the
  3188 // middle of fontSize's action, ripped out into its own function.  Returns null
  3189 // if the size is invalid.
  3190 function normalizeFontSize(value) {
  3191 	// "Strip leading and trailing whitespace from value."
  3192 	//
  3193 	// Cheap hack, not following the actual algorithm.
  3194 	value = value.trim();
  3196 	// "If value is not a valid floating point number, and would not be a valid
  3197 	// floating point number if a single leading "+" character were stripped,
  3198 	// return false."
  3199 	if (!/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
  3200 		return null;
  3203 	var mode;
  3205 	// "If the first character of value is "+", delete the character and let
  3206 	// mode be "relative-plus"."
  3207 	if (value[0] == "+") {
  3208 		value = value.slice(1);
  3209 		mode = "relative-plus";
  3210 	// "Otherwise, if the first character of value is "-", delete the character
  3211 	// and let mode be "relative-minus"."
  3212 	} else if (value[0] == "-") {
  3213 		value = value.slice(1);
  3214 		mode = "relative-minus";
  3215 	// "Otherwise, let mode be "absolute"."
  3216 	} else {
  3217 		mode = "absolute";
  3220 	// "Apply the rules for parsing non-negative integers to value, and let
  3221 	// number be the result."
  3222 	//
  3223 	// Another cheap hack.
  3224 	var num = parseInt(value);
  3226 	// "If mode is "relative-plus", add three to number."
  3227 	if (mode == "relative-plus") {
  3228 		num += 3;
  3231 	// "If mode is "relative-minus", negate number, then add three to it."
  3232 	if (mode == "relative-minus") {
  3233 		num = 3 - num;
  3236 	// "If number is less than one, let number equal 1."
  3237 	if (num < 1) {
  3238 		num = 1;
  3241 	// "If number is greater than seven, let number equal 7."
  3242 	if (num > 7) {
  3243 		num = 7;
  3246 	// "Set value to the string here corresponding to number:" [table omitted]
  3247 	value = {
  3248 		1: "x-small",
  3249 		2: "small",
  3250 		3: "medium",
  3251 		4: "large",
  3252 		5: "x-large",
  3253 		6: "xx-large",
  3254 		7: "xxx-large"
  3255 	}[num];
  3257 	return value;
  3260 commands.fontsize = {
  3261 	action: function(value) {
  3262 		value = normalizeFontSize(value);
  3263 		if (value === null) {
  3264 			return false;
  3267 		// "Set the selection's value to value."
  3268 		setSelectionValue("fontsize", value);
  3270 		// "Return true."
  3271 		return true;
  3272 	}, indeterm: function() {
  3273 		// "True if among formattable nodes that are effectively contained in
  3274 		// the active range, there are two that have distinct effective command
  3275 		// values.  Otherwise false."
  3276 		return getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
  3277 		.map(function(node) {
  3278 			return getEffectiveCommandValue(node, "fontsize");
  3279 		}).filter(function(value, i, arr) {
  3280 			return arr.slice(0, i).indexOf(value) == -1;
  3281 		}).length >= 2;
  3282 	}, value: function() {
  3283 		// "If the active range is null, return the empty string."
  3284 		if (!getActiveRange()) {
  3285 			return "";
  3288 		// "Let pixel size be the effective command value of the first
  3289 		// formattable node that is effectively contained in the active range,
  3290 		// or if there is no such node, the effective command value of the
  3291 		// active range's start node, in either case interpreted as a number of
  3292 		// pixels."
  3293 		var node = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
  3294 		if (node === undefined) {
  3295 			node = getActiveRange().startContainer;
  3297 		var pixelSize = getEffectiveCommandValue(node, "fontsize");
  3299 		// "Return the legacy font size for pixel size."
  3300 		return getLegacyFontSize(pixelSize);
  3301 	}, relevantCssProperty: "fontSize"
  3302 };
  3304 function getLegacyFontSize(size) {
  3305 	if (getLegacyFontSize.resultCache === undefined) {
  3306 		getLegacyFontSize.resultCache = {};
  3309 	if (getLegacyFontSize.resultCache[size] !== undefined) {
  3310 		return getLegacyFontSize.resultCache[size];
  3313 	// For convenience in other places in my code, I handle all sizes, not just
  3314 	// pixel sizes as the spec says.  This means pixel sizes have to be passed
  3315 	// in suffixed with "px", not as plain numbers.
  3316 	if (normalizeFontSize(size) !== null) {
  3317 		return getLegacyFontSize.resultCache[size] = cssSizeToLegacy(normalizeFontSize(size));
  3320 	if (["x-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(size) == -1
  3321 	&& !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
  3322 		// There is no sensible legacy size for things like "2em".
  3323 		return getLegacyFontSize.resultCache[size] = null;
  3326 	var font = document.createElement("font");
  3327 	document.body.appendChild(font);
  3328 	if (size == "xxx-large") {
  3329 		font.size = 7;
  3330 	} else {
  3331 		font.style.fontSize = size;
  3333 	var pixelSize = parseInt(getComputedStyle(font).fontSize);
  3334 	document.body.removeChild(font);
  3336 	// "Let returned size be 1."
  3337 	var returnedSize = 1;
  3339 	// "While returned size is less than 7:"
  3340 	while (returnedSize < 7) {
  3341 		// "Let lower bound be the resolved value of "font-size" in pixels
  3342 		// of a font element whose size attribute is set to returned size."
  3343 		var font = document.createElement("font");
  3344 		font.size = returnedSize;
  3345 		document.body.appendChild(font);
  3346 		var lowerBound = parseInt(getComputedStyle(font).fontSize);
  3348 		// "Let upper bound be the resolved value of "font-size" in pixels
  3349 		// of a font element whose size attribute is set to one plus
  3350 		// returned size."
  3351 		font.size = 1 + returnedSize;
  3352 		var upperBound = parseInt(getComputedStyle(font).fontSize);
  3353 		document.body.removeChild(font);
  3355 		// "Let average be the average of upper bound and lower bound."
  3356 		var average = (upperBound + lowerBound)/2;
  3358 		// "If pixel size is less than average, return the one-element
  3359 		// string consisting of the digit returned size."
  3360 		if (pixelSize < average) {
  3361 			return getLegacyFontSize.resultCache[size] = String(returnedSize);
  3364 		// "Add one to returned size."
  3365 		returnedSize++;
  3368 	// "Return "7"."
  3369 	return getLegacyFontSize.resultCache[size] = "7";
  3372 //@}
  3373 ///// The foreColor command /////
  3374 //@{
  3375 commands.forecolor = {
  3376 	action: function(value) {
  3377 		// Copy-pasted, same as backColor and hiliteColor
  3379 		// "If value is not a valid CSS color, prepend "#" to it."
  3380 		//
  3381 		// "If value is still not a valid CSS color, or if it is currentColor,
  3382 		// return false."
  3383 		//
  3384 		// Cheap hack for testing, no attempt to be comprehensive.
  3385 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
  3386 			value = "#" + value;
  3388 		if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
  3389 		&& !parseSimpleColor(value)
  3390 		&& value.toLowerCase() != "transparent") {
  3391 			return false;
  3394 		// "Set the selection's value to value."
  3395 		setSelectionValue("forecolor", value);
  3397 		// "Return true."
  3398 		return true;
  3399 	}, standardInlineValueCommand: true, relevantCssProperty: "color",
  3400 	equivalentValues: function(val1, val2) {
  3401 		// "Either both strings are valid CSS colors and have the same red,
  3402 		// green, blue, and alpha components, or neither string is a valid CSS
  3403 		// color."
  3404 		return normalizeColor(val1) === normalizeColor(val2);
  3405 	},
  3406 };
  3408 //@}
  3409 ///// The hiliteColor command /////
  3410 //@{
  3411 commands.hilitecolor = {
  3412 	// Copy-pasted, same as backColor
  3413 	action: function(value) {
  3414 		// Action is further copy-pasted, same as foreColor
  3416 		// "If value is not a valid CSS color, prepend "#" to it."
  3417 		//
  3418 		// "If value is still not a valid CSS color, or if it is currentColor,
  3419 		// return false."
  3420 		//
  3421 		// Cheap hack for testing, no attempt to be comprehensive.
  3422 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
  3423 			value = "#" + value;
  3425 		if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
  3426 		&& !parseSimpleColor(value)
  3427 		&& value.toLowerCase() != "transparent") {
  3428 			return false;
  3431 		// "Set the selection's value to value."
  3432 		setSelectionValue("hilitecolor", value);
  3434 		// "Return true."
  3435 		return true;
  3436 	}, indeterm: function() {
  3437 		// "True if among editable Text nodes that are effectively contained in
  3438 		// the active range, there are two that have distinct effective command
  3439 		// values.  Otherwise false."
  3440 		return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
  3441 			return isEditable(node) && node.nodeType == Node.TEXT_NODE;
  3442 		}).map(function(node) {
  3443 			return getEffectiveCommandValue(node, "hilitecolor");
  3444 		}).filter(function(value, i, arr) {
  3445 			return arr.slice(0, i).indexOf(value) == -1;
  3446 		}).length >= 2;
  3447 	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
  3448 	equivalentValues: function(val1, val2) {
  3449 		// "Either both strings are valid CSS colors and have the same red,
  3450 		// green, blue, and alpha components, or neither string is a valid CSS
  3451 		// color."
  3452 		return normalizeColor(val1) === normalizeColor(val2);
  3453 	},
  3454 };
  3456 //@}
  3457 ///// The italic command /////
  3458 //@{
  3459 commands.italic = {
  3460 	action: function() {
  3461 		// "If queryCommandState("italic") returns true, set the selection's
  3462 		// value to "normal". Otherwise set the selection's value to "italic".
  3463 		// Either way, return true."
  3464 		if (myQueryCommandState("italic")) {
  3465 			setSelectionValue("italic", "normal");
  3466 		} else {
  3467 			setSelectionValue("italic", "italic");
  3469 		return true;
  3470 	}, inlineCommandActivatedValues: ["italic", "oblique"],
  3471 	relevantCssProperty: "fontStyle"
  3472 };
  3474 //@}
  3475 ///// The removeFormat command /////
  3476 //@{
  3477 commands.removeformat = {
  3478 	action: function() {
  3479 		// "A removeFormat candidate is an editable HTML element with local
  3480 		// name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
  3481 		// "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
  3482 		// "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
  3483 		// "tt", "u", or "var"."
  3484 		function isRemoveFormatCandidate(node) {
  3485 			return isEditable(node)
  3486 				&& isHtmlElement(node, ["abbr", "acronym", "b", "bdi", "bdo",
  3487 				"big", "blink", "cite", "code", "dfn", "em", "font", "i",
  3488 				"ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
  3489 				"span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
  3492 		// "Let elements to remove be a list of every removeFormat candidate
  3493 		// effectively contained in the active range."
  3494 		var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
  3496 		// "For each element in elements to remove:"
  3497 		elementsToRemove.forEach(function(element) {
  3498 			// "While element has children, insert the first child of element
  3499 			// into the parent of element immediately before element,
  3500 			// preserving ranges."
  3501 			while (element.hasChildNodes()) {
  3502 				movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
  3505 			// "Remove element from its parent."
  3506 			element.parentNode.removeChild(element);
  3507 		});
  3509 		// "If the active range's start node is an editable Text node, and its
  3510 		// start offset is neither zero nor its start node's length, call
  3511 		// splitText() on the active range's start node, with argument equal to
  3512 		// the active range's start offset. Then set the active range's start
  3513 		// node to the result, and its start offset to zero."
  3514 		if (isEditable(getActiveRange().startContainer)
  3515 		&& getActiveRange().startContainer.nodeType == Node.TEXT_NODE
  3516 		&& getActiveRange().startOffset != 0
  3517 		&& getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
  3518 			// Account for browsers not following range mutation rules
  3519 			if (getActiveRange().startContainer == getActiveRange().endContainer) {
  3520 				var newEnd = getActiveRange().endOffset - getActiveRange().startOffset;
  3521 				var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
  3522 				getActiveRange().setStart(newNode, 0);
  3523 				getActiveRange().setEnd(newNode, newEnd);
  3524 			} else {
  3525 				getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0);
  3529 		// "If the active range's end node is an editable Text node, and its
  3530 		// end offset is neither zero nor its end node's length, call
  3531 		// splitText() on the active range's end node, with argument equal to
  3532 		// the active range's end offset."
  3533 		if (isEditable(getActiveRange().endContainer)
  3534 		&& getActiveRange().endContainer.nodeType == Node.TEXT_NODE
  3535 		&& getActiveRange().endOffset != 0
  3536 		&& getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
  3537 			// IE seems to mutate the range incorrectly here, so we need
  3538 			// correction here as well.  Have to be careful to set the range to
  3539 			// something not including the text node so that getActiveRange()
  3540 			// doesn't throw an exception due to a temporarily detached
  3541 			// endpoint.
  3542 			var newStart = [getActiveRange().startContainer, getActiveRange().startOffset];
  3543 			var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset];
  3544 			getActiveRange().setEnd(document.documentElement, 0);
  3545 			newEnd[0].splitText(newEnd[1]);
  3546 			getActiveRange().setStart(newStart[0], newStart[1]);
  3547 			getActiveRange().setEnd(newEnd[0], newEnd[1]);
  3550 		// "Let node list consist of all editable nodes effectively contained
  3551 		// in the active range."
  3552 		//
  3553 		// "For each node in node list, while node's parent is a removeFormat
  3554 		// candidate in the same editing host as node, split the parent of the
  3555 		// one-node list consisting of node."
  3556 		getAllEffectivelyContainedNodes(getActiveRange(), isEditable).forEach(function(node) {
  3557 			while (isRemoveFormatCandidate(node.parentNode)
  3558 			&& inSameEditingHost(node.parentNode, node)) {
  3559 				splitParent([node]);
  3561 		});
  3563 		// "For each of the entries in the following list, in the given order,
  3564 		// set the selection's value to null, with command as given."
  3566 			"subscript",
  3567 			"bold",
  3568 			"fontname",
  3569 			"fontsize",
  3570 			"forecolor",
  3571 			"hilitecolor",
  3572 			"italic",
  3573 			"strikethrough",
  3574 			"underline",
  3575 		].forEach(function(command) {
  3576 			setSelectionValue(command, null);
  3577 		});
  3579 		// "Return true."
  3580 		return true;
  3582 };
  3584 //@}
  3585 ///// The strikethrough command /////
  3586 //@{
  3587 commands.strikethrough = {
  3588 	action: function() {
  3589 		// "If queryCommandState("strikethrough") returns true, set the
  3590 		// selection's value to null. Otherwise set the selection's value to
  3591 		// "line-through".  Either way, return true."
  3592 		if (myQueryCommandState("strikethrough")) {
  3593 			setSelectionValue("strikethrough", null);
  3594 		} else {
  3595 			setSelectionValue("strikethrough", "line-through");
  3597 		return true;
  3598 	}, inlineCommandActivatedValues: ["line-through"]
  3599 };
  3601 //@}
  3602 ///// The subscript command /////
  3603 //@{
  3604 commands.subscript = {
  3605 	action: function() {
  3606 		// "Call queryCommandState("subscript"), and let state be the result."
  3607 		var state = myQueryCommandState("subscript");
  3609 		// "Set the selection's value to null."
  3610 		setSelectionValue("subscript", null);
  3612 		// "If state is false, set the selection's value to "subscript"."
  3613 		if (!state) {
  3614 			setSelectionValue("subscript", "subscript");
  3617 		// "Return true."
  3618 		return true;
  3619 	}, indeterm: function() {
  3620 		// "True if either among formattable nodes that are effectively
  3621 		// contained in the active range, there is at least one with effective
  3622 		// command value "subscript" and at least one with some other effective
  3623 		// command value; or if there is some formattable node effectively
  3624 		// contained in the active range with effective command value "mixed".
  3625 		// Otherwise false."
  3626 		var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
  3627 		return (nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" })
  3628 			&& nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
  3629 			|| nodes.some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
  3630 	}, inlineCommandActivatedValues: ["subscript"],
  3631 };
  3633 //@}
  3634 ///// The superscript command /////
  3635 //@{
  3636 commands.superscript = {
  3637 	action: function() {
  3638 		// "Call queryCommandState("superscript"), and let state be the
  3639 		// result."
  3640 		var state = myQueryCommandState("superscript");
  3642 		// "Set the selection's value to null."
  3643 		setSelectionValue("superscript", null);
  3645 		// "If state is false, set the selection's value to "superscript"."
  3646 		if (!state) {
  3647 			setSelectionValue("superscript", "superscript");
  3650 		// "Return true."
  3651 		return true;
  3652 	}, indeterm: function() {
  3653 		// "True if either among formattable nodes that are effectively
  3654 		// contained in the active range, there is at least one with effective
  3655 		// command value "superscript" and at least one with some other
  3656 		// effective command value; or if there is some formattable node
  3657 		// effectively contained in the active range with effective command
  3658 		// value "mixed".  Otherwise false."
  3659 		var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
  3660 		return (nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" })
  3661 			&& nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
  3662 			|| nodes.some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
  3663 	}, inlineCommandActivatedValues: ["superscript"],
  3664 };
  3666 //@}
  3667 ///// The underline command /////
  3668 //@{
  3669 commands.underline = {
  3670 	action: function() {
  3671 		// "If queryCommandState("underline") returns true, set the selection's
  3672 		// value to null. Otherwise set the selection's value to "underline".
  3673 		// Either way, return true."
  3674 		if (myQueryCommandState("underline")) {
  3675 			setSelectionValue("underline", null);
  3676 		} else {
  3677 			setSelectionValue("underline", "underline");
  3679 		return true;
  3680 	}, inlineCommandActivatedValues: ["underline"]
  3681 };
  3683 //@}
  3684 ///// The unlink command /////
  3685 //@{
  3686 commands.unlink = {
  3687 	action: function() {
  3688 		// "Let hyperlinks be a list of every a element that has an href
  3689 		// attribute and is contained in the active range or is an ancestor of
  3690 		// one of its boundary points."
  3691 		//
  3692 		// As usual, take care to ensure it's tree order.  The correctness of
  3693 		// the following is left as an exercise for the reader.
  3694 		var range = getActiveRange();
  3695 		var hyperlinks = [];
  3696 		for (
  3697 			var node = range.startContainer;
  3698 			node;
  3699 			node = node.parentNode
  3700 		) {
  3701 			if (isHtmlElement(node, "A")
  3702 			&& node.hasAttribute("href")) {
  3703 				hyperlinks.unshift(node);
  3706 		for (
  3707 			var node = range.startContainer;
  3708 			node != nextNodeDescendants(range.endContainer);
  3709 			node = nextNode(node)
  3710 		) {
  3711 			if (isHtmlElement(node, "A")
  3712 			&& node.hasAttribute("href")
  3713 			&& (isContained(node, range)
  3714 			|| isAncestor(node, range.endContainer)
  3715 			|| node == range.endContainer)) {
  3716 				hyperlinks.push(node);
  3720 		// "Clear the value of each member of hyperlinks."
  3721 		for (var i = 0; i < hyperlinks.length; i++) {
  3722 			clearValue(hyperlinks[i], "unlink");
  3725 		// "Return true."
  3726 		return true;
  3728 };
  3730 //@}
  3732 /////////////////////////////////////
  3733 ///// Block formatting commands /////
  3734 /////////////////////////////////////
  3736 ///// Block formatting command definitions /////
  3737 //@{
  3739 // "An indentation element is either a blockquote, or a div that has a style
  3740 // attribute that sets "margin" or some subproperty of it."
  3741 function isIndentationElement(node) {
  3742 	if (!isHtmlElement(node)) {
  3743 		return false;
  3746 	if (node.tagName == "BLOCKQUOTE") {
  3747 		return true;
  3750 	if (node.tagName != "DIV") {
  3751 		return false;
  3754 	for (var i = 0; i < node.style.length; i++) {
  3755 		// Approximate check
  3756 		if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
  3757 			return true;
  3761 	return false;
  3764 // "A simple indentation element is an indentation element that has no
  3765 // attributes except possibly
  3766 //
  3767 //   * "a style attribute that sets no properties other than "margin",
  3768 //     "border", "padding", or subproperties of those; and/or
  3769 //   * "a dir attribute."
  3770 function isSimpleIndentationElement(node) {
  3771 	if (!isIndentationElement(node)) {
  3772 		return false;
  3775 	for (var i = 0; i < node.attributes.length; i++) {
  3776 		if (!isHtmlNamespace(node.attributes[i].namespaceURI)
  3777 		|| ["style", "dir"].indexOf(node.attributes[i].name) == -1) {
  3778 			return false;
  3782 	for (var i = 0; i < node.style.length; i++) {
  3783 		// This is approximate, but it works well enough for my purposes.
  3784 		if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
  3785 			return false;
  3789 	return true;
  3792 // "A non-list single-line container is an HTML element with local name
  3793 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
  3794 // or "xmp"."
  3795 function isNonListSingleLineContainer(node) {
  3796 	return isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
  3797 		"h6", "listing", "p", "pre", "xmp"]);
  3800 // "A single-line container is either a non-list single-line container, or an
  3801 // HTML element with local name "li", "dt", or "dd"."
  3802 function isSingleLineContainer(node) {
  3803 	return isNonListSingleLineContainer(node)
  3804 		|| isHtmlElement(node, ["li", "dt", "dd"]);
  3807 function getBlockNodeOf(node) {
  3808 	// "While node is an inline node, set node to its parent."
  3809 	while (isInlineNode(node)) {
  3810 		node = node.parentNode;
  3813 	// "Return node."
  3814 	return node;
  3817 //@}
  3818 ///// Assorted block formatting command algorithms /////
  3819 //@{
  3821 function fixDisallowedAncestors(node) {
  3822 	// "If node is not editable, abort these steps."
  3823 	if (!isEditable(node)) {
  3824 		return;
  3827 	// "If node is not an allowed child of any of its ancestors in the same
  3828 	// editing host:"
  3829 	if (getAncestors(node).every(function(ancestor) {
  3830 		return !inSameEditingHost(node, ancestor)
  3831 			|| !isAllowedChild(node, ancestor)
  3832 	})) {
  3833 		// "If node is a dd or dt, wrap the one-node list consisting of node,
  3834 		// with sibling criteria returning true for any dl with no attributes
  3835 		// and false otherwise, and new parent instructions returning the
  3836 		// result of calling createElement("dl") on the context object. Then
  3837 		// abort these steps."
  3838 		if (isHtmlElement(node, ["dd", "dt"])) {
  3839 			wrap([node],
  3840 				function(sibling) { return isHtmlElement(sibling, "dl") && !sibling.attributes.length },
  3841 				function() { return document.createElement("dl") });
  3842 			return;
  3845 		// "If "p" is not an allowed child of the editing host of node, abort
  3846 		// these steps."
  3847 		if (!isAllowedChild("p", getEditingHostOf(node))) {
  3848 			return;
  3851 		// "If node is not a prohibited paragraph child, abort these steps."
  3852 		if (!isProhibitedParagraphChild(node)) {
  3853 			return;
  3856 		// "Set the tag name of node to the default single-line container name,
  3857 		// and let node be the result."
  3858 		node = setTagName(node, defaultSingleLineContainerName);
  3860 		// "Fix disallowed ancestors of node."
  3861 		fixDisallowedAncestors(node);
  3863 		// "Let children be node's children."
  3864 		var children = [].slice.call(node.childNodes);
  3866 		// "For each child in children, if child is a prohibited paragraph
  3867 		// child:"
  3868 		children.filter(isProhibitedParagraphChild)
  3869 		.forEach(function(child) {
  3870 			// "Record the values of the one-node list consisting of child, and
  3871 			// let values be the result."
  3872 			var values = recordValues([child]);
  3874 			// "Split the parent of the one-node list consisting of child."
  3875 			splitParent([child]);
  3877 			// "Restore the values from values."
  3878 			restoreValues(values);
  3879 		});
  3881 		// "Abort these steps."
  3882 		return;
  3885 	// "Record the values of the one-node list consisting of node, and let
  3886 	// values be the result."
  3887 	var values = recordValues([node]);
  3889 	// "While node is not an allowed child of its parent, split the parent of
  3890 	// the one-node list consisting of node."
  3891 	while (!isAllowedChild(node, node.parentNode)) {
  3892 		splitParent([node]);
  3895 	// "Restore the values from values."
  3896 	restoreValues(values);
  3899 function normalizeSublists(item) {
  3900 	// "If item is not an li or it is not editable or its parent is not
  3901 	// editable, abort these steps."
  3902 	if (!isHtmlElement(item, "LI")
  3903 	|| !isEditable(item)
  3904 	|| !isEditable(item.parentNode)) {
  3905 		return;
  3908 	// "Let new item be null."
  3909 	var newItem = null;
  3911 	// "While item has an ol or ul child:"
  3912 	while ([].some.call(item.childNodes, function (node) { return isHtmlElement(node, ["OL", "UL"]) })) {
  3913 		// "Let child be the last child of item."
  3914 		var child = item.lastChild;
  3916 		// "If child is an ol or ul, or new item is null and child is a Text
  3917 		// node whose data consists of zero of more space characters:"
  3918 		if (isHtmlElement(child, ["OL", "UL"])
  3919 		|| (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) {
  3920 			// "Set new item to null."
  3921 			newItem = null;
  3923 			// "Insert child into the parent of item immediately following
  3924 			// item, preserving ranges."
  3925 			movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item));
  3927 		// "Otherwise:"
  3928 		} else {
  3929 			// "If new item is null, let new item be the result of calling
  3930 			// createElement("li") on the ownerDocument of item, then insert
  3931 			// new item into the parent of item immediately after item."
  3932 			if (!newItem) {
  3933 				newItem = item.ownerDocument.createElement("li");
  3934 				item.parentNode.insertBefore(newItem, item.nextSibling);
  3937 			// "Insert child into new item as its first child, preserving
  3938 			// ranges."
  3939 			movePreservingRanges(child, newItem, 0);
  3944 function getSelectionListState() {
  3945 	// "If the active range is null, return "none"."
  3946 	if (!getActiveRange()) {
  3947 		return "none";
  3950 	// "Block-extend the active range, and let new range be the result."
  3951 	var newRange = blockExtend(getActiveRange());
  3953 	// "Let node list be a list of nodes, initially empty."
  3954 	//
  3955 	// "For each node contained in new range, append node to node list if the
  3956 	// last member of node list (if any) is not an ancestor of node; node is
  3957 	// editable; node is not an indentation element; and node is either an ol
  3958 	// or ul, or the child of an ol or ul, or an allowed child of "li"."
  3959 	var nodeList = getContainedNodes(newRange, function(node) {
  3960 		return isEditable(node)
  3961 			&& !isIndentationElement(node)
  3962 			&& (isHtmlElement(node, ["ol", "ul"])
  3963 			|| isHtmlElement(node.parentNode, ["ol", "ul"])
  3964 			|| isAllowedChild(node, "li"));
  3965 	});
  3967 	// "If node list is empty, return "none"."
  3968 	if (!nodeList.length) {
  3969 		return "none";
  3972 	// "If every member of node list is either an ol or the child of an ol or
  3973 	// the child of an li child of an ol, and none is a ul or an ancestor of a
  3974 	// ul, return "ol"."
  3975 	if (nodeList.every(function(node) {
  3976 		return isHtmlElement(node, "ol")
  3977 			|| isHtmlElement(node.parentNode, "ol")
  3978 			|| (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
  3979 	})
  3980 	&& !nodeList.some(function(node) { return isHtmlElement(node, "ul") || ("querySelector" in node && node.querySelector("ul")) })) {
  3981 		return "ol";
  3984 	// "If every member of node list is either a ul or the child of a ul or the
  3985 	// child of an li child of a ul, and none is an ol or an ancestor of an ol,
  3986 	// return "ul"."
  3987 	if (nodeList.every(function(node) {
  3988 		return isHtmlElement(node, "ul")
  3989 			|| isHtmlElement(node.parentNode, "ul")
  3990 			|| (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
  3991 	})
  3992 	&& !nodeList.some(function(node) { return isHtmlElement(node, "ol") || ("querySelector" in node && node.querySelector("ol")) })) {
  3993 		return "ul";
  3996 	var hasOl = nodeList.some(function(node) {
  3997 		return isHtmlElement(node, "ol")
  3998 			|| isHtmlElement(node.parentNode, "ol")
  3999 			|| ("querySelector" in node && node.querySelector("ol"))
  4000 			|| (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ol"));
  4001 	});
  4002 	var hasUl = nodeList.some(function(node) {
  4003 		return isHtmlElement(node, "ul")
  4004 			|| isHtmlElement(node.parentNode, "ul")
  4005 			|| ("querySelector" in node && node.querySelector("ul"))
  4006 			|| (isHtmlElement(node.parentNode, "li") && isHtmlElement(node.parentNode.parentNode, "ul"));
  4007 	});
  4008 	// "If some member of node list is either an ol or the child or ancestor of
  4009 	// an ol or the child of an li child of an ol, and some member of node list
  4010 	// is either a ul or the child or ancestor of a ul or the child of an li
  4011 	// child of a ul, return "mixed"."
  4012 	if (hasOl && hasUl) {
  4013 		return "mixed";
  4016 	// "If some member of node list is either an ol or the child or ancestor of
  4017 	// an ol or the child of an li child of an ol, return "mixed ol"."
  4018 	if (hasOl) {
  4019 		return "mixed ol";
  4022 	// "If some member of node list is either a ul or the child or ancestor of
  4023 	// a ul or the child of an li child of a ul, return "mixed ul"."
  4024 	if (hasUl) {
  4025 		return "mixed ul";
  4028 	// "Return "none"."
  4029 	return "none";
  4032 function getAlignmentValue(node) {
  4033 	// "While node is neither null nor an Element, or it is an Element but its
  4034 	// "display" property has resolved value "inline" or "none", set node to
  4035 	// its parent."
  4036 	while ((node && node.nodeType != Node.ELEMENT_NODE)
  4037 	|| (node.nodeType == Node.ELEMENT_NODE
  4038 	&& ["inline", "none"].indexOf(getComputedStyle(node).display) != -1)) {
  4039 		node = node.parentNode;
  4042 	// "If node is not an Element, return "left"."
  4043 	if (!node || node.nodeType != Node.ELEMENT_NODE) {
  4044 		return "left";
  4047 	var resolvedValue = getComputedStyle(node).textAlign
  4048 		// Hack around browser non-standardness
  4049 		.replace(/^-(moz|webkit)-/, "")
  4050 		.replace(/^auto$/, "start");
  4052 	// "If node's "text-align" property has resolved value "start", return
  4053 	// "left" if the directionality of node is "ltr", "right" if it is "rtl"."
  4054 	if (resolvedValue == "start") {
  4055 		return getDirectionality(node) == "ltr" ? "left" : "right";
  4058 	// "If node's "text-align" property has resolved value "end", return
  4059 	// "right" if the directionality of node is "ltr", "left" if it is "rtl"."
  4060 	if (resolvedValue == "end") {
  4061 		return getDirectionality(node) == "ltr" ? "right" : "left";
  4064 	// "If node's "text-align" property has resolved value "center", "justify",
  4065 	// "left", or "right", return that value."
  4066 	if (["center", "justify", "left", "right"].indexOf(resolvedValue) != -1) {
  4067 		return resolvedValue;
  4070 	// "Return "left"."
  4071 	return "left";
  4074 function getNextEquivalentPoint(node, offset) {
  4075 	// "If node's length is zero, return null."
  4076 	if (getNodeLength(node) == 0) {
  4077 		return null;
  4080 	// "If offset is node's length, and node's parent is not null, and node is
  4081 	// an inline node, return (node's parent, 1 + node's index)."
  4082 	if (offset == getNodeLength(node)
  4083 	&& node.parentNode
  4084 	&& isInlineNode(node)) {
  4085 		return [node.parentNode, 1 + getNodeIndex(node)];
  4088 	// "If node has a child with index offset, and that child's length is not
  4089 	// zero, and that child is an inline node, return (that child, 0)."
  4090 	if (0 <= offset
  4091 	&& offset < node.childNodes.length
  4092 	&& getNodeLength(node.childNodes[offset]) != 0
  4093 	&& isInlineNode(node.childNodes[offset])) {
  4094 		return [node.childNodes[offset], 0];
  4097 	// "Return null."
  4098 	return null;
  4101 function getPreviousEquivalentPoint(node, offset) {
  4102 	// "If node's length is zero, return null."
  4103 	if (getNodeLength(node) == 0) {
  4104 		return null;
  4107 	// "If offset is 0, and node's parent is not null, and node is an inline
  4108 	// node, return (node's parent, node's index)."
  4109 	if (offset == 0
  4110 	&& node.parentNode
  4111 	&& isInlineNode(node)) {
  4112 		return [node.parentNode, getNodeIndex(node)];
  4115 	// "If node has a child with index offset − 1, and that child's length is
  4116 	// not zero, and that child is an inline node, return (that child, that
  4117 	// child's length)."
  4118 	if (0 <= offset - 1
  4119 	&& offset - 1 < node.childNodes.length
  4120 	&& getNodeLength(node.childNodes[offset - 1]) != 0
  4121 	&& isInlineNode(node.childNodes[offset - 1])) {
  4122 		return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])];
  4125 	// "Return null."
  4126 	return null;
  4129 function getFirstEquivalentPoint(node, offset) {
  4130 	// "While (node, offset)'s previous equivalent point is not null, set
  4131 	// (node, offset) to its previous equivalent point."
  4132 	var prev;
  4133 	while (prev = getPreviousEquivalentPoint(node, offset)) {
  4134 		node = prev[0];
  4135 		offset = prev[1];
  4138 	// "Return (node, offset)."
  4139 	return [node, offset];
  4142 function getLastEquivalentPoint(node, offset) {
  4143 	// "While (node, offset)'s next equivalent point is not null, set (node,
  4144 	// offset) to its next equivalent point."
  4145 	var next;
  4146 	while (next = getNextEquivalentPoint(node, offset)) {
  4147 		node = next[0];
  4148 		offset = next[1];
  4151 	// "Return (node, offset)."
  4152 	return [node, offset];
  4155 //@}
  4156 ///// Block-extending a range /////
  4157 //@{
  4159 // "A boundary point (node, offset) is a block start point if either node's
  4160 // parent is null and offset is zero; or node has a child with index offset −
  4161 // 1, and that child is either a visible block node or a visible br."
  4162 function isBlockStartPoint(node, offset) {
  4163 	return (!node.parentNode && offset == 0)
  4164 		|| (0 <= offset - 1
  4165 		&& offset - 1 < node.childNodes.length
  4166 		&& isVisible(node.childNodes[offset - 1])
  4167 		&& (isBlockNode(node.childNodes[offset - 1])
  4168 		|| isHtmlElement(node.childNodes[offset - 1], "br")));
  4171 // "A boundary point (node, offset) is a block end point if either node's
  4172 // parent is null and offset is node's length; or node has a child with index
  4173 // offset, and that child is a visible block node."
  4174 function isBlockEndPoint(node, offset) {
  4175 	return (!node.parentNode && offset == getNodeLength(node))
  4176 		|| (offset < node.childNodes.length
  4177 		&& isVisible(node.childNodes[offset])
  4178 		&& isBlockNode(node.childNodes[offset]));
  4181 // "A boundary point is a block boundary point if it is either a block start
  4182 // point or a block end point."
  4183 function isBlockBoundaryPoint(node, offset) {
  4184 	return isBlockStartPoint(node, offset)
  4185 		|| isBlockEndPoint(node, offset);
  4188 function blockExtend(range) {
  4189 	// "Let start node, start offset, end node, and end offset be the start
  4190 	// and end nodes and offsets of the range."
  4191 	var startNode = range.startContainer;
  4192 	var startOffset = range.startOffset;
  4193 	var endNode = range.endContainer;
  4194 	var endOffset = range.endOffset;
  4196 	// "If some ancestor container of start node is an li, set start offset to
  4197 	// the index of the last such li in tree order, and set start node to that
  4198 	// li's parent."
  4199 	var liAncestors = getAncestors(startNode).concat(startNode)
  4200 		.filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
  4201 		.slice(-1);
  4202 	if (liAncestors.length) {
  4203 		startOffset = getNodeIndex(liAncestors[0]);
  4204 		startNode = liAncestors[0].parentNode;
  4207 	// "If (start node, start offset) is not a block start point, repeat the
  4208 	// following steps:"
  4209 	if (!isBlockStartPoint(startNode, startOffset)) do {
  4210 		// "If start offset is zero, set it to start node's index, then set
  4211 		// start node to its parent."
  4212 		if (startOffset == 0) {
  4213 			startOffset = getNodeIndex(startNode);
  4214 			startNode = startNode.parentNode;
  4216 		// "Otherwise, subtract one from start offset."
  4217 		} else {
  4218 			startOffset--;
  4221 		// "If (start node, start offset) is a block boundary point, break from
  4222 		// this loop."
  4223 	} while (!isBlockBoundaryPoint(startNode, startOffset));
  4225 	// "While start offset is zero and start node's parent is not null, set
  4226 	// start offset to start node's index, then set start node to its parent."
  4227 	while (startOffset == 0
  4228 	&& startNode.parentNode) {
  4229 		startOffset = getNodeIndex(startNode);
  4230 		startNode = startNode.parentNode;
  4233 	// "If some ancestor container of end node is an li, set end offset to one
  4234 	// plus the index of the last such li in tree order, and set end node to
  4235 	// that li's parent."
  4236 	var liAncestors = getAncestors(endNode).concat(endNode)
  4237 		.filter(function(ancestor) { return isHtmlElement(ancestor, "li") })
  4238 		.slice(-1);
  4239 	if (liAncestors.length) {
  4240 		endOffset = 1 + getNodeIndex(liAncestors[0]);
  4241 		endNode = liAncestors[0].parentNode;
  4244 	// "If (end node, end offset) is not a block end point, repeat the
  4245 	// following steps:"
  4246 	if (!isBlockEndPoint(endNode, endOffset)) do {
  4247 		// "If end offset is end node's length, set it to one plus end node's
  4248 		// index, then set end node to its parent."
  4249 		if (endOffset == getNodeLength(endNode)) {
  4250 			endOffset = 1 + getNodeIndex(endNode);
  4251 			endNode = endNode.parentNode;
  4253 		// "Otherwise, add one to end offset.
  4254 		} else {
  4255 			endOffset++;
  4258 		// "If (end node, end offset) is a block boundary point, break from
  4259 		// this loop."
  4260 	} while (!isBlockBoundaryPoint(endNode, endOffset));
  4262 	// "While end offset is end node's length and end node's parent is not
  4263 	// null, set end offset to one plus end node's index, then set end node to
  4264 	// its parent."
  4265 	while (endOffset == getNodeLength(endNode)
  4266 	&& endNode.parentNode) {
  4267 		endOffset = 1 + getNodeIndex(endNode);
  4268 		endNode = endNode.parentNode;
  4271 	// "Let new range be a new range whose start and end nodes and offsets
  4272 	// are start node, start offset, end node, and end offset."
  4273 	var newRange = startNode.ownerDocument.createRange();
  4274 	newRange.setStart(startNode, startOffset);
  4275 	newRange.setEnd(endNode, endOffset);
  4277 	// "Return new range."
  4278 	return newRange;
  4281 function followsLineBreak(node) {
  4282 	// "Let offset be zero."
  4283 	var offset = 0;
  4285 	// "While (node, offset) is not a block boundary point:"
  4286 	while (!isBlockBoundaryPoint(node, offset)) {
  4287 		// "If node has a visible child with index offset minus one, return
  4288 		// false."
  4289 		if (0 <= offset - 1
  4290 		&& offset - 1 < node.childNodes.length
  4291 		&& isVisible(node.childNodes[offset - 1])) {
  4292 			return false;
  4295 		// "If offset is zero or node has no children, set offset to node's
  4296 		// index, then set node to its parent."
  4297 		if (offset == 0
  4298 		|| !node.hasChildNodes()) {
  4299 			offset = getNodeIndex(node);
  4300 			node = node.parentNode;
  4302 		// "Otherwise, set node to its child with index offset minus one, then
  4303 		// set offset to node's length."
  4304 		} else {
  4305 			node = node.childNodes[offset - 1];
  4306 			offset = getNodeLength(node);
  4310 	// "Return true."
  4311 	return true;
  4314 function precedesLineBreak(node) {
  4315 	// "Let offset be node's length."
  4316 	var offset = getNodeLength(node);
  4318 	// "While (node, offset) is not a block boundary point:"
  4319 	while (!isBlockBoundaryPoint(node, offset)) {
  4320 		// "If node has a visible child with index offset, return false."
  4321 		if (offset < node.childNodes.length
  4322 		&& isVisible(node.childNodes[offset])) {
  4323 			return false;
  4326 		// "If offset is node's length or node has no children, set offset to
  4327 		// one plus node's index, then set node to its parent."
  4328 		if (offset == getNodeLength(node)
  4329 		|| !node.hasChildNodes()) {
  4330 			offset = 1 + getNodeIndex(node);
  4331 			node = node.parentNode;
  4333 		// "Otherwise, set node to its child with index offset and set offset
  4334 		// to zero."
  4335 		} else {
  4336 			node = node.childNodes[offset];
  4337 			offset = 0;
  4341 	// "Return true."
  4342 	return true;
  4345 //@}
  4346 ///// Recording and restoring overrides /////
  4347 //@{
  4349 function recordCurrentOverrides() {
  4350 	// "Let overrides be a list of (string, string or boolean) ordered pairs,
  4351 	// initially empty."
  4352 	var overrides = [];
  4354 	// "If there is a value override for "createLink", add ("createLink", value
  4355 	// override for "createLink") to overrides."
  4356 	if (getValueOverride("createlink") !== undefined) {
  4357 		overrides.push(["createlink", getValueOverride("createlink")]);
  4360 	// "For each command in the list "bold", "italic", "strikethrough",
  4361 	// "subscript", "superscript", "underline", in order: if there is a state
  4362 	// override for command, add (command, command's state override) to
  4363 	// overrides."
  4364 	["bold", "italic", "strikethrough", "subscript", "superscript",
  4365 	"underline"].forEach(function(command) {
  4366 		if (getStateOverride(command) !== undefined) {
  4367 			overrides.push([command, getStateOverride(command)]);
  4369 	});
  4371 	// "For each command in the list "fontName", "fontSize", "foreColor",
  4372 	// "hiliteColor", in order: if there is a value override for command, add
  4373 	// (command, command's value override) to overrides."
  4374 	["fontname", "fontsize", "forecolor",
  4375 	"hilitecolor"].forEach(function(command) {
  4376 		if (getValueOverride(command) !== undefined) {
  4377 			overrides.push([command, getValueOverride(command)]);
  4379 	});
  4381 	// "Return overrides."
  4382 	return overrides;
  4385 function recordCurrentStatesAndValues() {
  4386 	// "Let overrides be a list of (string, string or boolean) ordered pairs,
  4387 	// initially empty."
  4388 	var overrides = [];
  4390 	// "Let node be the first formattable node effectively contained in the
  4391 	// active range, or null if there is none."
  4392 	var node = getAllEffectivelyContainedNodes(getActiveRange())
  4393 		.filter(isFormattableNode)[0];
  4395 	// "If node is null, return overrides."
  4396 	if (!node) {
  4397 		return overrides;
  4400 	// "Add ("createLink", node's effective command value for "createLink") to
  4401 	// overrides."
  4402 	overrides.push(["createlink", getEffectiveCommandValue(node, "createlink")]);
  4404 	// "For each command in the list "bold", "italic", "strikethrough",
  4405 	// "subscript", "superscript", "underline", in order: if node's effective
  4406 	// command value for command is one of its inline command activated values,
  4407 	// add (command, true) to overrides, and otherwise add (command, false) to
  4408 	// overrides."
  4409 	["bold", "italic", "strikethrough", "subscript", "superscript",
  4410 	"underline"].forEach(function(command) {
  4411 		if (commands[command].inlineCommandActivatedValues
  4412 		.indexOf(getEffectiveCommandValue(node, command)) != -1) {
  4413 			overrides.push([command, true]);
  4414 		} else {
  4415 			overrides.push([command, false]);
  4417 	});
  4419 	// "For each command in the list "fontName", "foreColor", "hiliteColor", in
  4420 	// order: add (command, command's value) to overrides."
  4421 	["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
  4422 		overrides.push([command, commands[command].value()]);
  4423 	});
  4425 	// "Add ("fontSize", node's effective command value for "fontSize") to
  4426 	// overrides."
  4427 	overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
  4429 	// "Return overrides."
  4430 	return overrides;
  4433 function restoreStatesAndValues(overrides) {
  4434 	// "Let node be the first formattable node effectively contained in the
  4435 	// active range, or null if there is none."
  4436 	var node = getAllEffectivelyContainedNodes(getActiveRange())
  4437 		.filter(isFormattableNode)[0];
  4439 	// "If node is not null, then for each (command, override) pair in
  4440 	// overrides, in order:"
  4441 	if (node) {
  4442 		for (var i = 0; i < overrides.length; i++) {
  4443 			var command = overrides[i][0];
  4444 			var override = overrides[i][1];
  4446 			// "If override is a boolean, and queryCommandState(command)
  4447 			// returns something different from override, take the action for
  4448 			// command, with value equal to the empty string."
  4449 			if (typeof override == "boolean"
  4450 			&& myQueryCommandState(command) != override) {
  4451 				commands[command].action("");
  4453 			// "Otherwise, if override is a string, and command is neither
  4454 			// "createLink" nor "fontSize", and queryCommandValue(command)
  4455 			// returns something not equivalent to override, take the action
  4456 			// for command, with value equal to override."
  4457 			} else if (typeof override == "string"
  4458 			&& command != "createlink"
  4459 			&& command != "fontsize"
  4460 			&& !areEquivalentValues(command, myQueryCommandValue(command), override)) {
  4461 				commands[command].action(override);
  4463 			// "Otherwise, if override is a string; and command is
  4464 			// "createLink"; and either there is a value override for
  4465 			// "createLink" that is not equal to override, or there is no value
  4466 			// override for "createLink" and node's effective command value for
  4467 			// "createLink" is not equal to override: take the action for
  4468 			// "createLink", with value equal to override."
  4469 			} else if (typeof override == "string"
  4470 			&& command == "createlink"
  4471 			&& (
  4473 					getValueOverride("createlink") !== undefined
  4474 					&& getValueOverride("createlink") !== override
  4475 				) || (
  4476 					getValueOverride("createlink") === undefined
  4477 					&& getEffectiveCommandValue(node, "createlink") !== override
  4479 			)) {
  4480 				commands.createlink.action(override);
  4482 			// "Otherwise, if override is a string; and command is "fontSize";
  4483 			// and either there is a value override for "fontSize" that is not
  4484 			// equal to override, or there is no value override for "fontSize"
  4485 			// and node's effective command value for "fontSize" is not loosely
  4486 			// equivalent to override:"
  4487 			} else if (typeof override == "string"
  4488 			&& command == "fontsize"
  4489 			&& (
  4491 					getValueOverride("fontsize") !== undefined
  4492 					&& getValueOverride("fontsize") !== override
  4493 				) || (
  4494 					getValueOverride("fontsize") === undefined
  4495 					&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
  4497 			)) {
  4498 				// "Convert override to an integer number of pixels, and set
  4499 				// override to the legacy font size for the result."
  4500 				override = getLegacyFontSize(override);
  4502 				// "Take the action for "fontSize", with value equal to
  4503 				// override."
  4504 				commands.fontsize.action(override);
  4506 			// "Otherwise, continue this loop from the beginning."
  4507 			} else {
  4508 				continue;
  4511 			// "Set node to the first formattable node effectively contained in
  4512 			// the active range, if there is one."
  4513 			node = getAllEffectivelyContainedNodes(getActiveRange())
  4514 				.filter(isFormattableNode)[0]
  4515 				|| node;
  4518 	// "Otherwise, for each (command, override) pair in overrides, in order:"
  4519 	} else {
  4520 		for (var i = 0; i < overrides.length; i++) {
  4521 			var command = overrides[i][0];
  4522 			var override = overrides[i][1];
  4524 			// "If override is a boolean, set the state override for command to
  4525 			// override."
  4526 			if (typeof override == "boolean") {
  4527 				setStateOverride(command, override);
  4530 			// "If override is a string, set the value override for command to
  4531 			// override."
  4532 			if (typeof override == "string") {
  4533 				setValueOverride(command, override);
  4539 //@}
  4540 ///// Deleting the selection /////
  4541 //@{
  4543 // The flags argument is a dictionary that can have blockMerging,
  4544 // stripWrappers, and/or direction as keys.
  4545 function deleteSelection(flags) {
  4546 	if (flags === undefined) {
  4547 		flags = {};
  4550 	var blockMerging = "blockMerging" in flags ? Boolean(flags.blockMerging) : true;
  4551 	var stripWrappers = "stripWrappers" in flags ? Boolean(flags.stripWrappers) : true;
  4552 	var direction = "direction" in flags ? flags.direction : "forward";
  4554 	// "If the active range is null, abort these steps and do nothing."
  4555 	if (!getActiveRange()) {
  4556 		return;
  4559 	// "Canonicalize whitespace at the active range's start."
  4560 	canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
  4562 	// "Canonicalize whitespace at the active range's end."
  4563 	canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset);
  4565 	// "Let (start node, start offset) be the last equivalent point for the
  4566 	// active range's start."
  4567 	var start = getLastEquivalentPoint(getActiveRange().startContainer, getActiveRange().startOffset);
  4568 	var startNode = start[0];
  4569 	var startOffset = start[1];
  4571 	// "Let (end node, end offset) be the first equivalent point for the active
  4572 	// range's end."
  4573 	var end = getFirstEquivalentPoint(getActiveRange().endContainer, getActiveRange().endOffset);
  4574 	var endNode = end[0];
  4575 	var endOffset = end[1];
  4577 	// "If (end node, end offset) is not after (start node, start offset):"
  4578 	if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
  4579 		// "If direction is "forward", call collapseToStart() on the context
  4580 		// object's Selection."
  4581 		//
  4582 		// Here and in a few other places, we check rangeCount to work around a
  4583 		// WebKit bug: it will sometimes incorrectly remove ranges from the
  4584 		// selection if nodes are removed, so collapseToStart() will throw.
  4585 		// This will break everything if we're using an actual selection, but
  4586 		// if getActiveRange() is really just returning globalRange and that's
  4587 		// all we care about, it will work fine.  I only add the extra check
  4588 		// for errors I actually hit in testing.
  4589 		if (direction == "forward") {
  4590 			if (getSelection().rangeCount) {
  4591 				getSelection().collapseToStart();
  4593 			getActiveRange().collapse(true);
  4595 		// "Otherwise, call collapseToEnd() on the context object's Selection."
  4596 		} else {
  4597 			getSelection().collapseToEnd();
  4598 			getActiveRange().collapse(false);
  4601 		// "Abort these steps."
  4602 		return;
  4605 	// "If start node is a Text node and start offset is 0, set start offset to
  4606 	// the index of start node, then set start node to its parent."
  4607 	if (startNode.nodeType == Node.TEXT_NODE
  4608 	&& startOffset == 0) {
  4609 		startOffset = getNodeIndex(startNode);
  4610 		startNode = startNode.parentNode;
  4613 	// "If end node is a Text node and end offset is its length, set end offset
  4614 	// to one plus the index of end node, then set end node to its parent."
  4615 	if (endNode.nodeType == Node.TEXT_NODE
  4616 	&& endOffset == getNodeLength(endNode)) {
  4617 		endOffset = 1 + getNodeIndex(endNode);
  4618 		endNode = endNode.parentNode;
  4621 	// "Call collapse(start node, start offset) on the context object's
  4622 	// Selection."
  4623 	getSelection().collapse(startNode, startOffset);
  4624 	getActiveRange().setStart(startNode, startOffset);
  4626 	// "Call extend(end node, end offset) on the context object's Selection."
  4627 	getSelection().extend(endNode, endOffset);
  4628 	getActiveRange().setEnd(endNode, endOffset);
  4630 	// "Let start block be the active range's start node."
  4631 	var startBlock = getActiveRange().startContainer;
  4633 	// "While start block's parent is in the same editing host and start block
  4634 	// is an inline node, set start block to its parent."
  4635 	while (inSameEditingHost(startBlock, startBlock.parentNode)
  4636 	&& isInlineNode(startBlock)) {
  4637 		startBlock = startBlock.parentNode;
  4640 	// "If start block is neither a block node nor an editing host, or "span"
  4641 	// is not an allowed child of start block, or start block is a td or th,
  4642 	// set start block to null."
  4643 	if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
  4644 	|| !isAllowedChild("span", startBlock)
  4645 	|| isHtmlElement(startBlock, ["td", "th"])) {
  4646 		startBlock = null;
  4649 	// "Let end block be the active range's end node."
  4650 	var endBlock = getActiveRange().endContainer;
  4652 	// "While end block's parent is in the same editing host and end block is
  4653 	// an inline node, set end block to its parent."
  4654 	while (inSameEditingHost(endBlock, endBlock.parentNode)
  4655 	&& isInlineNode(endBlock)) {
  4656 		endBlock = endBlock.parentNode;
  4659 	// "If end block is neither a block node nor an editing host, or "span" is
  4660 	// not an allowed child of end block, or end block is a td or th, set end
  4661 	// block to null."
  4662 	if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
  4663 	|| !isAllowedChild("span", endBlock)
  4664 	|| isHtmlElement(endBlock, ["td", "th"])) {
  4665 		endBlock = null;
  4668 	// "Record current states and values, and let overrides be the result."
  4669 	var overrides = recordCurrentStatesAndValues();
  4671 	// "If start node and end node are the same, and start node is an editable
  4672 	// Text node:"
  4673 	if (startNode == endNode
  4674 	&& isEditable(startNode)
  4675 	&& startNode.nodeType == Node.TEXT_NODE) {
  4676 		// "Call deleteData(start offset, end offset − start offset) on start
  4677 		// node."
  4678 		startNode.deleteData(startOffset, endOffset - startOffset);
  4680 		// "Canonicalize whitespace at (start node, start offset), with fix
  4681 		// collapsed space false."
  4682 		canonicalizeWhitespace(startNode, startOffset, false);
  4684 		// "If direction is "forward", call collapseToStart() on the context
  4685 		// object's Selection."
  4686 		if (direction == "forward") {
  4687 			if (getSelection().rangeCount) {
  4688 				getSelection().collapseToStart();
  4690 			getActiveRange().collapse(true);
  4692 		// "Otherwise, call collapseToEnd() on the context object's Selection."
  4693 		} else {
  4694 			getSelection().collapseToEnd();
  4695 			getActiveRange().collapse(false);
  4698 		// "Restore states and values from overrides."
  4699 		restoreStatesAndValues(overrides);
  4701 		// "Abort these steps."
  4702 		return;
  4705 	// "If start node is an editable Text node, call deleteData() on it, with
  4706 	// start offset as the first argument and (length of start node − start
  4707 	// offset) as the second argument."
  4708 	if (isEditable(startNode)
  4709 	&& startNode.nodeType == Node.TEXT_NODE) {
  4710 		startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
  4713 	// "Let node list be a list of nodes, initially empty."
  4714 	//
  4715 	// "For each node contained in the active range, append node to node list
  4716 	// if the last member of node list (if any) is not an ancestor of node;
  4717 	// node is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
  4718 	var nodeList = getContainedNodes(getActiveRange(),
  4719 		function(node) {
  4720 			return isEditable(node)
  4721 				&& !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
  4723 	);
  4725 	// "For each node in node list:"
  4726 	for (var i = 0; i < nodeList.length; i++) {
  4727 		var node = nodeList[i];
  4729 		// "Let parent be the parent of node."
  4730 		var parent_ = node.parentNode;
  4732 		// "Remove node from parent."
  4733 		parent_.removeChild(node);
  4735 		// "If the block node of parent has no visible children, and parent is
  4736 		// editable or an editing host, call createElement("br") on the context
  4737 		// object and append the result as the last child of parent."
  4738 		if (![].some.call(getBlockNodeOf(parent_).childNodes, isVisible)
  4739 		&& (isEditable(parent_) || isEditingHost(parent_))) {
  4740 			parent_.appendChild(document.createElement("br"));
  4743 		// "If strip wrappers is true or parent is not an ancestor container of
  4744 		// start node, while parent is an editable inline node with length 0,
  4745 		// let grandparent be the parent of parent, then remove parent from
  4746 		// grandparent, then set parent to grandparent."
  4747 		if (stripWrappers
  4748 		|| (!isAncestor(parent_, startNode) && parent_ != startNode)) {
  4749 			while (isEditable(parent_)
  4750 			&& isInlineNode(parent_)
  4751 			&& getNodeLength(parent_) == 0) {
  4752 				var grandparent = parent_.parentNode;
  4753 				grandparent.removeChild(parent_);
  4754 				parent_ = grandparent;
  4759 	// "If end node is an editable Text node, call deleteData(0, end offset) on
  4760 	// it."
  4761 	if (isEditable(endNode)
  4762 	&& endNode.nodeType == Node.TEXT_NODE) {
  4763 		endNode.deleteData(0, endOffset);
  4766 	// "Canonicalize whitespace at the active range's start, with fix collapsed
  4767 	// space false."
  4768 	canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
  4770 	// "Canonicalize whitespace at the active range's end, with fix collapsed
  4771 	// space false."
  4772 	canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
  4774 	// "If block merging is false, or start block or end block is null, or
  4775 	// start block is not in the same editing host as end block, or start block
  4776 	// and end block are the same:"
  4777 	if (!blockMerging
  4778 	|| !startBlock
  4779 	|| !endBlock
  4780 	|| !inSameEditingHost(startBlock, endBlock)
  4781 	|| startBlock == endBlock) {
  4782 		// "If direction is "forward", call collapseToStart() on the context
  4783 		// object's Selection."
  4784 		if (direction == "forward") {
  4785 			if (getSelection().rangeCount) {
  4786 				getSelection().collapseToStart();
  4788 			getActiveRange().collapse(true);
  4790 		// "Otherwise, call collapseToEnd() on the context object's Selection."
  4791 		} else {
  4792 			if (getSelection().rangeCount) {
  4793 				getSelection().collapseToEnd();
  4795 			getActiveRange().collapse(false);
  4798 		// "Restore states and values from overrides."
  4799 		restoreStatesAndValues(overrides);
  4801 		// "Abort these steps."
  4802 		return;
  4805 	// "If start block has one child, which is a collapsed block prop, remove
  4806 	// its child from it."
  4807 	if (startBlock.children.length == 1
  4808 	&& isCollapsedBlockProp(startBlock.firstChild)) {
  4809 		startBlock.removeChild(startBlock.firstChild);
  4812 	// "If start block is an ancestor of end block:"
  4813 	if (isAncestor(startBlock, endBlock)) {
  4814 		// "Let reference node be end block."
  4815 		var referenceNode = endBlock;
  4817 		// "While reference node is not a child of start block, set reference
  4818 		// node to its parent."
  4819 		while (referenceNode.parentNode != startBlock) {
  4820 			referenceNode = referenceNode.parentNode;
  4823 		// "Call collapse() on the context object's Selection, with first
  4824 		// argument start block and second argument the index of reference
  4825 		// node."
  4826 		getSelection().collapse(startBlock, getNodeIndex(referenceNode));
  4827 		getActiveRange().setStart(startBlock, getNodeIndex(referenceNode));
  4828 		getActiveRange().collapse(true);
  4830 		// "If end block has no children:"
  4831 		if (!endBlock.hasChildNodes()) {
  4832 			// "While end block is editable and is the only child of its parent
  4833 			// and is not a child of start block, let parent equal end block,
  4834 			// then remove end block from parent, then set end block to
  4835 			// parent."
  4836 			while (isEditable(endBlock)
  4837 			&& endBlock.parentNode.childNodes.length == 1
  4838 			&& endBlock.parentNode != startBlock) {
  4839 				var parent_ = endBlock;
  4840 				parent_.removeChild(endBlock);
  4841 				endBlock = parent_;
  4844 			// "If end block is editable and is not an inline node, and its
  4845 			// previousSibling and nextSibling are both inline nodes, call
  4846 			// createElement("br") on the context object and insert it into end
  4847 			// block's parent immediately after end block."
  4848 			if (isEditable(endBlock)
  4849 			&& !isInlineNode(endBlock)
  4850 			&& isInlineNode(endBlock.previousSibling)
  4851 			&& isInlineNode(endBlock.nextSibling)) {
  4852 				endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling);
  4855 			// "If end block is editable, remove it from its parent."
  4856 			if (isEditable(endBlock)) {
  4857 				endBlock.parentNode.removeChild(endBlock);
  4860 			// "Restore states and values from overrides."
  4861 			restoreStatesAndValues(overrides);
  4863 			// "Abort these steps."
  4864 			return;
  4867 		// "If end block's firstChild is not an inline node, restore states and
  4868 		// values from overrides, then abort these steps."
  4869 		if (!isInlineNode(endBlock.firstChild)) {
  4870 			restoreStatesAndValues(overrides);
  4871 			return;
  4874 		// "Let children be a list of nodes, initially empty."
  4875 		var children = [];
  4877 		// "Append the first child of end block to children."
  4878 		children.push(endBlock.firstChild);
  4880 		// "While children's last member is not a br, and children's last
  4881 		// member's nextSibling is an inline node, append children's last
  4882 		// member's nextSibling to children."
  4883 		while (!isHtmlElement(children[children.length - 1], "br")
  4884 		&& isInlineNode(children[children.length - 1].nextSibling)) {
  4885 			children.push(children[children.length - 1].nextSibling);
  4888 		// "Record the values of children, and let values be the result."
  4889 		var values = recordValues(children);
  4891 		// "While children's first member's parent is not start block, split
  4892 		// the parent of children."
  4893 		while (children[0].parentNode != startBlock) {
  4894 			splitParent(children);
  4897 		// "If children's first member's previousSibling is an editable br,
  4898 		// remove that br from its parent."
  4899 		if (isEditable(children[0].previousSibling)
  4900 		&& isHtmlElement(children[0].previousSibling, "br")) {
  4901 			children[0].parentNode.removeChild(children[0].previousSibling);
  4904 	// "Otherwise, if start block is a descendant of end block:"
  4905 	} else if (isDescendant(startBlock, endBlock)) {
  4906 		// "Call collapse() on the context object's Selection, with first
  4907 		// argument start block and second argument start block's length."
  4908 		getSelection().collapse(startBlock, getNodeLength(startBlock));
  4909 		getActiveRange().setStart(startBlock, getNodeLength(startBlock));
  4910 		getActiveRange().collapse(true);
  4912 		// "Let reference node be start block."
  4913 		var referenceNode = startBlock;
  4915 		// "While reference node is not a child of end block, set reference
  4916 		// node to its parent."
  4917 		while (referenceNode.parentNode != endBlock) {
  4918 			referenceNode = referenceNode.parentNode;
  4921 		// "If reference node's nextSibling is an inline node and start block's
  4922 		// lastChild is a br, remove start block's lastChild from it."
  4923 		if (isInlineNode(referenceNode.nextSibling)
  4924 		&& isHtmlElement(startBlock.lastChild, "br")) {
  4925 			startBlock.removeChild(startBlock.lastChild);
  4928 		// "Let nodes to move be a list of nodes, initially empty."
  4929 		var nodesToMove = [];
  4931 		// "If reference node's nextSibling is neither null nor a block node,
  4932 		// append it to nodes to move."
  4933 		if (referenceNode.nextSibling
  4934 		&& !isBlockNode(referenceNode.nextSibling)) {
  4935 			nodesToMove.push(referenceNode.nextSibling);
  4938 		// "While nodes to move is nonempty and its last member isn't a br and
  4939 		// its last member's nextSibling is neither null nor a block node,
  4940 		// append its last member's nextSibling to nodes to move."
  4941 		if (nodesToMove.length
  4942 		&& !isHtmlElement(nodesToMove[nodesToMove.length - 1], "br")
  4943 		&& nodesToMove[nodesToMove.length - 1].nextSibling
  4944 		&& !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
  4945 			nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
  4948 		// "Record the values of nodes to move, and let values be the result."
  4949 		var values = recordValues(nodesToMove);
  4951 		// "For each node in nodes to move, append node as the last child of
  4952 		// start block, preserving ranges."
  4953 		nodesToMove.forEach(function(node) {
  4954 			movePreservingRanges(node, startBlock, -1);
  4955 		});
  4957 	// "Otherwise:"
  4958 	} else {
  4959 		// "Call collapse() on the context object's Selection, with first
  4960 		// argument start block and second argument start block's length."
  4961 		getSelection().collapse(startBlock, getNodeLength(startBlock));
  4962 		getActiveRange().setStart(startBlock, getNodeLength(startBlock));
  4963 		getActiveRange().collapse(true);
  4965 		// "If end block's firstChild is an inline node and start block's
  4966 		// lastChild is a br, remove start block's lastChild from it."
  4967 		if (isInlineNode(endBlock.firstChild)
  4968 		&& isHtmlElement(startBlock.lastChild, "br")) {
  4969 			startBlock.removeChild(startBlock.lastChild);
  4972 		// "Record the values of end block's children, and let values be the
  4973 		// result."
  4974 		var values = recordValues([].slice.call(endBlock.childNodes));
  4976 		// "While end block has children, append the first child of end block
  4977 		// to start block, preserving ranges."
  4978 		while (endBlock.hasChildNodes()) {
  4979 			movePreservingRanges(endBlock.firstChild, startBlock, -1);
  4982 		// "While end block has no children, let parent be the parent of end
  4983 		// block, then remove end block from parent, then set end block to
  4984 		// parent."
  4985 		while (!endBlock.hasChildNodes()) {
  4986 			var parent_ = endBlock.parentNode;
  4987 			parent_.removeChild(endBlock);
  4988 			endBlock = parent_;
  4992 	// "Let ancestor be start block."
  4993 	var ancestor = startBlock;
  4995 	// "While ancestor has an inclusive ancestor ol in the same editing host
  4996 	// whose nextSibling is also an ol in the same editing host, or an
  4997 	// inclusive ancestor ul in the same editing host whose nextSibling is also
  4998 	// a ul in the same editing host:"
  4999 	while (getInclusiveAncestors(ancestor).some(function(node) {
  5000 		return inSameEditingHost(ancestor, node)
  5001 			&& (
  5002 				(isHtmlElement(node, "ol") && isHtmlElement(node.nextSibling, "ol"))
  5003 				|| (isHtmlElement(node, "ul") && isHtmlElement(node.nextSibling, "ul"))
  5004 			) && inSameEditingHost(ancestor, node.nextSibling);
  5005 	})) {
  5006 		// "While ancestor and its nextSibling are not both ols in the same
  5007 		// editing host, and are also not both uls in the same editing host,
  5008 		// set ancestor to its parent."
  5009 		while (!(
  5010 			isHtmlElement(ancestor, "ol")
  5011 			&& isHtmlElement(ancestor.nextSibling, "ol")
  5012 			&& inSameEditingHost(ancestor, ancestor.nextSibling)
  5013 		) && !(
  5014 			isHtmlElement(ancestor, "ul")
  5015 			&& isHtmlElement(ancestor.nextSibling, "ul")
  5016 			&& inSameEditingHost(ancestor, ancestor.nextSibling)
  5017 		)) {
  5018 			ancestor = ancestor.parentNode;
  5021 		// "While ancestor's nextSibling has children, append ancestor's
  5022 		// nextSibling's firstChild as the last child of ancestor, preserving
  5023 		// ranges."
  5024 		while (ancestor.nextSibling.hasChildNodes()) {
  5025 			movePreservingRanges(ancestor.nextSibling.firstChild, ancestor, -1);
  5028 		// "Remove ancestor's nextSibling from its parent."
  5029 		ancestor.parentNode.removeChild(ancestor.nextSibling);
  5032 	// "Restore the values from values."
  5033 	restoreValues(values);
  5035 	// "If start block has no children, call createElement("br") on the context
  5036 	// object and append the result as the last child of start block."
  5037 	if (!startBlock.hasChildNodes()) {
  5038 		startBlock.appendChild(document.createElement("br"));
  5041 	// "Remove extraneous line breaks at the end of start block."
  5042 	removeExtraneousLineBreaksAtTheEndOf(startBlock);
  5044 	// "Restore states and values from overrides."
  5045 	restoreStatesAndValues(overrides);
  5049 //@}
  5050 ///// Splitting a node list's parent /////
  5051 //@{
  5053 function splitParent(nodeList) {
  5054 	// "Let original parent be the parent of the first member of node list."
  5055 	var originalParent = nodeList[0].parentNode;
  5057 	// "If original parent is not editable or its parent is null, do nothing
  5058 	// and abort these steps."
  5059 	if (!isEditable(originalParent)
  5060 	|| !originalParent.parentNode) {
  5061 		return;
  5064 	// "If the first child of original parent is in node list, remove
  5065 	// extraneous line breaks before original parent."
  5066 	if (nodeList.indexOf(originalParent.firstChild) != -1) {
  5067 		removeExtraneousLineBreaksBefore(originalParent);
  5070 	// "If the first child of original parent is in node list, and original
  5071 	// parent follows a line break, set follows line break to true. Otherwise,
  5072 	// set follows line break to false."
  5073 	var followsLineBreak_ = nodeList.indexOf(originalParent.firstChild) != -1
  5074 		&& followsLineBreak(originalParent);
  5076 	// "If the last child of original parent is in node list, and original
  5077 	// parent precedes a line break, set precedes line break to true.
  5078 	// Otherwise, set precedes line break to false."
  5079 	var precedesLineBreak_ = nodeList.indexOf(originalParent.lastChild) != -1
  5080 		&& precedesLineBreak(originalParent);
  5082 	// "If the first child of original parent is not in node list, but its last
  5083 	// child is:"
  5084 	if (nodeList.indexOf(originalParent.firstChild) == -1
  5085 	&& nodeList.indexOf(originalParent.lastChild) != -1) {
  5086 		// "For each node in node list, in reverse order, insert node into the
  5087 		// parent of original parent immediately after original parent,
  5088 		// preserving ranges."
  5089 		for (var i = nodeList.length - 1; i >= 0; i--) {
  5090 			movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent));
  5093 		// "If precedes line break is true, and the last member of node list
  5094 		// does not precede a line break, call createElement("br") on the
  5095 		// context object and insert the result immediately after the last
  5096 		// member of node list."
  5097 		if (precedesLineBreak_
  5098 		&& !precedesLineBreak(nodeList[nodeList.length - 1])) {
  5099 			nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
  5102 		// "Remove extraneous line breaks at the end of original parent."
  5103 		removeExtraneousLineBreaksAtTheEndOf(originalParent);
  5105 		// "Abort these steps."
  5106 		return;
  5109 	// "If the first child of original parent is not in node list:"
  5110 	if (nodeList.indexOf(originalParent.firstChild) == -1) {
  5111 		// "Let cloned parent be the result of calling cloneNode(false) on
  5112 		// original parent."
  5113 		var clonedParent = originalParent.cloneNode(false);
  5115 		// "If original parent has an id attribute, unset it."
  5116 		originalParent.removeAttribute("id");
  5118 		// "Insert cloned parent into the parent of original parent immediately
  5119 		// before original parent."
  5120 		originalParent.parentNode.insertBefore(clonedParent, originalParent);
  5122 		// "While the previousSibling of the first member of node list is not
  5123 		// null, append the first child of original parent as the last child of
  5124 		// cloned parent, preserving ranges."
  5125 		while (nodeList[0].previousSibling) {
  5126 			movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length);
  5130 	// "For each node in node list, insert node into the parent of original
  5131 	// parent immediately before original parent, preserving ranges."
  5132 	for (var i = 0; i < nodeList.length; i++) {
  5133 		movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent));
  5136 	// "If follows line break is true, and the first member of node list does
  5137 	// not follow a line break, call createElement("br") on the context object
  5138 	// and insert the result immediately before the first member of node list."
  5139 	if (followsLineBreak_
  5140 	&& !followsLineBreak(nodeList[0])) {
  5141 		nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]);
  5144 	// "If the last member of node list is an inline node other than a br, and
  5145 	// the first child of original parent is a br, and original parent is not
  5146 	// an inline node, remove the first child of original parent from original
  5147 	// parent."
  5148 	if (isInlineNode(nodeList[nodeList.length - 1])
  5149 	&& !isHtmlElement(nodeList[nodeList.length - 1], "br")
  5150 	&& isHtmlElement(originalParent.firstChild, "br")
  5151 	&& !isInlineNode(originalParent)) {
  5152 		originalParent.removeChild(originalParent.firstChild);
  5155 	// "If original parent has no children:"
  5156 	if (!originalParent.hasChildNodes()) {
  5157 		// "Remove original parent from its parent."
  5158 		originalParent.parentNode.removeChild(originalParent);
  5160 		// "If precedes line break is true, and the last member of node list
  5161 		// does not precede a line break, call createElement("br") on the
  5162 		// context object and insert the result immediately after the last
  5163 		// member of node list."
  5164 		if (precedesLineBreak_
  5165 		&& !precedesLineBreak(nodeList[nodeList.length - 1])) {
  5166 			nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
  5169 	// "Otherwise, remove extraneous line breaks before original parent."
  5170 	} else {
  5171 		removeExtraneousLineBreaksBefore(originalParent);
  5174 	// "If node list's last member's nextSibling is null, but its parent is not
  5175 	// null, remove extraneous line breaks at the end of node list's last
  5176 	// member's parent."
  5177 	if (!nodeList[nodeList.length - 1].nextSibling
  5178 	&& nodeList[nodeList.length - 1].parentNode) {
  5179 		removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode);
  5183 // "To remove a node node while preserving its descendants, split the parent of
  5184 // node's children if it has any. If it has no children, instead remove it from
  5185 // its parent."
  5186 function removePreservingDescendants(node) {
  5187 	if (node.hasChildNodes()) {
  5188 		splitParent([].slice.call(node.childNodes));
  5189 	} else {
  5190 		node.parentNode.removeChild(node);
  5195 //@}
  5196 ///// Canonical space sequences /////
  5197 //@{
  5199 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
  5200 	// "If n is zero, return the empty string."
  5201 	if (n == 0) {
  5202 		return "";
  5205 	// "If n is one and both non-breaking start and non-breaking end are false,
  5206 	// return a single space (U+0020)."
  5207 	if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
  5208 		return " ";
  5211 	// "If n is one, return a single non-breaking space (U+00A0)."
  5212 	if (n == 1) {
  5213 		return "\xa0";
  5216 	// "Let buffer be the empty string."
  5217 	var buffer = "";
  5219 	// "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
  5220 	// Otherwise, let it be U+0020 U+00A0."
  5221 	var repeatedPair;
  5222 	if (nonBreakingStart) {
  5223 		repeatedPair = "\xa0 ";
  5224 	} else {
  5225 		repeatedPair = " \xa0";
  5228 	// "While n is greater than three, append repeated pair to buffer and
  5229 	// subtract two from n."
  5230 	while (n > 3) {
  5231 		buffer += repeatedPair;
  5232 		n -= 2;
  5235 	// "If n is three, append a three-element string to buffer depending on
  5236 	// non-breaking start and non-breaking end:"
  5237 	if (n == 3) {
  5238 		buffer +=
  5239 			!nonBreakingStart && !nonBreakingEnd ? " \xa0 "
  5240 			: nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
  5241 			: !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
  5242 			: nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
  5243 			: "impossible";
  5245 	// "Otherwise, append a two-element string to buffer depending on
  5246 	// non-breaking start and non-breaking end:"
  5247 	} else {
  5248 		buffer +=
  5249 			!nonBreakingStart && !nonBreakingEnd ? "\xa0 "
  5250 			: nonBreakingStart && !nonBreakingEnd ? "\xa0 "
  5251 			: !nonBreakingStart && nonBreakingEnd ? " \xa0"
  5252 			: nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
  5253 			: "impossible";
  5256 	// "Return buffer."
  5257 	return buffer;
  5260 function canonicalizeWhitespace(node, offset, fixCollapsedSpace) {
  5261 	if (fixCollapsedSpace === undefined) {
  5262 		// "an optional boolean argument fix collapsed space that defaults to
  5263 		// true"
  5264 		fixCollapsedSpace = true;
  5267 	// "If node is neither editable nor an editing host, abort these steps."
  5268 	if (!isEditable(node) && !isEditingHost(node)) {
  5269 		return;
  5272 	// "Let start node equal node and let start offset equal offset."
  5273 	var startNode = node;
  5274 	var startOffset = offset;
  5276 	// "Repeat the following steps:"
  5277 	while (true) {
  5278 		// "If start node has a child in the same editing host with index start
  5279 		// offset minus one, set start node to that child, then set start
  5280 		// offset to start node's length."
  5281 		if (0 <= startOffset - 1
  5282 		&& inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
  5283 			startNode = startNode.childNodes[startOffset - 1];
  5284 			startOffset = getNodeLength(startNode);
  5286 		// "Otherwise, if start offset is zero and start node does not follow a
  5287 		// line break and start node's parent is in the same editing host, set
  5288 		// start offset to start node's index, then set start node to its
  5289 		// parent."
  5290 		} else if (startOffset == 0
  5291 		&& !followsLineBreak(startNode)
  5292 		&& inSameEditingHost(startNode, startNode.parentNode)) {
  5293 			startOffset = getNodeIndex(startNode);
  5294 			startNode = startNode.parentNode;
  5296 		// "Otherwise, if start node is a Text node and its parent's resolved
  5297 		// value for "white-space" is neither "pre" nor "pre-wrap" and start
  5298 		// offset is not zero and the (start offset − 1)st element of start
  5299 		// node's data is a space (0x0020) or non-breaking space (0x00A0),
  5300 		// subtract one from start offset."
  5301 		} else if (startNode.nodeType == Node.TEXT_NODE
  5302 		&& ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1
  5303 		&& startOffset != 0
  5304 		&& /[ \xa0]/.test(startNode.data[startOffset - 1])) {
  5305 			startOffset--;
  5307 		// "Otherwise, break from this loop."
  5308 		} else {
  5309 			break;
  5313 	// "Let end node equal start node and end offset equal start offset."
  5314 	var endNode = startNode;
  5315 	var endOffset = startOffset;
  5317 	// "Let length equal zero."
  5318 	var length = 0;
  5320 	// "Let collapse spaces be true if start offset is zero and start node
  5321 	// follows a line break, otherwise false."
  5322 	var collapseSpaces = startOffset == 0 && followsLineBreak(startNode);
  5324 	// "Repeat the following steps:"
  5325 	while (true) {
  5326 		// "If end node has a child in the same editing host with index end
  5327 		// offset, set end node to that child, then set end offset to zero."
  5328 		if (endOffset < endNode.childNodes.length
  5329 		&& inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
  5330 			endNode = endNode.childNodes[endOffset];
  5331 			endOffset = 0;
  5333 		// "Otherwise, if end offset is end node's length and end node does not
  5334 		// precede a line break and end node's parent is in the same editing
  5335 		// host, set end offset to one plus end node's index, then set end node
  5336 		// to its parent."
  5337 		} else if (endOffset == getNodeLength(endNode)
  5338 		&& !precedesLineBreak(endNode)
  5339 		&& inSameEditingHost(endNode, endNode.parentNode)) {
  5340 			endOffset = 1 + getNodeIndex(endNode);
  5341 			endNode = endNode.parentNode;
  5343 		// "Otherwise, if end node is a Text node and its parent's resolved
  5344 		// value for "white-space" is neither "pre" nor "pre-wrap" and end
  5345 		// offset is not end node's length and the end offsetth element of
  5346 		// end node's data is a space (0x0020) or non-breaking space (0x00A0):"
  5347 		} else if (endNode.nodeType == Node.TEXT_NODE
  5348 		&& ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
  5349 		&& endOffset != getNodeLength(endNode)
  5350 		&& /[ \xa0]/.test(endNode.data[endOffset])) {
  5351 			// "If fix collapsed space is true, and collapse spaces is true,
  5352 			// and the end offsetth code unit of end node's data is a space
  5353 			// (0x0020): call deleteData(end offset, 1) on end node, then
  5354 			// continue this loop from the beginning."
  5355 			if (fixCollapsedSpace
  5356 			&& collapseSpaces
  5357 			&& " " == endNode.data[endOffset]) {
  5358 				endNode.deleteData(endOffset, 1);
  5359 				continue;
  5362 			// "Set collapse spaces to true if the end offsetth element of end
  5363 			// node's data is a space (0x0020), false otherwise."
  5364 			collapseSpaces = " " == endNode.data[endOffset];
  5366 			// "Add one to end offset."
  5367 			endOffset++;
  5369 			// "Add one to length."
  5370 			length++;
  5372 		// "Otherwise, break from this loop."
  5373 		} else {
  5374 			break;
  5378 	// "If fix collapsed space is true, then while (start node, start offset)
  5379 	// is before (end node, end offset):"
  5380 	if (fixCollapsedSpace) {
  5381 		while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
  5382 			// "If end node has a child in the same editing host with index end
  5383 			// offset − 1, set end node to that child, then set end offset to end
  5384 			// node's length."
  5385 			if (0 <= endOffset - 1
  5386 			&& endOffset - 1 < endNode.childNodes.length
  5387 			&& inSameEditingHost(endNode, endNode.childNodes[endOffset - 1])) {
  5388 				endNode = endNode.childNodes[endOffset - 1];
  5389 				endOffset = getNodeLength(endNode);
  5391 			// "Otherwise, if end offset is zero and end node's parent is in the
  5392 			// same editing host, set end offset to end node's index, then set end
  5393 			// node to its parent."
  5394 			} else if (endOffset == 0
  5395 			&& inSameEditingHost(endNode, endNode.parentNode)) {
  5396 				endOffset = getNodeIndex(endNode);
  5397 				endNode = endNode.parentNode;
  5399 			// "Otherwise, if end node is a Text node and its parent's resolved
  5400 			// value for "white-space" is neither "pre" nor "pre-wrap" and end
  5401 			// offset is end node's length and the last code unit of end node's
  5402 			// data is a space (0x0020) and end node precedes a line break:"
  5403 			} else if (endNode.nodeType == Node.TEXT_NODE
  5404 			&& ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
  5405 			&& endOffset == getNodeLength(endNode)
  5406 			&& endNode.data[endNode.data.length - 1] == " "
  5407 			&& precedesLineBreak(endNode)) {
  5408 				// "Subtract one from end offset."
  5409 				endOffset--;
  5411 				// "Subtract one from length."
  5412 				length--;
  5414 				// "Call deleteData(end offset, 1) on end node."
  5415 				endNode.deleteData(endOffset, 1);
  5417 			// "Otherwise, break from this loop."
  5418 			} else {
  5419 				break;
  5424 	// "Let replacement whitespace be the canonical space sequence of length
  5425 	// length. non-breaking start is true if start offset is zero and start
  5426 	// node follows a line break, and false otherwise. non-breaking end is true
  5427 	// if end offset is end node's length and end node precedes a line break,
  5428 	// and false otherwise."
  5429 	var replacementWhitespace = canonicalSpaceSequence(length,
  5430 		startOffset == 0 && followsLineBreak(startNode),
  5431 		endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
  5433 	// "While (start node, start offset) is before (end node, end offset):"
  5434 	while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
  5435 		// "If start node has a child with index start offset, set start node
  5436 		// to that child, then set start offset to zero."
  5437 		if (startOffset < startNode.childNodes.length) {
  5438 			startNode = startNode.childNodes[startOffset];
  5439 			startOffset = 0;
  5441 		// "Otherwise, if start node is not a Text node or if start offset is
  5442 		// start node's length, set start offset to one plus start node's
  5443 		// index, then set start node to its parent."
  5444 		} else if (startNode.nodeType != Node.TEXT_NODE
  5445 		|| startOffset == getNodeLength(startNode)) {
  5446 			startOffset = 1 + getNodeIndex(startNode);
  5447 			startNode = startNode.parentNode;
  5449 		// "Otherwise:"
  5450 		} else {
  5451 			// "Remove the first element from replacement whitespace, and let
  5452 			// element be that element."
  5453 			var element = replacementWhitespace[0];
  5454 			replacementWhitespace = replacementWhitespace.slice(1);
  5456 			// "If element is not the same as the start offsetth element of
  5457 			// start node's data:"
  5458 			if (element != startNode.data[startOffset]) {
  5459 				// "Call insertData(start offset, element) on start node."
  5460 				startNode.insertData(startOffset, element);
  5462 				// "Call deleteData(start offset + 1, 1) on start node."
  5463 				startNode.deleteData(startOffset + 1, 1);
  5466 			// "Add one to start offset."
  5467 			startOffset++;
  5473 //@}
  5474 ///// Indenting and outdenting /////
  5475 //@{
  5477 function indentNodes(nodeList) {
  5478 	// "If node list is empty, do nothing and abort these steps."
  5479 	if (!nodeList.length) {
  5480 		return;
  5483 	// "Let first node be the first member of node list."
  5484 	var firstNode = nodeList[0];
  5486 	// "If first node's parent is an ol or ul:"
  5487 	if (isHtmlElement(firstNode.parentNode, ["OL", "UL"])) {
  5488 		// "Let tag be the local name of the parent of first node."
  5489 		var tag = firstNode.parentNode.tagName;
  5491 		// "Wrap node list, with sibling criteria returning true for an HTML
  5492 		// element with local name tag and false otherwise, and new parent
  5493 		// instructions returning the result of calling createElement(tag) on
  5494 		// the ownerDocument of first node."
  5495 		wrap(nodeList,
  5496 			function(node) { return isHtmlElement(node, tag) },
  5497 			function() { return firstNode.ownerDocument.createElement(tag) });
  5499 		// "Abort these steps."
  5500 		return;
  5503 	// "Wrap node list, with sibling criteria returning true for a simple
  5504 	// indentation element and false otherwise, and new parent instructions
  5505 	// returning the result of calling createElement("blockquote") on the
  5506 	// ownerDocument of first node. Let new parent be the result."
  5507 	var newParent = wrap(nodeList,
  5508 		function(node) { return isSimpleIndentationElement(node) },
  5509 		function() { return firstNode.ownerDocument.createElement("blockquote") });
  5511 	// "Fix disallowed ancestors of new parent."
  5512 	fixDisallowedAncestors(newParent);
  5515 function outdentNode(node) {
  5516 	// "If node is not editable, abort these steps."
  5517 	if (!isEditable(node)) {
  5518 		return;
  5521 	// "If node is a simple indentation element, remove node, preserving its
  5522 	// descendants.  Then abort these steps."
  5523 	if (isSimpleIndentationElement(node)) {
  5524 		removePreservingDescendants(node);
  5525 		return;
  5528 	// "If node is an indentation element:"
  5529 	if (isIndentationElement(node)) {
  5530 		// "Unset the dir attribute of node, if any."
  5531 		node.removeAttribute("dir");
  5533 		// "Unset the margin, padding, and border CSS properties of node."
  5534 		node.style.margin = "";
  5535 		node.style.padding = "";
  5536 		node.style.border = "";
  5537 		if (node.getAttribute("style") == ""
  5538 		// Crazy WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=68551
  5539 		|| node.getAttribute("style") == "border-width: initial; border-color: initial; ") {
  5540 			node.removeAttribute("style");
  5543 		// "Set the tag name of node to "div"."
  5544 		setTagName(node, "div");
  5546 		// "Abort these steps."
  5547 		return;
  5550 	// "Let current ancestor be node's parent."
  5551 	var currentAncestor = node.parentNode;
  5553 	// "Let ancestor list be a list of nodes, initially empty."
  5554 	var ancestorList = [];
  5556 	// "While current ancestor is an editable Element that is neither a simple
  5557 	// indentation element nor an ol nor a ul, append current ancestor to
  5558 	// ancestor list and then set current ancestor to its parent."
  5559 	while (isEditable(currentAncestor)
  5560 	&& currentAncestor.nodeType == Node.ELEMENT_NODE
  5561 	&& !isSimpleIndentationElement(currentAncestor)
  5562 	&& !isHtmlElement(currentAncestor, ["ol", "ul"])) {
  5563 		ancestorList.push(currentAncestor);
  5564 		currentAncestor = currentAncestor.parentNode;
  5567 	// "If current ancestor is not an editable simple indentation element:"
  5568 	if (!isEditable(currentAncestor)
  5569 	|| !isSimpleIndentationElement(currentAncestor)) {
  5570 		// "Let current ancestor be node's parent."
  5571 		currentAncestor = node.parentNode;
  5573 		// "Let ancestor list be the empty list."
  5574 		ancestorList = [];
  5576 		// "While current ancestor is an editable Element that is neither an
  5577 		// indentation element nor an ol nor a ul, append current ancestor to
  5578 		// ancestor list and then set current ancestor to its parent."
  5579 		while (isEditable(currentAncestor)
  5580 		&& currentAncestor.nodeType == Node.ELEMENT_NODE
  5581 		&& !isIndentationElement(currentAncestor)
  5582 		&& !isHtmlElement(currentAncestor, ["ol", "ul"])) {
  5583 			ancestorList.push(currentAncestor);
  5584 			currentAncestor = currentAncestor.parentNode;
  5588 	// "If node is an ol or ul and current ancestor is not an editable
  5589 	// indentation element:"
  5590 	if (isHtmlElement(node, ["OL", "UL"])
  5591 	&& (!isEditable(currentAncestor)
  5592 	|| !isIndentationElement(currentAncestor))) {
  5593 		// "Unset the reversed, start, and type attributes of node, if any are
  5594 		// set."
  5595 		node.removeAttribute("reversed");
  5596 		node.removeAttribute("start");
  5597 		node.removeAttribute("type");
  5599 		// "Let children be the children of node."
  5600 		var children = [].slice.call(node.childNodes);
  5602 		// "If node has attributes, and its parent is not an ol or ul, set the
  5603 		// tag name of node to "div"."
  5604 		if (node.attributes.length
  5605 		&& !isHtmlElement(node.parentNode, ["OL", "UL"])) {
  5606 			setTagName(node, "div");
  5608 		// "Otherwise:"
  5609 		} else {
  5610 			// "Record the values of node's children, and let values be the
  5611 			// result."
  5612 			var values = recordValues([].slice.call(node.childNodes));
  5614 			// "Remove node, preserving its descendants."
  5615 			removePreservingDescendants(node);
  5617 			// "Restore the values from values."
  5618 			restoreValues(values);
  5621 		// "Fix disallowed ancestors of each member of children."
  5622 		for (var i = 0; i < children.length; i++) {
  5623 			fixDisallowedAncestors(children[i]);
  5626 		// "Abort these steps."
  5627 		return;
  5630 	// "If current ancestor is not an editable indentation element, abort these
  5631 	// steps."
  5632 	if (!isEditable(currentAncestor)
  5633 	|| !isIndentationElement(currentAncestor)) {
  5634 		return;
  5637 	// "Append current ancestor to ancestor list."
  5638 	ancestorList.push(currentAncestor);
  5640 	// "Let original ancestor be current ancestor."
  5641 	var originalAncestor = currentAncestor;
  5643 	// "While ancestor list is not empty:"
  5644 	while (ancestorList.length) {
  5645 		// "Let current ancestor be the last member of ancestor list."
  5646 		//
  5647 		// "Remove the last member of ancestor list."
  5648 		currentAncestor = ancestorList.pop();
  5650 		// "Let target be the child of current ancestor that is equal to either
  5651 		// node or the last member of ancestor list."
  5652 		var target = node.parentNode == currentAncestor
  5653 			? node
  5654 			: ancestorList[ancestorList.length - 1];
  5656 		// "If target is an inline node that is not a br, and its nextSibling
  5657 		// is a br, remove target's nextSibling from its parent."
  5658 		if (isInlineNode(target)
  5659 		&& !isHtmlElement(target, "BR")
  5660 		&& isHtmlElement(target.nextSibling, "BR")) {
  5661 			target.parentNode.removeChild(target.nextSibling);
  5664 		// "Let preceding siblings be the preceding siblings of target, and let
  5665 		// following siblings be the following siblings of target."
  5666 		var precedingSiblings = [].slice.call(currentAncestor.childNodes, 0, getNodeIndex(target));
  5667 		var followingSiblings = [].slice.call(currentAncestor.childNodes, 1 + getNodeIndex(target));
  5669 		// "Indent preceding siblings."
  5670 		indentNodes(precedingSiblings);
  5672 		// "Indent following siblings."
  5673 		indentNodes(followingSiblings);
  5676 	// "Outdent original ancestor."
  5677 	outdentNode(originalAncestor);
  5681 //@}
  5682 ///// Toggling lists /////
  5683 //@{
  5685 function toggleLists(tagName) {
  5686 	// "Let mode be "disable" if the selection's list state is tag name, and
  5687 	// "enable" otherwise."
  5688 	var mode = getSelectionListState() == tagName ? "disable" : "enable";
  5690 	var range = getActiveRange();
  5691 	tagName = tagName.toUpperCase();
  5693 	// "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
  5694 	// "ol"."
  5695 	var otherTagName = tagName == "OL" ? "UL" : "OL";
  5697 	// "Let items be a list of all lis that are ancestor containers of the
  5698 	// range's start and/or end node."
  5699 	//
  5700 	// It's annoying to get this in tree order using functional stuff without
  5701 	// doing getDescendants(document), which is slow, so I do it imperatively.
  5702 	var items = [];
  5703 	(function(){
  5704 		for (
  5705 			var ancestorContainer = range.endContainer;
  5706 			ancestorContainer != range.commonAncestorContainer;
  5707 			ancestorContainer = ancestorContainer.parentNode
  5708 		) {
  5709 			if (isHtmlElement(ancestorContainer, "li")) {
  5710 				items.unshift(ancestorContainer);
  5713 		for (
  5714 			var ancestorContainer = range.startContainer;
  5715 			ancestorContainer;
  5716 			ancestorContainer = ancestorContainer.parentNode
  5717 		) {
  5718 			if (isHtmlElement(ancestorContainer, "li")) {
  5719 				items.unshift(ancestorContainer);
  5722 	})();
  5724 	// "For each item in items, normalize sublists of item."
  5725 	items.forEach(normalizeSublists);
  5727 	// "Block-extend the range, and let new range be the result."
  5728 	var newRange = blockExtend(range);
  5730 	// "If mode is "enable", then let lists to convert consist of every
  5731 	// editable HTML element with local name other tag name that is contained
  5732 	// in new range, and for every list in lists to convert:"
  5733 	if (mode == "enable") {
  5734 		getAllContainedNodes(newRange, function(node) {
  5735 			return isEditable(node)
  5736 				&& isHtmlElement(node, otherTagName);
  5737 		}).forEach(function(list) {
  5738 			// "If list's previousSibling or nextSibling is an editable HTML
  5739 			// element with local name tag name:"
  5740 			if ((isEditable(list.previousSibling) && isHtmlElement(list.previousSibling, tagName))
  5741 			|| (isEditable(list.nextSibling) && isHtmlElement(list.nextSibling, tagName))) {
  5742 				// "Let children be list's children."
  5743 				var children = [].slice.call(list.childNodes);
  5745 				// "Record the values of children, and let values be the
  5746 				// result."
  5747 				var values = recordValues(children);
  5749 				// "Split the parent of children."
  5750 				splitParent(children);
  5752 				// "Wrap children, with sibling criteria returning true for an
  5753 				// HTML element with local name tag name and false otherwise."
  5754 				wrap(children, function(node) { return isHtmlElement(node, tagName) });
  5756 				// "Restore the values from values."
  5757 				restoreValues(values);
  5759 			// "Otherwise, set the tag name of list to tag name."
  5760 			} else {
  5761 				setTagName(list, tagName);
  5763 		});
  5766 	// "Let node list be a list of nodes, initially empty."
  5767 	//
  5768 	// "For each node node contained in new range, if node is editable; the
  5769 	// last member of node list (if any) is not an ancestor of node; node
  5770 	// is not an indentation element; and either node is an ol or ul, or its
  5771 	// parent is an ol or ul, or it is an allowed child of "li"; then append
  5772 	// node to node list."
  5773 	var nodeList = getContainedNodes(newRange, function(node) {
  5774 		return isEditable(node)
  5775 		&& !isIndentationElement(node)
  5776 		&& (isHtmlElement(node, ["OL", "UL"])
  5777 		|| isHtmlElement(node.parentNode, ["OL", "UL"])
  5778 		|| isAllowedChild(node, "li"));
  5779 	});
  5781 	// "If mode is "enable", remove from node list any ol or ul whose parent is
  5782 	// not also an ol or ul."
  5783 	if (mode == "enable") {
  5784 		nodeList = nodeList.filter(function(node) {
  5785 			return !isHtmlElement(node, ["ol", "ul"])
  5786 				|| isHtmlElement(node.parentNode, ["ol", "ul"]);
  5787 		});
  5790 	// "If mode is "disable", then while node list is not empty:"
  5791 	if (mode == "disable") {
  5792 		while (nodeList.length) {
  5793 			// "Let sublist be an empty list of nodes."
  5794 			var sublist = [];
  5796 			// "Remove the first member from node list and append it to
  5797 			// sublist."
  5798 			sublist.push(nodeList.shift());
  5800 			// "If the first member of sublist is an HTML element with local
  5801 			// name tag name, outdent it and continue this loop from the
  5802 			// beginning."
  5803 			if (isHtmlElement(sublist[0], tagName)) {
  5804 				outdentNode(sublist[0]);
  5805 				continue;
  5808 			// "While node list is not empty, and the first member of node list
  5809 			// is the nextSibling of the last member of sublist and is not an
  5810 			// HTML element with local name tag name, remove the first member
  5811 			// from node list and append it to sublist."
  5812 			while (nodeList.length
  5813 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling
  5814 			&& !isHtmlElement(nodeList[0], tagName)) {
  5815 				sublist.push(nodeList.shift());
  5818 			// "Record the values of sublist, and let values be the result."
  5819 			var values = recordValues(sublist);
  5821 			// "Split the parent of sublist."
  5822 			splitParent(sublist);
  5824 			// "Fix disallowed ancestors of each member of sublist."
  5825 			for (var i = 0; i < sublist.length; i++) {
  5826 				fixDisallowedAncestors(sublist[i]);
  5829 			// "Restore the values from values."
  5830 			restoreValues(values);
  5833 	// "Otherwise, while node list is not empty:"
  5834 	} else {
  5835 		while (nodeList.length) {
  5836 			// "Let sublist be an empty list of nodes."
  5837 			var sublist = [];
  5839 			// "While either sublist is empty, or node list is not empty and
  5840 			// its first member is the nextSibling of sublist's last member:"
  5841 			while (!sublist.length
  5842 			|| (nodeList.length
  5843 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
  5844 				// "If node list's first member is a p or div, set the tag name
  5845 				// of node list's first member to "li", and append the result
  5846 				// to sublist. Remove the first member from node list."
  5847 				if (isHtmlElement(nodeList[0], ["p", "div"])) {
  5848 					sublist.push(setTagName(nodeList[0], "li"));
  5849 					nodeList.shift();
  5851 				// "Otherwise, if the first member of node list is an li or ol
  5852 				// or ul, remove it from node list and append it to sublist."
  5853 				} else if (isHtmlElement(nodeList[0], ["li", "ol", "ul"])) {
  5854 					sublist.push(nodeList.shift());
  5856 				// "Otherwise:"
  5857 				} else {
  5858 					// "Let nodes to wrap be a list of nodes, initially empty."
  5859 					var nodesToWrap = [];
  5861 					// "While nodes to wrap is empty, or node list is not empty
  5862 					// and its first member is the nextSibling of nodes to
  5863 					// wrap's last member and the first member of node list is
  5864 					// an inline node and the last member of nodes to wrap is
  5865 					// an inline node other than a br, remove the first member
  5866 					// from node list and append it to nodes to wrap."
  5867 					while (!nodesToWrap.length
  5868 					|| (nodeList.length
  5869 					&& nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling
  5870 					&& isInlineNode(nodeList[0])
  5871 					&& isInlineNode(nodesToWrap[nodesToWrap.length - 1])
  5872 					&& !isHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) {
  5873 						nodesToWrap.push(nodeList.shift());
  5876 					// "Wrap nodes to wrap, with new parent instructions
  5877 					// returning the result of calling createElement("li") on
  5878 					// the context object. Append the result to sublist."
  5879 					sublist.push(wrap(nodesToWrap,
  5880 						undefined,
  5881 						function() { return document.createElement("li") }));
  5885 			// "If sublist's first member's parent is an HTML element with
  5886 			// local name tag name, or if every member of sublist is an ol or
  5887 			// ul, continue this loop from the beginning."
  5888 			if (isHtmlElement(sublist[0].parentNode, tagName)
  5889 			|| sublist.every(function(node) { return isHtmlElement(node, ["ol", "ul"]) })) {
  5890 				continue;
  5893 			// "If sublist's first member's parent is an HTML element with
  5894 			// local name other tag name:"
  5895 			if (isHtmlElement(sublist[0].parentNode, otherTagName)) {
  5896 				// "Record the values of sublist, and let values be the
  5897 				// result."
  5898 				var values = recordValues(sublist);
  5900 				// "Split the parent of sublist."
  5901 				splitParent(sublist);
  5903 				// "Wrap sublist, with sibling criteria returning true for an
  5904 				// HTML element with local name tag name and false otherwise,
  5905 				// and new parent instructions returning the result of calling
  5906 				// createElement(tag name) on the context object."
  5907 				wrap(sublist,
  5908 					function(node) { return isHtmlElement(node, tagName) },
  5909 					function() { return document.createElement(tagName) });
  5911 				// "Restore the values from values."
  5912 				restoreValues(values);
  5914 				// "Continue this loop from the beginning."
  5915 				continue;
  5918 			// "Wrap sublist, with sibling criteria returning true for an HTML
  5919 			// element with local name tag name and false otherwise, and new
  5920 			// parent instructions being the following:"
  5921 			// . . .
  5922 			// "Fix disallowed ancestors of the previous step's result."
  5923 			fixDisallowedAncestors(wrap(sublist,
  5924 				function(node) { return isHtmlElement(node, tagName) },
  5925 				function() {
  5926 					// "If sublist's first member's parent is not an editable
  5927 					// simple indentation element, or sublist's first member's
  5928 					// parent's previousSibling is not an editable HTML element
  5929 					// with local name tag name, call createElement(tag name)
  5930 					// on the context object and return the result."
  5931 					if (!isEditable(sublist[0].parentNode)
  5932 					|| !isSimpleIndentationElement(sublist[0].parentNode)
  5933 					|| !isEditable(sublist[0].parentNode.previousSibling)
  5934 					|| !isHtmlElement(sublist[0].parentNode.previousSibling, tagName)) {
  5935 						return document.createElement(tagName);
  5938 					// "Let list be sublist's first member's parent's
  5939 					// previousSibling."
  5940 					var list = sublist[0].parentNode.previousSibling;
  5942 					// "Normalize sublists of list's lastChild."
  5943 					normalizeSublists(list.lastChild);
  5945 					// "If list's lastChild is not an editable HTML element
  5946 					// with local name tag name, call createElement(tag name)
  5947 					// on the context object, and append the result as the last
  5948 					// child of list."
  5949 					if (!isEditable(list.lastChild)
  5950 					|| !isHtmlElement(list.lastChild, tagName)) {
  5951 						list.appendChild(document.createElement(tagName));
  5954 					// "Return the last child of list."
  5955 					return list.lastChild;
  5957 			));
  5963 //@}
  5964 ///// Justifying the selection /////
  5965 //@{
  5967 function justifySelection(alignment) {
  5968 	// "Block-extend the active range, and let new range be the result."
  5969 	var newRange = blockExtend(globalRange);
  5971 	// "Let element list be a list of all editable Elements contained in new
  5972 	// range that either has an attribute in the HTML namespace whose local
  5973 	// name is "align", or has a style attribute that sets "text-align", or is
  5974 	// a center."
  5975 	var elementList = getAllContainedNodes(newRange, function(node) {
  5976 		return node.nodeType == Node.ELEMENT_NODE
  5977 			&& isEditable(node)
  5978 			// Ignoring namespaces here
  5979 			&& (
  5980 				node.hasAttribute("align")
  5981 				|| node.style.textAlign != ""
  5982 				|| isHtmlElement(node, "center")
  5983 			);
  5984 	});
  5986 	// "For each element in element list:"
  5987 	for (var i = 0; i < elementList.length; i++) {
  5988 		var element = elementList[i];
  5990 		// "If element has an attribute in the HTML namespace whose local name
  5991 		// is "align", remove that attribute."
  5992 		element.removeAttribute("align");
  5994 		// "Unset the CSS property "text-align" on element, if it's set by a
  5995 		// style attribute."
  5996 		element.style.textAlign = "";
  5997 		if (element.getAttribute("style") == "") {
  5998 			element.removeAttribute("style");
  6001 		// "If element is a div or span or center with no attributes, remove
  6002 		// it, preserving its descendants."
  6003 		if (isHtmlElement(element, ["div", "span", "center"])
  6004 		&& !element.attributes.length) {
  6005 			removePreservingDescendants(element);
  6008 		// "If element is a center with one or more attributes, set the tag
  6009 		// name of element to "div"."
  6010 		if (isHtmlElement(element, "center")
  6011 		&& element.attributes.length) {
  6012 			setTagName(element, "div");
  6016 	// "Block-extend the active range, and let new range be the result."
  6017 	newRange = blockExtend(globalRange);
  6019 	// "Let node list be a list of nodes, initially empty."
  6020 	var nodeList = [];
  6022 	// "For each node node contained in new range, append node to node list if
  6023 	// the last member of node list (if any) is not an ancestor of node; node
  6024 	// is editable; node is an allowed child of "div"; and node's alignment
  6025 	// value is not alignment."
  6026 	nodeList = getContainedNodes(newRange, function(node) {
  6027 		return isEditable(node)
  6028 			&& isAllowedChild(node, "div")
  6029 			&& getAlignmentValue(node) != alignment;
  6030 	});
  6032 	// "While node list is not empty:"
  6033 	while (nodeList.length) {
  6034 		// "Let sublist be a list of nodes, initially empty."
  6035 		var sublist = [];
  6037 		// "Remove the first member of node list and append it to sublist."
  6038 		sublist.push(nodeList.shift());
  6040 		// "While node list is not empty, and the first member of node list is
  6041 		// the nextSibling of the last member of sublist, remove the first
  6042 		// member of node list and append it to sublist."
  6043 		while (nodeList.length
  6044 		&& nodeList[0] == sublist[sublist.length - 1].nextSibling) {
  6045 			sublist.push(nodeList.shift());
  6048 		// "Wrap sublist. Sibling criteria returns true for any div that has
  6049 		// one or both of the following two attributes and no other attributes,
  6050 		// and false otherwise:"
  6051 		//
  6052 		//   * "An align attribute whose value is an ASCII case-insensitive
  6053 		//     match for alignment.
  6054 		//   * "A style attribute which sets exactly one CSS property
  6055 		//     (including unrecognized or invalid attributes), which is
  6056 		//     "text-align", which is set to alignment.
  6057 		//
  6058 		// "New parent instructions are to call createElement("div") on the
  6059 		// context object, then set its CSS property "text-align" to alignment
  6060 		// and return the result."
  6061 		wrap(sublist,
  6062 			function(node) {
  6063 				return isHtmlElement(node, "div")
  6064 					&& [].every.call(node.attributes, function(attr) {
  6065 						return (attr.name == "align" && attr.value.toLowerCase() == alignment)
  6066 							|| (attr.name == "style" && node.style.length == 1 && node.style.textAlign == alignment);
  6067 					});
  6068 			},
  6069 			function() {
  6070 				var newParent = document.createElement("div");
  6071 				newParent.setAttribute("style", "text-align: " + alignment);
  6072 				return newParent;
  6074 		);
  6079 //@}
  6080 ///// Automatic linking /////
  6081 //@{
  6082 // "An autolinkable URL is a string of the following form:"
  6083 var autolinkableUrlRegexp =
  6084 	// "Either a string matching the scheme pattern from RFC 3986 section 3.1
  6085 	// followed by the literal string ://, or the literal string mailto:;
  6086 	// followed by"
  6087 	//
  6088 	// From the RFC: scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
  6089 	"([a-zA-Z][a-zA-Z0-9+.-]*://|mailto:)"
  6090 	// "Zero or more characters other than space characters; followed by"
  6091 	+ "[^ \t\n\f\r]*"
  6092 	// "A character that is not one of the ASCII characters !"'(),-.:;<>[]`{}."
  6093 	+ "[^!\"'(),\\-.:;<>[\\]`{}]";
  6095 // "A valid e-mail address is a string that matches the ABNF production 1*(
  6096 // atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined in RFC
  6097 // 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section 3.5."
  6098 //
  6099 // atext: ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" /
  6100 // "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
  6101 //
  6102 //<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
  6103 //<let-dig-hyp> ::= <let-dig> | "-"
  6104 //<let-dig> ::= <letter> | <digit>
  6105 var validEmailRegexp =
  6106 	"[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~.]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*";
  6108 function autolink(node, endOffset) {
  6109 	// "While (node, end offset)'s previous equivalent point is not null, set
  6110 	// it to its previous equivalent point."
  6111 	while (getPreviousEquivalentPoint(node, endOffset)) {
  6112 		var prev = getPreviousEquivalentPoint(node, endOffset);
  6113 		node = prev[0];
  6114 		endOffset = prev[1];
  6117 	// "If node is not a Text node, or has an a ancestor, do nothing and abort
  6118 	// these steps."
  6119 	if (node.nodeType != Node.TEXT_NODE
  6120 	|| getAncestors(node).some(function(ancestor) { return isHtmlElement(ancestor, "a") })) {
  6121 		return;
  6124 	// "Let search be the largest substring of node's data whose end is end
  6125 	// offset and that contains no space characters."
  6126 	var search = /[^ \t\n\f\r]*$/.exec(node.substringData(0, endOffset))[0];
  6128 	// "If some substring of search is an autolinkable URL:"
  6129 	if (new RegExp(autolinkableUrlRegexp).test(search)) {
  6130 		// "While there is no substring of node's data ending at end offset
  6131 		// that is an autolinkable URL, decrement end offset."
  6132 		while (!(new RegExp(autolinkableUrlRegexp + "$").test(node.substringData(0, endOffset)))) {
  6133 			endOffset--;
  6136 		// "Let start offset be the start index of the longest substring of
  6137 		// node's data that is an autolinkable URL ending at end offset."
  6138 		var startOffset = new RegExp(autolinkableUrlRegexp + "$").exec(node.substringData(0, endOffset)).index;
  6140 		// "Let href be the substring of node's data starting at start offset
  6141 		// and ending at end offset."
  6142 		var href = node.substringData(startOffset, endOffset - startOffset);
  6144 	// "Otherwise, if some substring of search is a valid e-mail address:"
  6145 	} else if (new RegExp(validEmailRegexp).test(search)) {
  6146 		// "While there is no substring of node's data ending at end offset
  6147 		// that is a valid e-mail address, decrement end offset."
  6148 		while (!(new RegExp(validEmailRegexp + "$").test(node.substringData(0, endOffset)))) {
  6149 			endOffset--;
  6152 		// "Let start offset be the start index of the longest substring of
  6153 		// node's data that is a valid e-mail address ending at end offset."
  6154 		var startOffset = new RegExp(validEmailRegexp + "$").exec(node.substringData(0, endOffset)).index;
  6156 		// "Let href be "mailto:" concatenated with the substring of node's
  6157 		// data starting at start offset and ending at end offset."
  6158 		var href = "mailto:" + node.substringData(startOffset, endOffset - startOffset);
  6160 	// "Otherwise, do nothing and abort these steps."
  6161 	} else {
  6162 		return;
  6165 	// "Let original range be the active range."
  6166 	var originalRange = getActiveRange();
  6168 	// "Create a new range with start (node, start offset) and end (node, end
  6169 	// offset), and set the context object's selection's range to it."
  6170 	var newRange = document.createRange();
  6171 	newRange.setStart(node, startOffset);
  6172 	newRange.setEnd(node, endOffset);
  6173 	getSelection().removeAllRanges();
  6174 	getSelection().addRange(newRange);
  6175 	globalRange = newRange;
  6177 	// "Take the action for "createLink", with value equal to href."
  6178 	commands.createlink.action(href);
  6180 	// "Set the context object's selection's range to original range."
  6181 	getSelection().removeAllRanges();
  6182 	getSelection().addRange(originalRange);
  6183 	globalRange = originalRange;
  6185 //@}
  6186 ///// The delete command /////
  6187 //@{
  6188 commands["delete"] = {
  6189 	preservesOverrides: true,
  6190 	action: function() {
  6191 		// "If the active range is not collapsed, delete the selection and
  6192 		// return true."
  6193 		if (!getActiveRange().collapsed) {
  6194 			deleteSelection();
  6195 			return true;
  6198 		// "Canonicalize whitespace at the active range's start."
  6199 		canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
  6201 		// "Let node and offset be the active range's start node and offset."
  6202 		var node = getActiveRange().startContainer;
  6203 		var offset = getActiveRange().startOffset;
  6205 		// "Repeat the following steps:"
  6206 		while (true) {
  6207 			// "If offset is zero and node's previousSibling is an editable
  6208 			// invisible node, remove node's previousSibling from its parent."
  6209 			if (offset == 0
  6210 			&& isEditable(node.previousSibling)
  6211 			&& isInvisible(node.previousSibling)) {
  6212 				node.parentNode.removeChild(node.previousSibling);
  6214 			// "Otherwise, if node has a child with index offset − 1 and that
  6215 			// child is an editable invisible node, remove that child from
  6216 			// node, then subtract one from offset."
  6217 			} else if (0 <= offset - 1
  6218 			&& offset - 1 < node.childNodes.length
  6219 			&& isEditable(node.childNodes[offset - 1])
  6220 			&& isInvisible(node.childNodes[offset - 1])) {
  6221 				node.removeChild(node.childNodes[offset - 1]);
  6222 				offset--;
  6224 			// "Otherwise, if offset is zero and node is an inline node, or if
  6225 			// node is an invisible node, set offset to the index of node, then
  6226 			// set node to its parent."
  6227 			} else if ((offset == 0
  6228 			&& isInlineNode(node))
  6229 			|| isInvisible(node)) {
  6230 				offset = getNodeIndex(node);
  6231 				node = node.parentNode;
  6233 			// "Otherwise, if node has a child with index offset − 1 and that
  6234 			// child is an editable a, remove that child from node, preserving
  6235 			// its descendants. Then return true."
  6236 			} else if (0 <= offset - 1
  6237 			&& offset - 1 < node.childNodes.length
  6238 			&& isEditable(node.childNodes[offset - 1])
  6239 			&& isHtmlElement(node.childNodes[offset - 1], "a")) {
  6240 				removePreservingDescendants(node.childNodes[offset - 1]);
  6241 				return true;
  6243 			// "Otherwise, if node has a child with index offset − 1 and that
  6244 			// child is not a block node or a br or an img, set node to that
  6245 			// child, then set offset to the length of node."
  6246 			} else if (0 <= offset - 1
  6247 			&& offset - 1 < node.childNodes.length
  6248 			&& !isBlockNode(node.childNodes[offset - 1])
  6249 			&& !isHtmlElement(node.childNodes[offset - 1], ["br", "img"])) {
  6250 				node = node.childNodes[offset - 1];
  6251 				offset = getNodeLength(node);
  6253 			// "Otherwise, break from this loop."
  6254 			} else {
  6255 				break;
  6259 		// "If node is a Text node and offset is not zero, or if node is a
  6260 		// block node that has a child with index offset − 1 and that child is
  6261 		// a br or hr or img:"
  6262 		if ((node.nodeType == Node.TEXT_NODE
  6263 		&& offset != 0)
  6264 		|| (isBlockNode(node)
  6265 		&& 0 <= offset - 1
  6266 		&& offset - 1 < node.childNodes.length
  6267 		&& isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"]))) {
  6268 			// "Call collapse(node, offset) on the context object's Selection."
  6269 			getSelection().collapse(node, offset);
  6270 			getActiveRange().setEnd(node, offset);
  6272 			// "Call extend(node, offset − 1) on the context object's
  6273 			// Selection."
  6274 			getSelection().extend(node, offset - 1);
  6275 			getActiveRange().setStart(node, offset - 1);
  6277 			// "Delete the selection."
  6278 			deleteSelection();
  6280 			// "Return true."
  6281 			return true;
  6284 		// "If node is an inline node, return true."
  6285 		if (isInlineNode(node)) {
  6286 			return true;
  6289 		// "If node is an li or dt or dd and is the first child of its parent,
  6290 		// and offset is zero:"
  6291 		if (isHtmlElement(node, ["li", "dt", "dd"])
  6292 		&& node == node.parentNode.firstChild
  6293 		&& offset == 0) {
  6294 			// "Let items be a list of all lis that are ancestors of node."
  6295 			//
  6296 			// Remember, must be in tree order.
  6297 			var items = [];
  6298 			for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
  6299 				if (isHtmlElement(ancestor, "li")) {
  6300 					items.unshift(ancestor);
  6304 			// "Normalize sublists of each item in items."
  6305 			for (var i = 0; i < items.length; i++) {
  6306 				normalizeSublists(items[i]);
  6309 			// "Record the values of the one-node list consisting of node, and
  6310 			// let values be the result."
  6311 			var values = recordValues([node]);
  6313 			// "Split the parent of the one-node list consisting of node."
  6314 			splitParent([node]);
  6316 			// "Restore the values from values."
  6317 			restoreValues(values);
  6319 			// "If node is a dd or dt, and it is not an allowed child of any of
  6320 			// its ancestors in the same editing host, set the tag name of node
  6321 			// to the default single-line container name and let node be the
  6322 			// result."
  6323 			if (isHtmlElement(node, ["dd", "dt"])
  6324 			&& getAncestors(node).every(function(ancestor) {
  6325 				return !inSameEditingHost(node, ancestor)
  6326 					|| !isAllowedChild(node, ancestor)
  6327 			})) {
  6328 				node = setTagName(node, defaultSingleLineContainerName);
  6331 			// "Fix disallowed ancestors of node."
  6332 			fixDisallowedAncestors(node);
  6334 			// "Return true."
  6335 			return true;
  6338 		// "Let start node equal node and let start offset equal offset."
  6339 		var startNode = node;
  6340 		var startOffset = offset;
  6342 		// "Repeat the following steps:"
  6343 		while (true) {
  6344 			// "If start offset is zero, set start offset to the index of start
  6345 			// node and then set start node to its parent."
  6346 			if (startOffset == 0) {
  6347 				startOffset = getNodeIndex(startNode);
  6348 				startNode = startNode.parentNode;
  6350 			// "Otherwise, if start node has an editable invisible child with
  6351 			// index start offset minus one, remove it from start node and
  6352 			// subtract one from start offset."
  6353 			} else if (0 <= startOffset - 1
  6354 			&& startOffset - 1 < startNode.childNodes.length
  6355 			&& isEditable(startNode.childNodes[startOffset - 1])
  6356 			&& isInvisible(startNode.childNodes[startOffset - 1])) {
  6357 				startNode.removeChild(startNode.childNodes[startOffset - 1]);
  6358 				startOffset--;
  6360 			// "Otherwise, break from this loop."
  6361 			} else {
  6362 				break;
  6366 		// "If offset is zero, and node has an editable ancestor container in
  6367 		// the same editing host that's an indentation element:"
  6368 		if (offset == 0
  6369 		&& getAncestors(node).concat(node).filter(function(ancestor) {
  6370 			return isEditable(ancestor)
  6371 				&& inSameEditingHost(ancestor, node)
  6372 				&& isIndentationElement(ancestor);
  6373 		}).length) {
  6374 			// "Block-extend the range whose start and end are both (node, 0),
  6375 			// and let new range be the result."
  6376 			var newRange = document.createRange();
  6377 			newRange.setStart(node, 0);
  6378 			newRange = blockExtend(newRange);
  6380 			// "Let node list be a list of nodes, initially empty."
  6381 			//
  6382 			// "For each node current node contained in new range, append
  6383 			// current node to node list if the last member of node list (if
  6384 			// any) is not an ancestor of current node, and current node is
  6385 			// editable but has no editable descendants."
  6386 			var nodeList = getContainedNodes(newRange, function(currentNode) {
  6387 				return isEditable(currentNode)
  6388 					&& !hasEditableDescendants(currentNode);
  6389 			});
  6391 			// "Outdent each node in node list."
  6392 			for (var i = 0; i < nodeList.length; i++) {
  6393 				outdentNode(nodeList[i]);
  6396 			// "Return true."
  6397 			return true;
  6400 		// "If the child of start node with index start offset is a table,
  6401 		// return true."
  6402 		if (isHtmlElement(startNode.childNodes[startOffset], "table")) {
  6403 			return true;
  6406 		// "If start node has a child with index start offset − 1, and that
  6407 		// child is a table:"
  6408 		if (0 <= startOffset - 1
  6409 		&& startOffset - 1 < startNode.childNodes.length
  6410 		&& isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
  6411 			// "Call collapse(start node, start offset − 1) on the context
  6412 			// object's Selection."
  6413 			getSelection().collapse(startNode, startOffset - 1);
  6414 			getActiveRange().setStart(startNode, startOffset - 1);
  6416 			// "Call extend(start node, start offset) on the context object's
  6417 			// Selection."
  6418 			getSelection().extend(startNode, startOffset);
  6419 			getActiveRange().setEnd(startNode, startOffset);
  6421 			// "Return true."
  6422 			return true;
  6425 		// "If offset is zero; and either the child of start node with index
  6426 		// start offset minus one is an hr, or the child is a br whose
  6427 		// previousSibling is either a br or not an inline node:"
  6428 		if (offset == 0
  6429 		&& (isHtmlElement(startNode.childNodes[startOffset - 1], "hr")
  6430 			|| (
  6431 				isHtmlElement(startNode.childNodes[startOffset - 1], "br")
  6432 				&& (
  6433 					isHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
  6434 					|| !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
  6437 		)) {
  6438 			// "Call collapse(start node, start offset − 1) on the context
  6439 			// object's Selection."
  6440 			getSelection().collapse(startNode, startOffset - 1);
  6441 			getActiveRange().setStart(startNode, startOffset - 1);
  6443 			// "Call extend(start node, start offset) on the context object's
  6444 			// Selection."
  6445 			getSelection().extend(startNode, startOffset);
  6446 			getActiveRange().setEnd(startNode, startOffset);
  6448 			// "Delete the selection."
  6449 			deleteSelection();
  6451 			// "Call collapse(node, offset) on the Selection."
  6452 			getSelection().collapse(node, offset);
  6453 			getActiveRange().setStart(node, offset);
  6454 			getActiveRange().collapse(true);
  6456 			// "Return true."
  6457 			return true;
  6460 		// "If the child of start node with index start offset is an li or dt
  6461 		// or dd, and that child's firstChild is an inline node, and start
  6462 		// offset is not zero:"
  6463 		if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
  6464 		&& isInlineNode(startNode.childNodes[startOffset].firstChild)
  6465 		&& startOffset != 0) {
  6466 			// "Let previous item be the child of start node with index start
  6467 			// offset minus one."
  6468 			var previousItem = startNode.childNodes[startOffset - 1];
  6470 			// "If previous item's lastChild is an inline node other than a br,
  6471 			// call createElement("br") on the context object and append the
  6472 			// result as the last child of previous item."
  6473 			if (isInlineNode(previousItem.lastChild)
  6474 			&& !isHtmlElement(previousItem.lastChild, "br")) {
  6475 				previousItem.appendChild(document.createElement("br"));
  6478 			// "If previous item's lastChild is an inline node, call
  6479 			// createElement("br") on the context object and append the result
  6480 			// as the last child of previous item."
  6481 			if (isInlineNode(previousItem.lastChild)) {
  6482 				previousItem.appendChild(document.createElement("br"));
  6486 		// "If start node's child with index start offset is an li or dt or dd,
  6487 		// and that child's previousSibling is also an li or dt or dd:"
  6488 		if (isHtmlElement(startNode.childNodes[startOffset], ["li", "dt", "dd"])
  6489 		&& isHtmlElement(startNode.childNodes[startOffset].previousSibling, ["li", "dt", "dd"])) {
  6490 			// "Call cloneRange() on the active range, and let original range
  6491 			// be the result."
  6492 			//
  6493 			// We need to add it to extraRanges so it will actually get updated
  6494 			// when moving preserving ranges.
  6495 			var originalRange = getActiveRange().cloneRange();
  6496 			extraRanges.push(originalRange);
  6498 			// "Set start node to its child with index start offset − 1."
  6499 			startNode = startNode.childNodes[startOffset - 1];
  6501 			// "Set start offset to start node's length."
  6502 			startOffset = getNodeLength(startNode);
  6504 			// "Set node to start node's nextSibling."
  6505 			node = startNode.nextSibling;
  6507 			// "Call collapse(start node, start offset) on the context object's
  6508 			// Selection."
  6509 			getSelection().collapse(startNode, startOffset);
  6510 			getActiveRange().setStart(startNode, startOffset);
  6512 			// "Call extend(node, 0) on the context object's Selection."
  6513 			getSelection().extend(node, 0);
  6514 			getActiveRange().setEnd(node, 0);
  6516 			// "Delete the selection."
  6517 			deleteSelection();
  6519 			// "Call removeAllRanges() on the context object's Selection."
  6520 			getSelection().removeAllRanges();
  6522 			// "Call addRange(original range) on the context object's
  6523 			// Selection."
  6524 			getSelection().addRange(originalRange);
  6525 			getActiveRange().setStart(originalRange.startContainer, originalRange.startOffset);
  6526 			getActiveRange().setEnd(originalRange.endContainer, originalRange.endOffset);
  6528 			// "Return true."
  6529 			extraRanges.pop();
  6530 			return true;
  6533 		// "While start node has a child with index start offset minus one:"
  6534 		while (0 <= startOffset - 1
  6535 		&& startOffset - 1 < startNode.childNodes.length) {
  6536 			// "If start node's child with index start offset minus one is
  6537 			// editable and invisible, remove it from start node, then subtract
  6538 			// one from start offset."
  6539 			if (isEditable(startNode.childNodes[startOffset - 1])
  6540 			&& isInvisible(startNode.childNodes[startOffset - 1])) {
  6541 				startNode.removeChild(startNode.childNodes[startOffset - 1]);
  6542 				startOffset--;
  6544 			// "Otherwise, set start node to its child with index start offset
  6545 			// minus one, then set start offset to the length of start node."
  6546 			} else {
  6547 				startNode = startNode.childNodes[startOffset - 1];
  6548 				startOffset = getNodeLength(startNode);
  6552 		// "Call collapse(start node, start offset) on the context object's
  6553 		// Selection."
  6554 		getSelection().collapse(startNode, startOffset);
  6555 		getActiveRange().setStart(startNode, startOffset);
  6557 		// "Call extend(node, offset) on the context object's Selection."
  6558 		getSelection().extend(node, offset);
  6559 		getActiveRange().setEnd(node, offset);
  6561 		// "Delete the selection, with direction "backward"."
  6562 		deleteSelection({direction: "backward"});
  6564 		// "Return true."
  6565 		return true;
  6567 };
  6569 //@}
  6570 ///// The formatBlock command /////
  6571 //@{
  6572 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
  6573 // "h4", "h5", "h6", "p", or "pre"."
  6574 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
  6575 	"h4", "h5", "h6", "p", "pre"];
  6577 commands.formatblock = {
  6578 	preservesOverrides: true,
  6579 	action: function(value) {
  6580 		// "If value begins with a "<" character and ends with a ">" character,
  6581 		// remove the first and last characters from it."
  6582 		if (/^<.*>$/.test(value)) {
  6583 			value = value.slice(1, -1);
  6586 		// "Let value be converted to ASCII lowercase."
  6587 		value = value.toLowerCase();
  6589 		// "If value is not a formattable block name, return false."
  6590 		if (formattableBlockNames.indexOf(value) == -1) {
  6591 			return false;
  6594 		// "Block-extend the active range, and let new range be the result."
  6595 		var newRange = blockExtend(getActiveRange());
  6597 		// "Let node list be an empty list of nodes."
  6598 		//
  6599 		// "For each node node contained in new range, append node to node list
  6600 		// if it is editable, the last member of original node list (if any) is
  6601 		// not an ancestor of node, node is either a non-list single-line
  6602 		// container or an allowed child of "p" or a dd or dt, and node is not
  6603 		// the ancestor of a prohibited paragraph child."
  6604 		var nodeList = getContainedNodes(newRange, function(node) {
  6605 			return isEditable(node)
  6606 				&& (isNonListSingleLineContainer(node)
  6607 				|| isAllowedChild(node, "p")
  6608 				|| isHtmlElement(node, ["dd", "dt"]))
  6609 				&& !getDescendants(node).some(isProhibitedParagraphChild);
  6610 		});
  6612 		// "Record the values of node list, and let values be the result."
  6613 		var values = recordValues(nodeList);
  6615 		// "For each node in node list, while node is the descendant of an
  6616 		// editable HTML element in the same editing host, whose local name is
  6617 		// a formattable block name, and which is not the ancestor of a
  6618 		// prohibited paragraph child, split the parent of the one-node list
  6619 		// consisting of node."
  6620 		for (var i = 0; i < nodeList.length; i++) {
  6621 			var node = nodeList[i];
  6622 			while (getAncestors(node).some(function(ancestor) {
  6623 				return isEditable(ancestor)
  6624 					&& inSameEditingHost(ancestor, node)
  6625 					&& isHtmlElement(ancestor, formattableBlockNames)
  6626 					&& !getDescendants(ancestor).some(isProhibitedParagraphChild);
  6627 			})) {
  6628 				splitParent([node]);
  6632 		// "Restore the values from values."
  6633 		restoreValues(values);
  6635 		// "While node list is not empty:"
  6636 		while (nodeList.length) {
  6637 			var sublist;
  6639 			// "If the first member of node list is a single-line
  6640 			// container:"
  6641 			if (isSingleLineContainer(nodeList[0])) {
  6642 				// "Let sublist be the children of the first member of node
  6643 				// list."
  6644 				sublist = [].slice.call(nodeList[0].childNodes);
  6646 				// "Record the values of sublist, and let values be the
  6647 				// result."
  6648 				var values = recordValues(sublist);
  6650 				// "Remove the first member of node list from its parent,
  6651 				// preserving its descendants."
  6652 				removePreservingDescendants(nodeList[0]);
  6654 				// "Restore the values from values."
  6655 				restoreValues(values);
  6657 				// "Remove the first member from node list."
  6658 				nodeList.shift();
  6660 			// "Otherwise:"
  6661 			} else {
  6662 				// "Let sublist be an empty list of nodes."
  6663 				sublist = [];
  6665 				// "Remove the first member of node list and append it to
  6666 				// sublist."
  6667 				sublist.push(nodeList.shift());
  6669 				// "While node list is not empty, and the first member of
  6670 				// node list is the nextSibling of the last member of
  6671 				// sublist, and the first member of node list is not a
  6672 				// single-line container, and the last member of sublist is
  6673 				// not a br, remove the first member of node list and
  6674 				// append it to sublist."
  6675 				while (nodeList.length
  6676 				&& nodeList[0] == sublist[sublist.length - 1].nextSibling
  6677 				&& !isSingleLineContainer(nodeList[0])
  6678 				&& !isHtmlElement(sublist[sublist.length - 1], "BR")) {
  6679 					sublist.push(nodeList.shift());
  6683 			// "Wrap sublist. If value is "div" or "p", sibling criteria
  6684 			// returns false; otherwise it returns true for an HTML element
  6685 			// with local name value and no attributes, and false otherwise.
  6686 			// New parent instructions return the result of running
  6687 			// createElement(value) on the context object. Then fix disallowed
  6688 			// ancestors of the result."
  6689 			fixDisallowedAncestors(wrap(sublist,
  6690 				["div", "p"].indexOf(value) == - 1
  6691 					? function(node) { return isHtmlElement(node, value) && !node.attributes.length }
  6692 					: function() { return false },
  6693 				function() { return document.createElement(value) }));
  6696 		// "Return true."
  6697 		return true;
  6698 	}, indeterm: function() {
  6699 		// "If the active range is null, return false."
  6700 		if (!getActiveRange()) {
  6701 			return false;
  6704 		// "Block-extend the active range, and let new range be the result."
  6705 		var newRange = blockExtend(getActiveRange());
  6707 		// "Let node list be all visible editable nodes that are contained in
  6708 		// new range and have no children."
  6709 		var nodeList = getAllContainedNodes(newRange, function(node) {
  6710 			return isVisible(node)
  6711 				&& isEditable(node)
  6712 				&& !node.hasChildNodes();
  6713 		});
  6715 		// "If node list is empty, return false."
  6716 		if (!nodeList.length) {
  6717 			return false;
  6720 		// "Let type be null."
  6721 		var type = null;
  6723 		// "For each node in node list:"
  6724 		for (var i = 0; i < nodeList.length; i++) {
  6725 			var node = nodeList[i];
  6727 			// "While node's parent is editable and in the same editing host as
  6728 			// node, and node is not an HTML element whose local name is a
  6729 			// formattable block name, set node to its parent."
  6730 			while (isEditable(node.parentNode)
  6731 			&& inSameEditingHost(node, node.parentNode)
  6732 			&& !isHtmlElement(node, formattableBlockNames)) {
  6733 				node = node.parentNode;
  6736 			// "Let current type be the empty string."
  6737 			var currentType = "";
  6739 			// "If node is an editable HTML element whose local name is a
  6740 			// formattable block name, and node is not the ancestor of a
  6741 			// prohibited paragraph child, set current type to node's local
  6742 			// name."
  6743 			if (isEditable(node)
  6744 			&& isHtmlElement(node, formattableBlockNames)
  6745 			&& !getDescendants(node).some(isProhibitedParagraphChild)) {
  6746 				currentType = node.tagName;
  6749 			// "If type is null, set type to current type."
  6750 			if (type === null) {
  6751 				type = currentType;
  6753 			// "Otherwise, if type does not equal current type, return true."
  6754 			} else if (type != currentType) {
  6755 				return true;
  6759 		// "Return false."
  6760 		return false;
  6761 	}, value: function() {
  6762 		// "If the active range is null, return the empty string."
  6763 		if (!getActiveRange()) {
  6764 			return "";
  6767 		// "Block-extend the active range, and let new range be the result."
  6768 		var newRange = blockExtend(getActiveRange());
  6770 		// "Let node be the first visible editable node that is contained in
  6771 		// new range and has no children. If there is no such node, return the
  6772 		// empty string."
  6773 		var nodes = getAllContainedNodes(newRange, function(node) {
  6774 			return isVisible(node)
  6775 				&& isEditable(node)
  6776 				&& !node.hasChildNodes();
  6777 		});
  6778 		if (!nodes.length) {
  6779 			return "";
  6781 		var node = nodes[0];
  6783 		// "While node's parent is editable and in the same editing host as
  6784 		// node, and node is not an HTML element whose local name is a
  6785 		// formattable block name, set node to its parent."
  6786 		while (isEditable(node.parentNode)
  6787 		&& inSameEditingHost(node, node.parentNode)
  6788 		&& !isHtmlElement(node, formattableBlockNames)) {
  6789 			node = node.parentNode;
  6792 		// "If node is an editable HTML element whose local name is a
  6793 		// formattable block name, and node is not the ancestor of a prohibited
  6794 		// paragraph child, return node's local name, converted to ASCII
  6795 		// lowercase."
  6796 		if (isEditable(node)
  6797 		&& isHtmlElement(node, formattableBlockNames)
  6798 		&& !getDescendants(node).some(isProhibitedParagraphChild)) {
  6799 			return node.tagName.toLowerCase();
  6802 		// "Return the empty string."
  6803 		return "";
  6805 };
  6807 //@}
  6808 ///// The forwardDelete command /////
  6809 //@{
  6810 commands.forwarddelete = {
  6811 	preservesOverrides: true,
  6812 	action: function() {
  6813 		// "If the active range is not collapsed, delete the selection and
  6814 		// return true."
  6815 		if (!getActiveRange().collapsed) {
  6816 			deleteSelection();
  6817 			return true;
  6820 		// "Canonicalize whitespace at the active range's start."
  6821 		canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset);
  6823 		// "Let node and offset be the active range's start node and offset."
  6824 		var node = getActiveRange().startContainer;
  6825 		var offset = getActiveRange().startOffset;
  6827 		// "Repeat the following steps:"
  6828 		while (true) {
  6829 			// "If offset is the length of node and node's nextSibling is an
  6830 			// editable invisible node, remove node's nextSibling from its
  6831 			// parent."
  6832 			if (offset == getNodeLength(node)
  6833 			&& isEditable(node.nextSibling)
  6834 			&& isInvisible(node.nextSibling)) {
  6835 				node.parentNode.removeChild(node.nextSibling);
  6837 			// "Otherwise, if node has a child with index offset and that child
  6838 			// is an editable invisible node, remove that child from node."
  6839 			} else if (offset < node.childNodes.length
  6840 			&& isEditable(node.childNodes[offset])
  6841 			&& isInvisible(node.childNodes[offset])) {
  6842 				node.removeChild(node.childNodes[offset]);
  6844 			// "Otherwise, if offset is the length of node and node is an
  6845 			// inline node, or if node is invisible, set offset to one plus the
  6846 			// index of node, then set node to its parent."
  6847 			} else if ((offset == getNodeLength(node)
  6848 			&& isInlineNode(node))
  6849 			|| isInvisible(node)) {
  6850 				offset = 1 + getNodeIndex(node);
  6851 				node = node.parentNode;
  6853 			// "Otherwise, if node has a child with index offset and that child
  6854 			// is neither a block node nor a br nor an img nor a collapsed
  6855 			// block prop, set node to that child, then set offset to zero."
  6856 			} else if (offset < node.childNodes.length
  6857 			&& !isBlockNode(node.childNodes[offset])
  6858 			&& !isHtmlElement(node.childNodes[offset], ["br", "img"])
  6859 			&& !isCollapsedBlockProp(node.childNodes[offset])) {
  6860 				node = node.childNodes[offset];
  6861 				offset = 0;
  6863 			// "Otherwise, break from this loop."
  6864 			} else {
  6865 				break;
  6869 		// "If node is a Text node and offset is not node's length:"
  6870 		if (node.nodeType == Node.TEXT_NODE
  6871 		&& offset != getNodeLength(node)) {
  6872 			// "Let end offset be offset plus one."
  6873 			var endOffset = offset + 1;
  6875 			// "While end offset is not node's length and the end offsetth
  6876 			// element of node's data has general category M when interpreted
  6877 			// as a Unicode code point, add one to end offset."
  6878 			//
  6879 			// TODO: Not even going to try handling anything beyond the most
  6880 			// basic combining marks, since I couldn't find a good list.  I
  6881 			// special-case a few Hebrew diacritics too to test basic coverage
  6882 			// of non-Latin stuff.
  6883 			while (endOffset != node.length
  6884 			&& /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
  6885 				endOffset++;
  6888 			// "Call collapse(node, offset) on the context object's Selection."
  6889 			getSelection().collapse(node, offset);
  6890 			getActiveRange().setStart(node, offset);
  6892 			// "Call extend(node, end offset) on the context object's
  6893 			// Selection."
  6894 			getSelection().extend(node, endOffset);
  6895 			getActiveRange().setEnd(node, endOffset);
  6897 			// "Delete the selection."
  6898 			deleteSelection();
  6900 			// "Return true."
  6901 			return true;
  6904 		// "If node is an inline node, return true."
  6905 		if (isInlineNode(node)) {
  6906 			return true;
  6909 		// "If node has a child with index offset and that child is a br or hr
  6910 		// or img, but is not a collapsed block prop:"
  6911 		if (offset < node.childNodes.length
  6912 		&& isHtmlElement(node.childNodes[offset], ["br", "hr", "img"])
  6913 		&& !isCollapsedBlockProp(node.childNodes[offset])) {
  6914 			// "Call collapse(node, offset) on the context object's Selection."
  6915 			getSelection().collapse(node, offset);
  6916 			getActiveRange().setStart(node, offset);
  6918 			// "Call extend(node, offset + 1) on the context object's
  6919 			// Selection."
  6920 			getSelection().extend(node, offset + 1);
  6921 			getActiveRange().setEnd(node, offset + 1);
  6923 			// "Delete the selection."
  6924 			deleteSelection();
  6926 			// "Return true."
  6927 			return true;
  6930 		// "Let end node equal node and let end offset equal offset."
  6931 		var endNode = node;
  6932 		var endOffset = offset;
  6934 		// "If end node has a child with index end offset, and that child is a
  6935 		// collapsed block prop, add one to end offset."
  6936 		if (endOffset < endNode.childNodes.length
  6937 		&& isCollapsedBlockProp(endNode.childNodes[endOffset])) {
  6938 			endOffset++;
  6941 		// "Repeat the following steps:"
  6942 		while (true) {
  6943 			// "If end offset is the length of end node, set end offset to one
  6944 			// plus the index of end node and then set end node to its parent."
  6945 			if (endOffset == getNodeLength(endNode)) {
  6946 				endOffset = 1 + getNodeIndex(endNode);
  6947 				endNode = endNode.parentNode;
  6949 			// "Otherwise, if end node has a an editable invisible child with
  6950 			// index end offset, remove it from end node."
  6951 			} else if (endOffset < endNode.childNodes.length
  6952 			&& isEditable(endNode.childNodes[endOffset])
  6953 			&& isInvisible(endNode.childNodes[endOffset])) {
  6954 				endNode.removeChild(endNode.childNodes[endOffset]);
  6956 			// "Otherwise, break from this loop."
  6957 			} else {
  6958 				break;
  6962 		// "If the child of end node with index end offset minus one is a
  6963 		// table, return true."
  6964 		if (isHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
  6965 			return true;
  6968 		// "If the child of end node with index end offset is a table:"
  6969 		if (isHtmlElement(endNode.childNodes[endOffset], "table")) {
  6970 			// "Call collapse(end node, end offset) on the context object's
  6971 			// Selection."
  6972 			getSelection().collapse(endNode, endOffset);
  6973 			getActiveRange().setStart(endNode, endOffset);
  6975 			// "Call extend(end node, end offset + 1) on the context object's
  6976 			// Selection."
  6977 			getSelection().extend(endNode, endOffset + 1);
  6978 			getActiveRange().setEnd(endNode, endOffset + 1);
  6980 			// "Return true."
  6981 			return true;
  6984 		// "If offset is the length of node, and the child of end node with
  6985 		// index end offset is an hr or br:"
  6986 		if (offset == getNodeLength(node)
  6987 		&& isHtmlElement(endNode.childNodes[endOffset], ["br", "hr"])) {
  6988 			// "Call collapse(end node, end offset) on the context object's
  6989 			// Selection."
  6990 			getSelection().collapse(endNode, endOffset);
  6991 			getActiveRange().setStart(endNode, endOffset);
  6993 			// "Call extend(end node, end offset + 1) on the context object's
  6994 			// Selection."
  6995 			getSelection().extend(endNode, endOffset + 1);
  6996 			getActiveRange().setEnd(endNode, endOffset + 1);
  6998 			// "Delete the selection."
  6999 			deleteSelection();
  7001 			// "Call collapse(node, offset) on the Selection."
  7002 			getSelection().collapse(node, offset);
  7003 			getActiveRange().setStart(node, offset);
  7004 			getActiveRange().collapse(true);
  7006 			// "Return true."
  7007 			return true;
  7010 		// "While end node has a child with index end offset:"
  7011 		while (endOffset < endNode.childNodes.length) {
  7012 			// "If end node's child with index end offset is editable and
  7013 			// invisible, remove it from end node."
  7014 			if (isEditable(endNode.childNodes[endOffset])
  7015 			&& isInvisible(endNode.childNodes[endOffset])) {
  7016 				endNode.removeChild(endNode.childNodes[endOffset]);
  7018 			// "Otherwise, set end node to its child with index end offset and
  7019 			// set end offset to zero."
  7020 			} else {
  7021 				endNode = endNode.childNodes[endOffset];
  7022 				endOffset = 0;
  7026 		// "Call collapse(node, offset) on the context object's Selection."
  7027 		getSelection().collapse(node, offset);
  7028 		getActiveRange().setStart(node, offset);
  7030 		// "Call extend(end node, end offset) on the context object's
  7031 		// Selection."
  7032 		getSelection().extend(endNode, endOffset);
  7033 		getActiveRange().setEnd(endNode, endOffset);
  7035 		// "Delete the selection."
  7036 		deleteSelection();
  7038 		// "Return true."
  7039 		return true;
  7041 };
  7043 //@}
  7044 ///// The indent command /////
  7045 //@{
  7046 commands.indent = {
  7047 	preservesOverrides: true,
  7048 	action: function() {
  7049 		// "Let items be a list of all lis that are ancestor containers of the
  7050 		// active range's start and/or end node."
  7051 		//
  7052 		// Has to be in tree order, remember!
  7053 		var items = [];
  7054 		for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
  7055 			if (isHtmlElement(node, "LI")) {
  7056 				items.unshift(node);
  7059 		for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
  7060 			if (isHtmlElement(node, "LI")) {
  7061 				items.unshift(node);
  7064 		for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
  7065 			if (isHtmlElement(node, "LI")) {
  7066 				items.unshift(node);
  7070 		// "For each item in items, normalize sublists of item."
  7071 		for (var i = 0; i < items.length; i++) {
  7072 			normalizeSublists(items[i]);
  7075 		// "Block-extend the active range, and let new range be the result."
  7076 		var newRange = blockExtend(getActiveRange());
  7078 		// "Let node list be a list of nodes, initially empty."
  7079 		var nodeList = [];
  7081 		// "For each node node contained in new range, if node is editable and
  7082 		// is an allowed child of "div" or "ol" and if the last member of node
  7083 		// list (if any) is not an ancestor of node, append node to node list."
  7084 		nodeList = getContainedNodes(newRange, function(node) {
  7085 			return isEditable(node)
  7086 				&& (isAllowedChild(node, "div")
  7087 				|| isAllowedChild(node, "ol"));
  7088 		});
  7090 		// "If the first visible member of node list is an li whose parent is
  7091 		// an ol or ul:"
  7092 		if (isHtmlElement(nodeList.filter(isVisible)[0], "li")
  7093 		&& isHtmlElement(nodeList.filter(isVisible)[0].parentNode, ["ol", "ul"])) {
  7094 			// "Let sibling be node list's first visible member's
  7095 			// previousSibling."
  7096 			var sibling = nodeList.filter(isVisible)[0].previousSibling;
  7098 			// "While sibling is invisible, set sibling to its
  7099 			// previousSibling."
  7100 			while (isInvisible(sibling)) {
  7101 				sibling = sibling.previousSibling;
  7104 			// "If sibling is an li, normalize sublists of sibling."
  7105 			if (isHtmlElement(sibling, "li")) {
  7106 				normalizeSublists(sibling);
  7110 		// "While node list is not empty:"
  7111 		while (nodeList.length) {
  7112 			// "Let sublist be a list of nodes, initially empty."
  7113 			var sublist = [];
  7115 			// "Remove the first member of node list and append it to sublist."
  7116 			sublist.push(nodeList.shift());
  7118 			// "While the first member of node list is the nextSibling of the
  7119 			// last member of sublist, remove the first member of node list and
  7120 			// append it to sublist."
  7121 			while (nodeList.length
  7122 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling) {
  7123 				sublist.push(nodeList.shift());
  7126 			// "Indent sublist."
  7127 			indentNodes(sublist);
  7130 		// "Return true."
  7131 		return true;
  7133 };
  7135 //@}
  7136 ///// The insertHorizontalRule command /////
  7137 //@{
  7138 commands.inserthorizontalrule = {
  7139 	preservesOverrides: true,
  7140 	action: function() {
  7141 		// "Let start node, start offset, end node, and end offset be the
  7142 		// active range's start and end nodes and offsets."
  7143 		var startNode = getActiveRange().startContainer;
  7144 		var startOffset = getActiveRange().startOffset;
  7145 		var endNode = getActiveRange().endContainer;
  7146 		var endOffset = getActiveRange().endOffset;
  7148 		// "While start offset is 0 and start node's parent is not null, set
  7149 		// start offset to start node's index, then set start node to its
  7150 		// parent."
  7151 		while (startOffset == 0
  7152 		&& startNode.parentNode) {
  7153 			startOffset = getNodeIndex(startNode);
  7154 			startNode = startNode.parentNode;
  7157 		// "While end offset is end node's length, and end node's parent is not
  7158 		// null, set end offset to one plus end node's index, then set end node
  7159 		// to its parent."
  7160 		while (endOffset == getNodeLength(endNode)
  7161 		&& endNode.parentNode) {
  7162 			endOffset = 1 + getNodeIndex(endNode);
  7163 			endNode = endNode.parentNode;
  7166 		// "Call collapse(start node, start offset) on the context object's
  7167 		// Selection."
  7168 		getSelection().collapse(startNode, startOffset);
  7169 		getActiveRange().setStart(startNode, startOffset);
  7171 		// "Call extend(end node, end offset) on the context object's
  7172 		// Selection."
  7173 		getSelection().extend(endNode, endOffset);
  7174 		getActiveRange().setEnd(endNode, endOffset);
  7176 		// "Delete the selection, with block merging false."
  7177 		deleteSelection({blockMerging: false});
  7179 		// "If the active range's start node is neither editable nor an editing
  7180 		// host, return true."
  7181 		if (!isEditable(getActiveRange().startContainer)
  7182 		&& !isEditingHost(getActiveRange().startContainer)) {
  7183 			return true;
  7186 		// "If the active range's start node is a Text node and its start
  7187 		// offset is zero, call collapse() on the context object's Selection,
  7188 		// with first argument the active range's start node's parent and
  7189 		// second argument the active range's start node's index."
  7190 		if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
  7191 		&& getActiveRange().startOffset == 0) {
  7192 			var newNode = getActiveRange().startContainer.parentNode;
  7193 			var newOffset = getNodeIndex(getActiveRange().startContainer);
  7194 			getSelection().collapse(newNode, newOffset);
  7195 			getActiveRange().setStart(newNode, newOffset);
  7196 			getActiveRange().collapse(true);
  7199 		// "If the active range's start node is a Text node and its start
  7200 		// offset is the length of its start node, call collapse() on the
  7201 		// context object's Selection, with first argument the active range's
  7202 		// start node's parent, and the second argument one plus the active
  7203 		// range's start node's index."
  7204 		if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
  7205 		&& getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
  7206 			var newNode = getActiveRange().startContainer.parentNode;
  7207 			var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
  7208 			getSelection().collapse(newNode, newOffset);
  7209 			getActiveRange().setStart(newNode, newOffset);
  7210 			getActiveRange().collapse(true);
  7213 		// "Let hr be the result of calling createElement("hr") on the
  7214 		// context object."
  7215 		var hr = document.createElement("hr");
  7217 		// "Run insertNode(hr) on the active range."
  7218 		getActiveRange().insertNode(hr);
  7220 		// "Fix disallowed ancestors of hr."
  7221 		fixDisallowedAncestors(hr);
  7223 		// "Run collapse() on the context object's Selection, with first
  7224 		// argument hr's parent and the second argument equal to one plus hr's
  7225 		// index."
  7226 		getSelection().collapse(hr.parentNode, 1 + getNodeIndex(hr));
  7227 		getActiveRange().setStart(hr.parentNode, 1 + getNodeIndex(hr));
  7228 		getActiveRange().collapse(true);
  7230 		// "Return true."
  7231 		return true;
  7233 };
  7235 //@}
  7236 ///// The insertHTML command /////
  7237 //@{
  7238 commands.inserthtml = {
  7239 	preservesOverrides: true,
  7240 	action: function(value) {
  7241 		// "Delete the selection."
  7242 		deleteSelection();
  7244 		// "If the active range's start node is neither editable nor an editing
  7245 		// host, return true."
  7246 		if (!isEditable(getActiveRange().startContainer)
  7247 		&& !isEditingHost(getActiveRange().startContainer)) {
  7248 			return true;
  7251 		// "Let frag be the result of calling createContextualFragment(value)
  7252 		// on the active range."
  7253 		var frag = getActiveRange().createContextualFragment(value);
  7255 		// "Let last child be the lastChild of frag."
  7256 		var lastChild = frag.lastChild;
  7258 		// "If last child is null, return true."
  7259 		if (!lastChild) {
  7260 			return true;
  7263 		// "Let descendants be all descendants of frag."
  7264 		var descendants = getDescendants(frag);
  7266 		// "If the active range's start node is a block node:"
  7267 		if (isBlockNode(getActiveRange().startContainer)) {
  7268 			// "Let collapsed block props be all editable collapsed block prop
  7269 			// children of the active range's start node that have index
  7270 			// greater than or equal to the active range's start offset."
  7271 			//
  7272 			// "For each node in collapsed block props, remove node from its
  7273 			// parent."
  7274 			[].filter.call(getActiveRange().startContainer.childNodes, function(node) {
  7275 				return isEditable(node)
  7276 					&& isCollapsedBlockProp(node)
  7277 					&& getNodeIndex(node) >= getActiveRange().startOffset;
  7278 			}).forEach(function(node) {
  7279 				node.parentNode.removeChild(node);
  7280 			});
  7283 		// "Call insertNode(frag) on the active range."
  7284 		getActiveRange().insertNode(frag);
  7286 		// "If the active range's start node is a block node with no visible
  7287 		// children, call createElement("br") on the context object and append
  7288 		// the result as the last child of the active range's start node."
  7289 		if (isBlockNode(getActiveRange().startContainer)
  7290 		&& ![].some.call(getActiveRange().startContainer.childNodes, isVisible)) {
  7291 			getActiveRange().startContainer.appendChild(document.createElement("br"));
  7294 		// "Call collapse() on the context object's Selection, with last
  7295 		// child's parent as the first argument and one plus its index as the
  7296 		// second."
  7297 		getActiveRange().setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
  7298 		getActiveRange().setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
  7300 		// "Fix disallowed ancestors of each member of descendants."
  7301 		for (var i = 0; i < descendants.length; i++) {
  7302 			fixDisallowedAncestors(descendants[i]);
  7305 		// "Return true."
  7306 		return true;
  7308 };
  7310 //@}
  7311 ///// The insertImage command /////
  7312 //@{
  7313 commands.insertimage = {
  7314 	preservesOverrides: true,
  7315 	action: function(value) {
  7316 		// "If value is the empty string, return false."
  7317 		if (value === "") {
  7318 			return false;
  7321 		// "Delete the selection, with strip wrappers false."
  7322 		deleteSelection({stripWrappers: false});
  7324 		// "Let range be the active range."
  7325 		var range = getActiveRange();
  7327 		// "If the active range's start node is neither editable nor an editing
  7328 		// host, return true."
  7329 		if (!isEditable(getActiveRange().startContainer)
  7330 		&& !isEditingHost(getActiveRange().startContainer)) {
  7331 			return true;
  7334 		// "If range's start node is a block node whose sole child is a br, and
  7335 		// its start offset is 0, remove its start node's child from it."
  7336 		if (isBlockNode(range.startContainer)
  7337 		&& range.startContainer.childNodes.length == 1
  7338 		&& isHtmlElement(range.startContainer.firstChild, "br")
  7339 		&& range.startOffset == 0) {
  7340 			range.startContainer.removeChild(range.startContainer.firstChild);
  7343 		// "Let img be the result of calling createElement("img") on the
  7344 		// context object."
  7345 		var img = document.createElement("img");
  7347 		// "Run setAttribute("src", value) on img."
  7348 		img.setAttribute("src", value);
  7350 		// "Run insertNode(img) on the range."
  7351 		range.insertNode(img);
  7353 		// "Run collapse() on the Selection, with first argument equal to the
  7354 		// parent of img and the second argument equal to one plus the index of
  7355 		// img."
  7356 		//
  7357 		// Not everyone actually supports collapse(), so we do it manually
  7358 		// instead.  Also, we need to modify the actual range we're given as
  7359 		// well, for the sake of autoimplementation.html's range-filling-in.
  7360 		range.setStart(img.parentNode, 1 + getNodeIndex(img));
  7361 		range.setEnd(img.parentNode, 1 + getNodeIndex(img));
  7362 		getSelection().removeAllRanges();
  7363 		getSelection().addRange(range);
  7365 		// IE adds width and height attributes for some reason, so remove those
  7366 		// to actually do what the spec says.
  7367 		img.removeAttribute("width");
  7368 		img.removeAttribute("height");
  7370 		// "Return true."
  7371 		return true;
  7373 };
  7375 //@}
  7376 ///// The insertLineBreak command /////
  7377 //@{
  7378 commands.insertlinebreak = {
  7379 	preservesOverrides: true,
  7380 	action: function(value) {
  7381 		// "Delete the selection, with strip wrappers false."
  7382 		deleteSelection({stripWrappers: false});
  7384 		// "If the active range's start node is neither editable nor an editing
  7385 		// host, return true."
  7386 		if (!isEditable(getActiveRange().startContainer)
  7387 		&& !isEditingHost(getActiveRange().startContainer)) {
  7388 			return true;
  7391 		// "If the active range's start node is an Element, and "br" is not an
  7392 		// allowed child of it, return true."
  7393 		if (getActiveRange().startContainer.nodeType == Node.ELEMENT_NODE
  7394 		&& !isAllowedChild("br", getActiveRange().startContainer)) {
  7395 			return true;
  7398 		// "If the active range's start node is not an Element, and "br" is not
  7399 		// an allowed child of the active range's start node's parent, return
  7400 		// true."
  7401 		if (getActiveRange().startContainer.nodeType != Node.ELEMENT_NODE
  7402 		&& !isAllowedChild("br", getActiveRange().startContainer.parentNode)) {
  7403 			return true;
  7406 		// "If the active range's start node is a Text node and its start
  7407 		// offset is zero, call collapse() on the context object's Selection,
  7408 		// with first argument equal to the active range's start node's parent
  7409 		// and second argument equal to the active range's start node's index."
  7410 		if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
  7411 		&& getActiveRange().startOffset == 0) {
  7412 			var newNode = getActiveRange().startContainer.parentNode;
  7413 			var newOffset = getNodeIndex(getActiveRange().startContainer);
  7414 			getSelection().collapse(newNode, newOffset);
  7415 			getActiveRange().setStart(newNode, newOffset);
  7416 			getActiveRange().setEnd(newNode, newOffset);
  7419 		// "If the active range's start node is a Text node and its start
  7420 		// offset is the length of its start node, call collapse() on the
  7421 		// context object's Selection, with first argument equal to the active
  7422 		// range's start node's parent and second argument equal to one plus
  7423 		// the active range's start node's index."
  7424 		if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE
  7425 		&& getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
  7426 			var newNode = getActiveRange().startContainer.parentNode;
  7427 			var newOffset = 1 + getNodeIndex(getActiveRange().startContainer);
  7428 			getSelection().collapse(newNode, newOffset);
  7429 			getActiveRange().setStart(newNode, newOffset);
  7430 			getActiveRange().setEnd(newNode, newOffset);
  7433 		// "Let br be the result of calling createElement("br") on the context
  7434 		// object."
  7435 		var br = document.createElement("br");
  7437 		// "Call insertNode(br) on the active range."
  7438 		getActiveRange().insertNode(br);
  7440 		// "Call collapse() on the context object's Selection, with br's parent
  7441 		// as the first argument and one plus br's index as the second
  7442 		// argument."
  7443 		getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
  7444 		getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
  7445 		getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
  7447 		// "If br is a collapsed line break, call createElement("br") on the
  7448 		// context object and let extra br be the result, then call
  7449 		// insertNode(extra br) on the active range."
  7450 		if (isCollapsedLineBreak(br)) {
  7451 			getActiveRange().insertNode(document.createElement("br"));
  7453 			// Compensate for nonstandard implementations of insertNode
  7454 			getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
  7455 			getActiveRange().setStart(br.parentNode, 1 + getNodeIndex(br));
  7456 			getActiveRange().setEnd(br.parentNode, 1 + getNodeIndex(br));
  7459 		// "Return true."
  7460 		return true;
  7462 };
  7464 //@}
  7465 ///// The insertOrderedList command /////
  7466 //@{
  7467 commands.insertorderedlist = {
  7468 	preservesOverrides: true,
  7469 	// "Toggle lists with tag name "ol", then return true."
  7470 	action: function() { toggleLists("ol"); return true },
  7471 	// "True if the selection's list state is "mixed" or "mixed ol", false
  7472 	// otherwise."
  7473 	indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
  7474 	// "True if the selection's list state is "ol", false otherwise."
  7475 	state: function() { return getSelectionListState() == "ol" },
  7476 };
  7478 //@}
  7479 ///// The insertParagraph command /////
  7480 //@{
  7481 commands.insertparagraph = {
  7482 	preservesOverrides: true,
  7483 	action: function() {
  7484 		// "Delete the selection."
  7485 		deleteSelection();
  7487 		// "If the active range's start node is neither editable nor an editing
  7488 		// host, return true."
  7489 		if (!isEditable(getActiveRange().startContainer)
  7490 		&& !isEditingHost(getActiveRange().startContainer)) {
  7491 			return true;
  7494 		// "Let node and offset be the active range's start node and offset."
  7495 		var node = getActiveRange().startContainer;
  7496 		var offset = getActiveRange().startOffset;
  7498 		// "If node is a Text node, and offset is neither 0 nor the length of
  7499 		// node, call splitText(offset) on node."
  7500 		if (node.nodeType == Node.TEXT_NODE
  7501 		&& offset != 0
  7502 		&& offset != getNodeLength(node)) {
  7503 			node.splitText(offset);
  7506 		// "If node is a Text node and offset is its length, set offset to one
  7507 		// plus the index of node, then set node to its parent."
  7508 		if (node.nodeType == Node.TEXT_NODE
  7509 		&& offset == getNodeLength(node)) {
  7510 			offset = 1 + getNodeIndex(node);
  7511 			node = node.parentNode;
  7514 		// "If node is a Text or Comment node, set offset to the index of node,
  7515 		// then set node to its parent."
  7516 		if (node.nodeType == Node.TEXT_NODE
  7517 		|| node.nodeType == Node.COMMENT_NODE) {
  7518 			offset = getNodeIndex(node);
  7519 			node = node.parentNode;
  7522 		// "Call collapse(node, offset) on the context object's Selection."
  7523 		getSelection().collapse(node, offset);
  7524 		getActiveRange().setStart(node, offset);
  7525 		getActiveRange().setEnd(node, offset);
  7527 		// "Let container equal node."
  7528 		var container = node;
  7530 		// "While container is not a single-line container, and container's
  7531 		// parent is editable and in the same editing host as node, set
  7532 		// container to its parent."
  7533 		while (!isSingleLineContainer(container)
  7534 		&& isEditable(container.parentNode)
  7535 		&& inSameEditingHost(node, container.parentNode)) {
  7536 			container = container.parentNode;
  7539 		// "If container is an editable single-line container in the same
  7540 		// editing host as node, and its local name is "p" or "div":"
  7541 		if (isEditable(container)
  7542 		&& isSingleLineContainer(container)
  7543 		&& inSameEditingHost(node, container.parentNode)
  7544 		&& (container.tagName == "P" || container.tagName == "DIV")) {
  7545 			// "Let outer container equal container."
  7546 			var outerContainer = container;
  7548 			// "While outer container is not a dd or dt or li, and outer
  7549 			// container's parent is editable, set outer container to its
  7550 			// parent."
  7551 			while (!isHtmlElement(outerContainer, ["dd", "dt", "li"])
  7552 			&& isEditable(outerContainer.parentNode)) {
  7553 				outerContainer = outerContainer.parentNode;
  7556 			// "If outer container is a dd or dt or li, set container to outer
  7557 			// container."
  7558 			if (isHtmlElement(outerContainer, ["dd", "dt", "li"])) {
  7559 				container = outerContainer;
  7563 		// "If container is not editable or not in the same editing host as
  7564 		// node or is not a single-line container:"
  7565 		if (!isEditable(container)
  7566 		|| !inSameEditingHost(container, node)
  7567 		|| !isSingleLineContainer(container)) {
  7568 			// "Let tag be the default single-line container name."
  7569 			var tag = defaultSingleLineContainerName;
  7571 			// "Block-extend the active range, and let new range be the
  7572 			// result."
  7573 			var newRange = blockExtend(getActiveRange());
  7575 			// "Let node list be a list of nodes, initially empty."
  7576 			//
  7577 			// "Append to node list the first node in tree order that is
  7578 			// contained in new range and is an allowed child of "p", if any."
  7579 			var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
  7580 				.slice(0, 1);
  7582 			// "If node list is empty:"
  7583 			if (!nodeList.length) {
  7584 				// "If tag is not an allowed child of the active range's start
  7585 				// node, return true."
  7586 				if (!isAllowedChild(tag, getActiveRange().startContainer)) {
  7587 					return true;
  7590 				// "Set container to the result of calling createElement(tag)
  7591 				// on the context object."
  7592 				container = document.createElement(tag);
  7594 				// "Call insertNode(container) on the active range."
  7595 				getActiveRange().insertNode(container);
  7597 				// "Call createElement("br") on the context object, and append
  7598 				// the result as the last child of container."
  7599 				container.appendChild(document.createElement("br"));
  7601 				// "Call collapse(container, 0) on the context object's
  7602 				// Selection."
  7603 				getSelection().collapse(container, 0);
  7604 				getActiveRange().setStart(container, 0);
  7605 				getActiveRange().setEnd(container, 0);
  7607 				// "Return true."
  7608 				return true;
  7611 			// "While the nextSibling of the last member of node list is not
  7612 			// null and is an allowed child of "p", append it to node list."
  7613 			while (nodeList[nodeList.length - 1].nextSibling
  7614 			&& isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
  7615 				nodeList.push(nodeList[nodeList.length - 1].nextSibling);
  7618 			// "Wrap node list, with sibling criteria returning false and new
  7619 			// parent instructions returning the result of calling
  7620 			// createElement(tag) on the context object. Set container to the
  7621 			// result."
  7622 			container = wrap(nodeList,
  7623 				function() { return false },
  7624 				function() { return document.createElement(tag) }
  7625 			);
  7628 		// "If container's local name is "address", "listing", or "pre":"
  7629 		if (container.tagName == "ADDRESS"
  7630 		|| container.tagName == "LISTING"
  7631 		|| container.tagName == "PRE") {
  7632 			// "Let br be the result of calling createElement("br") on the
  7633 			// context object."
  7634 			var br = document.createElement("br");
  7636 			// "Call insertNode(br) on the active range."
  7637 			getActiveRange().insertNode(br);
  7639 			// "Call collapse(node, offset + 1) on the context object's
  7640 			// Selection."
  7641 			getSelection().collapse(node, offset + 1);
  7642 			getActiveRange().setStart(node, offset + 1);
  7643 			getActiveRange().setEnd(node, offset + 1);
  7645 			// "If br is the last descendant of container, let br be the result
  7646 			// of calling createElement("br") on the context object, then call
  7647 			// insertNode(br) on the active range."
  7648 			//
  7649 			// Work around browser bugs: some browsers select the
  7650 			// newly-inserted node, not per spec.
  7651 			if (!isDescendant(nextNode(br), container)) {
  7652 				getActiveRange().insertNode(document.createElement("br"));
  7653 				getSelection().collapse(node, offset + 1);
  7654 				getActiveRange().setEnd(node, offset + 1);
  7657 			// "Return true."
  7658 			return true;
  7661 		// "If container's local name is "li", "dt", or "dd"; and either it has
  7662 		// no children or it has a single child and that child is a br:"
  7663 		if (["LI", "DT", "DD"].indexOf(container.tagName) != -1
  7664 		&& (!container.hasChildNodes()
  7665 		|| (container.childNodes.length == 1
  7666 		&& isHtmlElement(container.firstChild, "br")))) {
  7667 			// "Split the parent of the one-node list consisting of container."
  7668 			splitParent([container]);
  7670 			// "If container has no children, call createElement("br") on the
  7671 			// context object and append the result as the last child of
  7672 			// container."
  7673 			if (!container.hasChildNodes()) {
  7674 				container.appendChild(document.createElement("br"));
  7677 			// "If container is a dd or dt, and it is not an allowed child of
  7678 			// any of its ancestors in the same editing host, set the tag name
  7679 			// of container to the default single-line container name and let
  7680 			// container be the result."
  7681 			if (isHtmlElement(container, ["dd", "dt"])
  7682 			&& getAncestors(container).every(function(ancestor) {
  7683 				return !inSameEditingHost(container, ancestor)
  7684 					|| !isAllowedChild(container, ancestor)
  7685 			})) {
  7686 				container = setTagName(container, defaultSingleLineContainerName);
  7689 			// "Fix disallowed ancestors of container."
  7690 			fixDisallowedAncestors(container);
  7692 			// "Return true."
  7693 			return true;
  7696 		// "Let new line range be a new range whose start is the same as
  7697 		// the active range's, and whose end is (container, length of
  7698 		// container)."
  7699 		var newLineRange = document.createRange();
  7700 		newLineRange.setStart(getActiveRange().startContainer, getActiveRange().startOffset);
  7701 		newLineRange.setEnd(container, getNodeLength(container));
  7703 		// "While new line range's start offset is zero and its start node is
  7704 		// not a prohibited paragraph child, set its start to (parent of start
  7705 		// node, index of start node)."
  7706 		while (newLineRange.startOffset == 0
  7707 		&& !isProhibitedParagraphChild(newLineRange.startContainer)) {
  7708 			newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
  7711 		// "While new line range's start offset is the length of its start node
  7712 		// and its start node is not a prohibited paragraph child, set its
  7713 		// start to (parent of start node, 1 + index of start node)."
  7714 		while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
  7715 		&& !isProhibitedParagraphChild(newLineRange.startContainer)) {
  7716 			newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
  7719 		// "Let end of line be true if new line range contains either nothing
  7720 		// or a single br, and false otherwise."
  7721 		var containedInNewLineRange = getContainedNodes(newLineRange);
  7722 		var endOfLine = !containedInNewLineRange.length
  7723 			|| (containedInNewLineRange.length == 1
  7724 			&& isHtmlElement(containedInNewLineRange[0], "br"));
  7726 		// "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
  7727 		// "h6", and end of line is true, let new container name be the default
  7728 		// single-line container name."
  7729 		var newContainerName;
  7730 		if (/^H[1-6]$/.test(container.tagName)
  7731 		&& endOfLine) {
  7732 			newContainerName = defaultSingleLineContainerName;
  7734 		// "Otherwise, if the local name of container is "dt" and end of line
  7735 		// is true, let new container name be "dd"."
  7736 		} else if (container.tagName == "DT"
  7737 		&& endOfLine) {
  7738 			newContainerName = "dd";
  7740 		// "Otherwise, if the local name of container is "dd" and end of line
  7741 		// is true, let new container name be "dt"."
  7742 		} else if (container.tagName == "DD"
  7743 		&& endOfLine) {
  7744 			newContainerName = "dt";
  7746 		// "Otherwise, let new container name be the local name of container."
  7747 		} else {
  7748 			newContainerName = container.tagName.toLowerCase();
  7751 		// "Let new container be the result of calling createElement(new
  7752 		// container name) on the context object."
  7753 		var newContainer = document.createElement(newContainerName);
  7755 		// "Copy all attributes of container to new container."
  7756 		for (var i = 0; i < container.attributes.length; i++) {
  7757 			newContainer.setAttributeNS(container.attributes[i].namespaceURI, container.attributes[i].name, container.attributes[i].value);
  7760 		// "If new container has an id attribute, unset it."
  7761 		newContainer.removeAttribute("id");
  7763 		// "Insert new container into the parent of container immediately after
  7764 		// container."
  7765 		container.parentNode.insertBefore(newContainer, container.nextSibling);
  7767 		// "Let contained nodes be all nodes contained in new line range."
  7768 		var containedNodes = getAllContainedNodes(newLineRange);
  7770 		// "Let frag be the result of calling extractContents() on new line
  7771 		// range."
  7772 		var frag = newLineRange.extractContents();
  7774 		// "Unset the id attribute (if any) of each Element descendant of frag
  7775 		// that is not in contained nodes."
  7776 		var descendants = getDescendants(frag);
  7777 		for (var i = 0; i < descendants.length; i++) {
  7778 			if (descendants[i].nodeType == Node.ELEMENT_NODE
  7779 			&& containedNodes.indexOf(descendants[i]) == -1) {
  7780 				descendants[i].removeAttribute("id");
  7784 		// "Call appendChild(frag) on new container."
  7785 		newContainer.appendChild(frag);
  7787 		// "While container's lastChild is a prohibited paragraph child, set
  7788 		// container to its lastChild."
  7789 		while (isProhibitedParagraphChild(container.lastChild)) {
  7790 			container = container.lastChild;
  7793 		// "While new container's lastChild is a prohibited paragraph child,
  7794 		// set new container to its lastChild."
  7795 		while (isProhibitedParagraphChild(newContainer.lastChild)) {
  7796 			newContainer = newContainer.lastChild;
  7799 		// "If container has no visible children, call createElement("br") on
  7800 		// the context object, and append the result as the last child of
  7801 		// container."
  7802 		if (![].some.call(container.childNodes, isVisible)) {
  7803 			container.appendChild(document.createElement("br"));
  7806 		// "If new container has no visible children, call createElement("br")
  7807 		// on the context object, and append the result as the last child of
  7808 		// new container."
  7809 		if (![].some.call(newContainer.childNodes, isVisible)) {
  7810 			newContainer.appendChild(document.createElement("br"));
  7813 		// "Call collapse(new container, 0) on the context object's Selection."
  7814 		getSelection().collapse(newContainer, 0);
  7815 		getActiveRange().setStart(newContainer, 0);
  7816 		getActiveRange().setEnd(newContainer, 0);
  7818 		// "Return true."
  7819 		return true;
  7821 };
  7823 //@}
  7824 ///// The insertText command /////
  7825 //@{
  7826 commands.inserttext = {
  7827 	action: function(value) {
  7828 		// "Delete the selection, with strip wrappers false."
  7829 		deleteSelection({stripWrappers: false});
  7831 		// "If the active range's start node is neither editable nor an editing
  7832 		// host, return true."
  7833 		if (!isEditable(getActiveRange().startContainer)
  7834 		&& !isEditingHost(getActiveRange().startContainer)) {
  7835 			return true;
  7838 		// "If value's length is greater than one:"
  7839 		if (value.length > 1) {
  7840 			// "For each element el in value, take the action for the
  7841 			// insertText command, with value equal to el."
  7842 			for (var i = 0; i < value.length; i++) {
  7843 				commands.inserttext.action(value[i]);
  7846 			// "Return true."
  7847 			return true;
  7850 		// "If value is the empty string, return true."
  7851 		if (value == "") {
  7852 			return true;
  7855 		// "If value is a newline (U+00A0), take the action for the
  7856 		// insertParagraph command and return true."
  7857 		if (value == "\n") {
  7858 			commands.insertparagraph.action();
  7859 			return true;
  7862 		// "Let node and offset be the active range's start node and offset."
  7863 		var node = getActiveRange().startContainer;
  7864 		var offset = getActiveRange().startOffset;
  7866 		// "If node has a child whose index is offset − 1, and that child is a
  7867 		// Text node, set node to that child, then set offset to node's
  7868 		// length."
  7869 		if (0 <= offset - 1
  7870 		&& offset - 1 < node.childNodes.length
  7871 		&& node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) {
  7872 			node = node.childNodes[offset - 1];
  7873 			offset = getNodeLength(node);
  7876 		// "If node has a child whose index is offset, and that child is a Text
  7877 		// node, set node to that child, then set offset to zero."
  7878 		if (0 <= offset
  7879 		&& offset < node.childNodes.length
  7880 		&& node.childNodes[offset].nodeType == Node.TEXT_NODE) {
  7881 			node = node.childNodes[offset];
  7882 			offset = 0;
  7885 		// "Record current overrides, and let overrides be the result."
  7886 		var overrides = recordCurrentOverrides();
  7888 		// "Call collapse(node, offset) on the context object's Selection."
  7889 		getSelection().collapse(node, offset);
  7890 		getActiveRange().setStart(node, offset);
  7891 		getActiveRange().setEnd(node, offset);
  7893 		// "Canonicalize whitespace at (node, offset)."
  7894 		canonicalizeWhitespace(node, offset);
  7896 		// "Let (node, offset) be the active range's start."
  7897 		node = getActiveRange().startContainer;
  7898 		offset = getActiveRange().startOffset;
  7900 		// "If node is a Text node:"
  7901 		if (node.nodeType == Node.TEXT_NODE) {
  7902 			// "Call insertData(offset, value) on node."
  7903 			node.insertData(offset, value);
  7905 			// "Call collapse(node, offset) on the context object's Selection."
  7906 			getSelection().collapse(node, offset);
  7907 			getActiveRange().setStart(node, offset);
  7909 			// "Call extend(node, offset + 1) on the context object's
  7910 			// Selection."
  7911 			//
  7912 			// Work around WebKit bug: the extend() can throw if the text we're
  7913 			// adding is trailing whitespace.
  7914 			try { getSelection().extend(node, offset + 1); } catch(e) {}
  7915 			getActiveRange().setEnd(node, offset + 1);
  7917 		// "Otherwise:"
  7918 		} else {
  7919 			// "If node has only one child, which is a collapsed line break,
  7920 			// remove its child from it."
  7921 			//
  7922 			// FIXME: IE incorrectly returns false here instead of true
  7923 			// sometimes?
  7924 			if (node.childNodes.length == 1
  7925 			&& isCollapsedLineBreak(node.firstChild)) {
  7926 				node.removeChild(node.firstChild);
  7929 			// "Let text be the result of calling createTextNode(value) on the
  7930 			// context object."
  7931 			var text = document.createTextNode(value);
  7933 			// "Call insertNode(text) on the active range."
  7934 			getActiveRange().insertNode(text);
  7936 			// "Call collapse(text, 0) on the context object's Selection."
  7937 			getSelection().collapse(text, 0);
  7938 			getActiveRange().setStart(text, 0);
  7940 			// "Call extend(text, 1) on the context object's Selection."
  7941 			getSelection().extend(text, 1);
  7942 			getActiveRange().setEnd(text, 1);
  7945 		// "Restore states and values from overrides."
  7946 		restoreStatesAndValues(overrides);
  7948 		// "Canonicalize whitespace at the active range's start, with fix
  7949 		// collapsed space false."
  7950 		canonicalizeWhitespace(getActiveRange().startContainer, getActiveRange().startOffset, false);
  7952 		// "Canonicalize whitespace at the active range's end, with fix
  7953 		// collapsed space false."
  7954 		canonicalizeWhitespace(getActiveRange().endContainer, getActiveRange().endOffset, false);
  7956 		// "If value is a space character, autolink the active range's start."
  7957 		if (/^[ \t\n\f\r]$/.test(value)) {
  7958 			autolink(getActiveRange().startContainer, getActiveRange().startOffset);
  7961 		// "Call collapseToEnd() on the context object's Selection."
  7962 		//
  7963 		// Work around WebKit bug: sometimes it blows up the selection and
  7964 		// throws, which we don't want.
  7965 		try { getSelection().collapseToEnd(); } catch(e) {}
  7966 		getActiveRange().collapse(false);
  7968 		// "Return true."
  7969 		return true;
  7971 };
  7973 //@}
  7974 ///// The insertUnorderedList command /////
  7975 //@{
  7976 commands.insertunorderedlist = {
  7977 	preservesOverrides: true,
  7978 	// "Toggle lists with tag name "ul", then return true."
  7979 	action: function() { toggleLists("ul"); return true },
  7980 	// "True if the selection's list state is "mixed" or "mixed ul", false
  7981 	// otherwise."
  7982 	indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
  7983 	// "True if the selection's list state is "ul", false otherwise."
  7984 	state: function() { return getSelectionListState() == "ul" },
  7985 };
  7987 //@}
  7988 ///// The justifyCenter command /////
  7989 //@{
  7990 commands.justifycenter = {
  7991 	preservesOverrides: true,
  7992 	// "Justify the selection with alignment "center", then return true."
  7993 	action: function() { justifySelection("center"); return true },
  7994 	indeterm: function() {
  7995 		// "Return false if the active range is null.  Otherwise, block-extend
  7996 		// the active range. Return true if among visible editable nodes that
  7997 		// are contained in the result and have no children, at least one has
  7998 		// alignment value "center" and at least one does not. Otherwise return
  7999 		// false."
  8000 		if (!getActiveRange()) {
  8001 			return false;
  8003 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8004 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8005 		});
  8006 		return nodes.some(function(node) { return getAlignmentValue(node) == "center" })
  8007 			&& nodes.some(function(node) { return getAlignmentValue(node) != "center" });
  8008 	}, state: function() {
  8009 		// "Return false if the active range is null.  Otherwise, block-extend
  8010 		// the active range. Return true if there is at least one visible
  8011 		// editable node that is contained in the result and has no children,
  8012 		// and all such nodes have alignment value "center".  Otherwise return
  8013 		// false."
  8014 		if (!getActiveRange()) {
  8015 			return false;
  8017 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8018 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8019 		});
  8020 		return nodes.length
  8021 			&& nodes.every(function(node) { return getAlignmentValue(node) == "center" });
  8022 	}, value: function() {
  8023 		// "Return the empty string if the active range is null.  Otherwise,
  8024 		// block-extend the active range, and return the alignment value of the
  8025 		// first visible editable node that is contained in the result and has
  8026 		// no children. If there is no such node, return "left"."
  8027 		if (!getActiveRange()) {
  8028 			return "";
  8030 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8031 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8032 		});
  8033 		if (nodes.length) {
  8034 			return getAlignmentValue(nodes[0]);
  8035 		} else {
  8036 			return "left";
  8038 	},
  8039 };
  8041 //@}
  8042 ///// The justifyFull command /////
  8043 //@{
  8044 commands.justifyfull = {
  8045 	preservesOverrides: true,
  8046 	// "Justify the selection with alignment "justify", then return true."
  8047 	action: function() { justifySelection("justify"); return true },
  8048 	indeterm: function() {
  8049 		// "Return false if the active range is null.  Otherwise, block-extend
  8050 		// the active range. Return true if among visible editable nodes that
  8051 		// are contained in the result and have no children, at least one has
  8052 		// alignment value "justify" and at least one does not. Otherwise
  8053 		// return false."
  8054 		if (!getActiveRange()) {
  8055 			return false;
  8057 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8058 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8059 		});
  8060 		return nodes.some(function(node) { return getAlignmentValue(node) == "justify" })
  8061 			&& nodes.some(function(node) { return getAlignmentValue(node) != "justify" });
  8062 	}, state: function() {
  8063 		// "Return false if the active range is null.  Otherwise, block-extend
  8064 		// the active range. Return true if there is at least one visible
  8065 		// editable node that is contained in the result and has no children,
  8066 		// and all such nodes have alignment value "justify".  Otherwise return
  8067 		// false."
  8068 		if (!getActiveRange()) {
  8069 			return false;
  8071 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8072 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8073 		});
  8074 		return nodes.length
  8075 			&& nodes.every(function(node) { return getAlignmentValue(node) == "justify" });
  8076 	}, value: function() {
  8077 		// "Return the empty string if the active range is null.  Otherwise,
  8078 		// block-extend the active range, and return the alignment value of the
  8079 		// first visible editable node that is contained in the result and has
  8080 		// no children. If there is no such node, return "left"."
  8081 		if (!getActiveRange()) {
  8082 			return "";
  8084 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8085 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8086 		});
  8087 		if (nodes.length) {
  8088 			return getAlignmentValue(nodes[0]);
  8089 		} else {
  8090 			return "left";
  8092 	},
  8093 };
  8095 //@}
  8096 ///// The justifyLeft command /////
  8097 //@{
  8098 commands.justifyleft = {
  8099 	preservesOverrides: true,
  8100 	// "Justify the selection with alignment "left", then return true."
  8101 	action: function() { justifySelection("left"); return true },
  8102 	indeterm: function() {
  8103 		// "Return false if the active range is null.  Otherwise, block-extend
  8104 		// the active range. Return true if among visible editable nodes that
  8105 		// are contained in the result and have no children, at least one has
  8106 		// alignment value "left" and at least one does not. Otherwise return
  8107 		// false."
  8108 		if (!getActiveRange()) {
  8109 			return false;
  8111 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8112 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8113 		});
  8114 		return nodes.some(function(node) { return getAlignmentValue(node) == "left" })
  8115 			&& nodes.some(function(node) { return getAlignmentValue(node) != "left" });
  8116 	}, state: function() {
  8117 		// "Return false if the active range is null.  Otherwise, block-extend
  8118 		// the active range. Return true if there is at least one visible
  8119 		// editable node that is contained in the result and has no children,
  8120 		// and all such nodes have alignment value "left".  Otherwise return
  8121 		// false."
  8122 		if (!getActiveRange()) {
  8123 			return false;
  8125 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8126 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8127 		});
  8128 		return nodes.length
  8129 			&& nodes.every(function(node) { return getAlignmentValue(node) == "left" });
  8130 	}, value: function() {
  8131 		// "Return the empty string if the active range is null.  Otherwise,
  8132 		// block-extend the active range, and return the alignment value of the
  8133 		// first visible editable node that is contained in the result and has
  8134 		// no children. If there is no such node, return "left"."
  8135 		if (!getActiveRange()) {
  8136 			return "";
  8138 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8139 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8140 		});
  8141 		if (nodes.length) {
  8142 			return getAlignmentValue(nodes[0]);
  8143 		} else {
  8144 			return "left";
  8146 	},
  8147 };
  8149 //@}
  8150 ///// The justifyRight command /////
  8151 //@{
  8152 commands.justifyright = {
  8153 	preservesOverrides: true,
  8154 	// "Justify the selection with alignment "right", then return true."
  8155 	action: function() { justifySelection("right"); return true },
  8156 	indeterm: function() {
  8157 		// "Return false if the active range is null.  Otherwise, block-extend
  8158 		// the active range. Return true if among visible editable nodes that
  8159 		// are contained in the result and have no children, at least one has
  8160 		// alignment value "right" and at least one does not. Otherwise return
  8161 		// false."
  8162 		if (!getActiveRange()) {
  8163 			return false;
  8165 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8166 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8167 		});
  8168 		return nodes.some(function(node) { return getAlignmentValue(node) == "right" })
  8169 			&& nodes.some(function(node) { return getAlignmentValue(node) != "right" });
  8170 	}, state: function() {
  8171 		// "Return false if the active range is null.  Otherwise, block-extend
  8172 		// the active range. Return true if there is at least one visible
  8173 		// editable node that is contained in the result and has no children,
  8174 		// and all such nodes have alignment value "right".  Otherwise return
  8175 		// false."
  8176 		if (!getActiveRange()) {
  8177 			return false;
  8179 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8180 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8181 		});
  8182 		return nodes.length
  8183 			&& nodes.every(function(node) { return getAlignmentValue(node) == "right" });
  8184 	}, value: function() {
  8185 		// "Return the empty string if the active range is null.  Otherwise,
  8186 		// block-extend the active range, and return the alignment value of the
  8187 		// first visible editable node that is contained in the result and has
  8188 		// no children. If there is no such node, return "left"."
  8189 		if (!getActiveRange()) {
  8190 			return "";
  8192 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
  8193 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
  8194 		});
  8195 		if (nodes.length) {
  8196 			return getAlignmentValue(nodes[0]);
  8197 		} else {
  8198 			return "left";
  8200 	},
  8201 };
  8203 //@}
  8204 ///// The outdent command /////
  8205 //@{
  8206 commands.outdent = {
  8207 	preservesOverrides: true,
  8208 	action: function() {
  8209 		// "Let items be a list of all lis that are ancestor containers of the
  8210 		// range's start and/or end node."
  8211 		//
  8212 		// It's annoying to get this in tree order using functional stuff
  8213 		// without doing getDescendants(document), which is slow, so I do it
  8214 		// imperatively.
  8215 		var items = [];
  8216 		(function(){
  8217 			for (
  8218 				var ancestorContainer = getActiveRange().endContainer;
  8219 				ancestorContainer != getActiveRange().commonAncestorContainer;
  8220 				ancestorContainer = ancestorContainer.parentNode
  8221 			) {
  8222 				if (isHtmlElement(ancestorContainer, "li")) {
  8223 					items.unshift(ancestorContainer);
  8226 			for (
  8227 				var ancestorContainer = getActiveRange().startContainer;
  8228 				ancestorContainer;
  8229 				ancestorContainer = ancestorContainer.parentNode
  8230 			) {
  8231 				if (isHtmlElement(ancestorContainer, "li")) {
  8232 					items.unshift(ancestorContainer);
  8235 		})();
  8237 		// "For each item in items, normalize sublists of item."
  8238 		items.forEach(normalizeSublists);
  8240 		// "Block-extend the active range, and let new range be the result."
  8241 		var newRange = blockExtend(getActiveRange());
  8243 		// "Let node list be a list of nodes, initially empty."
  8244 		//
  8245 		// "For each node node contained in new range, append node to node list
  8246 		// if the last member of node list (if any) is not an ancestor of node;
  8247 		// node is editable; and either node has no editable descendants, or is
  8248 		// an ol or ul, or is an li whose parent is an ol or ul."
  8249 		var nodeList = getContainedNodes(newRange, function(node) {
  8250 			return isEditable(node)
  8251 				&& (!getDescendants(node).some(isEditable)
  8252 				|| isHtmlElement(node, ["ol", "ul"])
  8253 				|| (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"])));
  8254 		});
  8256 		// "While node list is not empty:"
  8257 		while (nodeList.length) {
  8258 			// "While the first member of node list is an ol or ul or is not
  8259 			// the child of an ol or ul, outdent it and remove it from node
  8260 			// list."
  8261 			while (nodeList.length
  8262 			&& (isHtmlElement(nodeList[0], ["OL", "UL"])
  8263 			|| !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
  8264 				outdentNode(nodeList.shift());
  8267 			// "If node list is empty, break from these substeps."
  8268 			if (!nodeList.length) {
  8269 				break;
  8272 			// "Let sublist be a list of nodes, initially empty."
  8273 			var sublist = [];
  8275 			// "Remove the first member of node list and append it to sublist."
  8276 			sublist.push(nodeList.shift());
  8278 			// "While the first member of node list is the nextSibling of the
  8279 			// last member of sublist, and the first member of node list is not
  8280 			// an ol or ul, remove the first member of node list and append it
  8281 			// to sublist."
  8282 			while (nodeList.length
  8283 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling
  8284 			&& !isHtmlElement(nodeList[0], ["OL", "UL"])) {
  8285 				sublist.push(nodeList.shift());
  8288 			// "Record the values of sublist, and let values be the result."
  8289 			var values = recordValues(sublist);
  8291 			// "Split the parent of sublist, with new parent null."
  8292 			splitParent(sublist);
  8294 			// "Fix disallowed ancestors of each member of sublist."
  8295 			sublist.forEach(fixDisallowedAncestors);
  8297 			// "Restore the values from values."
  8298 			restoreValues(values);
  8301 		// "Return true."
  8302 		return true;
  8304 };
  8306 //@}
  8308 //////////////////////////////////
  8309 ///// Miscellaneous commands /////
  8310 //////////////////////////////////
  8312 ///// The defaultParagraphSeparator command /////
  8313 //@{
  8314 commands.defaultparagraphseparator = {
  8315 	action: function(value) {
  8316 		// "Let value be converted to ASCII lowercase. If value is then equal
  8317 		// to "p" or "div", set the context object's default single-line
  8318 		// container name to value and return true. Otherwise, return false."
  8319 		value = value.toLowerCase();
  8320 		if (value == "p" || value == "div") {
  8321 			defaultSingleLineContainerName = value;
  8322 			return true;
  8324 		return false;
  8325 	}, value: function() {
  8326 		// "Return the context object's default single-line container name."
  8327 		return defaultSingleLineContainerName;
  8328 	},
  8329 };
  8331 //@}
  8332 ///// The selectAll command /////
  8333 //@{
  8334 commands.selectall = {
  8335 	// Note, this ignores the whole globalRange/getActiveRange() thing and
  8336 	// works with actual selections.  Not suitable for autoimplementation.html.
  8337 	action: function() {
  8338 		// "Let target be the body element of the context object."
  8339 		var target = document.body;
  8341 		// "If target is null, let target be the context object's
  8342 		// documentElement."
  8343 		if (!target) {
  8344 			target = document.documentElement;
  8347 		// "If target is null, call getSelection() on the context object, and
  8348 		// call removeAllRanges() on the result."
  8349 		if (!target) {
  8350 			getSelection().removeAllRanges();
  8352 		// "Otherwise, call getSelection() on the context object, and call
  8353 		// selectAllChildren(target) on the result."
  8354 		} else {
  8355 			getSelection().selectAllChildren(target);
  8358 		// "Return true."
  8359 		return true;
  8361 };
  8363 //@}
  8364 ///// The styleWithCSS command /////
  8365 //@{
  8366 commands.stylewithcss = {
  8367 	action: function(value) {
  8368 		// "If value is an ASCII case-insensitive match for the string
  8369 		// "false", set the CSS styling flag to false. Otherwise, set the
  8370 		// CSS styling flag to true.  Either way, return true."
  8371 		cssStylingFlag = String(value).toLowerCase() != "false";
  8372 		return true;
  8373 	}, state: function() { return cssStylingFlag }
  8374 };
  8376 //@}
  8377 ///// The useCSS command /////
  8378 //@{
  8379 commands.usecss = {
  8380 	action: function(value) {
  8381 		// "If value is an ASCII case-insensitive match for the string "false",
  8382 		// set the CSS styling flag to true. Otherwise, set the CSS styling
  8383 		// flag to false.  Either way, return true."
  8384 		cssStylingFlag = String(value).toLowerCase() == "false";
  8385 		return true;
  8387 };
  8388 //@}
  8390 // Some final setup
  8391 //@{
  8392 (function() {
  8393 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
  8394 // temporary, which means I need an extra closure to not leak the temporaries
  8395 // into the global namespace.  >:(
  8396 var commandNames = [];
  8397 for (var command in commands) {
  8398 	commandNames.push(command);
  8400 commandNames.forEach(function(command) {
  8401 	// "If a command does not have a relevant CSS property specified, it
  8402 	// defaults to null."
  8403 	if (!("relevantCssProperty" in commands[command])) {
  8404 		commands[command].relevantCssProperty = null;
  8407 	// "If a command has inline command activated values defined but nothing
  8408 	// else defines when it is indeterminate, it is indeterminate if among
  8409 	// formattable nodes effectively contained in the active range, there is at
  8410 	// least one whose effective command value is one of the given values and
  8411 	// at least one whose effective command value is not one of the given
  8412 	// values."
  8413 	if ("inlineCommandActivatedValues" in commands[command]
  8414 	&& !("indeterm" in commands[command])) {
  8415 		commands[command].indeterm = function() {
  8416 			if (!getActiveRange()) {
  8417 				return false;
  8420 			var values = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)
  8421 				.map(function(node) { return getEffectiveCommandValue(node, command) });
  8423 			var matchingValues = values.filter(function(value) {
  8424 				return commands[command].inlineCommandActivatedValues.indexOf(value) != -1;
  8425 			});
  8427 			return matchingValues.length >= 1
  8428 				&& values.length - matchingValues.length >= 1;
  8429 		};
  8432 	// "If a command has inline command activated values defined, its state is
  8433 	// true if either no formattable node is effectively contained in the
  8434 	// active range, and the active range's start node's effective command
  8435 	// value is one of the given values; or if there is at least one
  8436 	// formattable node effectively contained in the active range, and all of
  8437 	// them have an effective command value equal to one of the given values."
  8438 	if ("inlineCommandActivatedValues" in commands[command]) {
  8439 		commands[command].state = function() {
  8440 			if (!getActiveRange()) {
  8441 				return false;
  8444 			var nodes = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode);
  8446 			if (nodes.length == 0) {
  8447 				return commands[command].inlineCommandActivatedValues
  8448 					.indexOf(getEffectiveCommandValue(getActiveRange().startContainer, command)) != -1;
  8449 			} else {
  8450 				return nodes.every(function(node) {
  8451 					return commands[command].inlineCommandActivatedValues
  8452 						.indexOf(getEffectiveCommandValue(node, command)) != -1;
  8453 				});
  8455 		};
  8458 	// "If a command is a standard inline value command, it is indeterminate if
  8459 	// among formattable nodes that are effectively contained in the active
  8460 	// range, there are two that have distinct effective command values. Its
  8461 	// value is the effective command value of the first formattable node that
  8462 	// is effectively contained in the active range; or if there is no such
  8463 	// node, the effective command value of the active range's start node; or
  8464 	// if that is null, the empty string."
  8465 	if ("standardInlineValueCommand" in commands[command]) {
  8466 		commands[command].indeterm = function() {
  8467 			if (!getActiveRange()) {
  8468 				return false;
  8471 			var values = getAllEffectivelyContainedNodes(getActiveRange())
  8472 				.filter(isFormattableNode)
  8473 				.map(function(node) { return getEffectiveCommandValue(node, command) });
  8474 			for (var i = 1; i < values.length; i++) {
  8475 				if (values[i] != values[i - 1]) {
  8476 					return true;
  8479 			return false;
  8480 		};
  8482 		commands[command].value = function() {
  8483 			if (!getActiveRange()) {
  8484 				return "";
  8487 			var refNode = getAllEffectivelyContainedNodes(getActiveRange(), isFormattableNode)[0];
  8489 			if (typeof refNode == "undefined") {
  8490 				refNode = getActiveRange().startContainer;
  8493 			var ret = getEffectiveCommandValue(refNode, command);
  8494 			if (ret === null) {
  8495 				return "";
  8497 			return ret;
  8498 		};
  8501 	// "If a command preserves overrides, then before taking its action, the
  8502 	// user agent must record current overrides. After taking the action, if
  8503 	// the active range is collapsed, it must restore states and values from
  8504 	// the recorded list."
  8505 	if ("preservesOverrides" in commands[command]) {
  8506 		var oldAction = commands[command].action;
  8508 		commands[command].action = function(value) {
  8509 			var overrides = recordCurrentOverrides();
  8510 			var ret = oldAction(value);
  8511 			if (getActiveRange().collapsed) {
  8512 				restoreStatesAndValues(overrides);
  8514 			return ret;
  8515 		};
  8517 });
  8518 })();
  8519 //@}
  8521 // vim: foldmarker=@{,@} foldmethod=marker

mercurial