1 define(
  2 //['aloha/ecma5'],
  3 ['aloha/ecma5shims', 'jquery'],
  4 function($_, jQuery) {
  5 	"use strict";
  6 
  7 function hasAttribute(obj, attr){
  8 	var native_method = obj.hasAttribute;  
  9 	if(native_method){
 10 		return obj.hasAttribute(attr); 
 11 	}
 12 	else {
 13 		return (typeof obj.attributes[attr] != "undefined")
 14 	}         
 15 }
 16 
 17 var htmlNamespace = "http://www.w3.org/1999/xhtml";
 18 
 19 var cssStylingFlag = false;
 20 
 21 // This is bad :(
 22 var globalRange = null;
 23 
 24 // Commands are stored in a dictionary where we call their actions and such
 25 var commands = {};
 26 
 27 ///////////////////////////////////////////////////////////////////////////////
 28 ////////////////////////////// Utility functions //////////////////////////////
 29 ///////////////////////////////////////////////////////////////////////////////
 30 //@{
 31 
 32 /**
 33  * Method to count the number of styles in the given style
 34  */
 35 function getStyleLength(node) {
 36 	if (!node) {
 37 		return 0;
 38 	} else if (!node.style) {
 39 		return 0;
 40 	}
 41 
 42 	// some browsers support .length on styles
 43 	if (typeof node.style.length !== 'undefined') {
 44 		return node.style.length;
 45 	} else {
 46 		// others don't, so we will count
 47 		var styleLength = 0;
 48 		for (var s in node.style) {
 49 			if (node.style[s] && node.style[s] !== 0 && node.style[s] !== 'false') {
 50 				styleLength++;
 51 			}
 52 		}
 53 
 54 		return styleLength;
 55 	}
 56 }
 57 
 58 function toArray(obj) {
 59 	if (!obj) {
 60 		return null;
 61 	}
 62 	var array = [], i, l = obj.length;
 63 	// iterate backwards ensuring that length is an UInt32
 64 	for (i = l >>> 0; i--;) {
 65 		array[i] = obj[i];
 66 	}
 67 	return array;
 68 }
 69 
 70 function nextNode(node) {
 71 	if (node.hasChildNodes()) {
 72 		return node.firstChild;
 73 	}
 74 	return nextNodeDescendants(node);
 75 }
 76 
 77 function previousNode(node) {
 78 	if (node.previousSibling) {
 79 		node = node.previousSibling;
 80 		while (node.hasChildNodes()) {
 81 			node = node.lastChild;
 82 		}
 83 		return node;
 84 	}
 85 	if (node.parentNode
 86 	&& node.parentNode.nodeType == $_.Node.ELEMENT_NODE) {
 87 		return node.parentNode;
 88 	}
 89 	return null;
 90 }
 91 
 92 function nextNodeDescendants(node) {
 93 	while (node && !node.nextSibling) {
 94 		node = node.parentNode;
 95 	}
 96 	if (!node) {
 97 		return null;
 98 	}
 99 	return node.nextSibling;
100 }
101 
102 /**
103  * Returns true if ancestor is an ancestor of descendant, false otherwise.
104  */
105 function isAncestor(ancestor, descendant) {
106 	return ancestor
107 		&& descendant
108 		&& Boolean($_.compareDocumentPosition(ancestor, descendant) & $_.Node.DOCUMENT_POSITION_CONTAINED_BY);
109 }
110 
111 /**
112  * Returns true if ancestor is an ancestor of or equal to descendant, false
113  * otherwise.
114  */
115 function isAncestorContainer(ancestor, descendant) {
116 	return (ancestor || descendant)
117 		&& (ancestor == descendant || isAncestor(ancestor, descendant));
118 }
119 
120 /**
121  * Returns true if descendant is a descendant of ancestor, false otherwise.
122  */
123 function isDescendant(descendant, ancestor) {
124 	return ancestor
125 		&& descendant
126 		&& Boolean($_.compareDocumentPosition(ancestor, descendant) & $_.Node.DOCUMENT_POSITION_CONTAINED_BY);
127 }
128 
129 /**
130  * Returns true if node1 is before node2 in tree order, false otherwise.
131  */
132 function isBefore(node1, node2) {
133 	return Boolean($_.compareDocumentPosition(node1,node2) & $_.Node.DOCUMENT_POSITION_FOLLOWING);
134 }
135 
136 /**
137  * Returns true if node1 is after node2 in tree order, false otherwise.
138  */
139 function isAfter(node1, node2) {
140 	return Boolean($_.compareDocumentPosition(node1,node2) & $_.Node.DOCUMENT_POSITION_PRECEDING);
141 }
142 
143 function getAncestors(node) {
144 	var ancestors = [];
145 	while (node.parentNode) {
146 		ancestors.unshift(node.parentNode);
147 		node = node.parentNode;
148 	}
149 	return ancestors;
150 }
151 
152 function getDescendants(node) {
153 	var descendants = [];
154 	var stop = nextNodeDescendants(node);
155 	while ((node = nextNode(node))
156 	&& node != stop) {
157 		descendants.push(node);
158 	}
159 	return descendants;
160 }
161 
162 function convertProperty(property) {
163 	// Special-case for now
164 	var map = {
165 		"fontFamily": "font-family",
166 		"fontSize": "font-size",
167 		"fontStyle": "font-style",
168 		"fontWeight": "font-weight",
169 		"textDecoration": "text-decoration"
170 	};
171 	if (typeof map[property] != "undefined") {
172 		return map[property];
173 	}
174 
175 	return property;
176 }
177 
178 // Return the <font size=X> value for the given CSS size, or undefined if there
179 // is none.
180 function cssSizeToLegacy(cssVal) {
181 	return {
182 		"xx-small": 1,
183 		"small": 2,
184 		"medium": 3,
185 		"large": 4,
186 		"x-large": 5,
187 		"xx-large": 6,
188 		"xxx-large": 7
189 	}[cssVal];
190 }
191 
192 // Return the CSS size given a legacy size.
193 function legacySizeToCss(legacyVal) {
194 	return {
195 		1: "xx-small",
196 		2: "small",
197 		3: "medium",
198 		4: "large",
199 		5: "x-large",
200 		6: "xx-large",
201 		7: "xxx-large"
202 	}[legacyVal];
203 }
204 
205 // Opera 11 puts HTML elements in the null namespace, it seems.
206 function isHtmlNamespace(ns) {
207 	return ns === null
208 		|| !ns
209 		|| ns === htmlNamespace;
210 }
211 
212 // "the directionality" from HTML.  I don't bother caring about non-HTML
213 // elements.
214 //
215 // "The directionality of an element is either 'ltr' or 'rtl', and is
216 // determined as per the first appropriate set of steps from the following
217 // list:"
218 function getDirectionality(element) {
219 	// "If the element's dir attribute is in the ltr state
220 	//     The directionality of the element is 'ltr'."
221 	if (element.dir == "ltr") {
222 		return "ltr";
223 	}
224 
225 	// "If the element's dir attribute is in the rtl state
226 	//     The directionality of the element is 'rtl'."
227 	if (element.dir == "rtl") {
228 		return "rtl";
229 	}
230 
231 	// "If the element's dir attribute is in the auto state
232 	// "If the element is a bdi element and the dir attribute is not in a
233 	// defined state (i.e. it is not present or has an invalid value)
234 	//     [lots of complicated stuff]
235 	//
236 	// Skip this, since no browser implements it anyway.
237 
238 	// "If the element is a root element and the dir attribute is not in a
239 	// defined state (i.e. it is not present or has an invalid value)
240 	//     The directionality of the element is 'ltr'."
241 	if (!isAnyHtmlElement(element.parentNode)) {
242 		return "ltr";
243 	}
244 
245 	// "If the element has a parent element and the dir attribute is not in a
246 	// defined state (i.e. it is not present or has an invalid value)
247 	//     The directionality of the element is the same as the element's
248 	//     parent element's directionality."
249 	return getDirectionality(element.parentNode);
250 }
251 
252 //@}
253 
254 ///////////////////////////////////////////////////////////////////////////////
255 ///////////////////////////// DOM Range functions /////////////////////////////
256 ///////////////////////////////////////////////////////////////////////////////
257 //@{
258 
259 function getNodeIndex(node) {
260 	var ret = 0;
261 	while (node.previousSibling) {
262 		ret++;
263 		node = node.previousSibling;
264 	}
265 	return ret;
266 }
267 
268 // "The length of a Node node is the following, depending on node:
269 //
270 // ProcessingInstruction
271 // DocumentType
272 //   Always 0.
273 // Text
274 // Comment
275 //   node's length.
276 // Any other node
277 //   node's childNodes's length."
278 function getNodeLength(node) {
279 	switch (node.nodeType) {
280 		case $_.Node.PROCESSING_INSTRUCTION_NODE:
281 		case $_.Node.DOCUMENT_TYPE_NODE:
282 			return 0;
283 
284 		case $_.Node.TEXT_NODE:
285 		case $_.Node.COMMENT_NODE:
286 			return node.length;
287 
288 		default:
289 			return node.childNodes.length;
290 	}
291 }
292 
293 /**
294  * The position of two boundary points relative to one another, as defined by
295  * DOM Range.
296  */
297 function getPosition(nodeA, offsetA, nodeB, offsetB) {
298 	// "If node A is the same as node B, return equal if offset A equals offset
299 	// B, before if offset A is less than offset B, and after if offset A is
300 	// greater than offset B."
301 	if (nodeA == nodeB) {
302 		if (offsetA == offsetB) {
303 			return "equal";
304 		}
305 		if (offsetA < offsetB) {
306 			return "before";
307 		}
308 		if (offsetA > offsetB) {
309 			return "after";
310 		}
311 	}
312 
313 	var documentPosition = $_.compareDocumentPosition(nodeB, nodeA);
314 	// "If node A is after node B in tree order, compute the position of (node
315 	// B, offset B) relative to (node A, offset A). If it is before, return
316 	// after. If it is after, return before."
317 	if (documentPosition & $_.Node.DOCUMENT_POSITION_FOLLOWING) {
318 		var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
319 		if (pos == "before") {
320 			return "after";
321 		}
322 		if (pos == "after") {
323 			return "before";
324 		}
325 	}
326 
327 	// "If node A is an ancestor of node B:"
328 	if (documentPosition & $_.Node.DOCUMENT_POSITION_CONTAINS) {
329 		// "Let child equal node B."
330 		var child = nodeB;
331 
332 		// "While child is not a child of node A, set child to its parent."
333 		while (child.parentNode != nodeA) {
334 			child = child.parentNode;
335 		}
336 
337 		// "If the index of child is less than offset A, return after."
338 		if (getNodeIndex(child) < offsetA) {
339 			return "after";
340 		}
341 	}
342 
343 	// "Return before."
344 	return "before";
345 }
346 
347 /**
348  * Returns the furthest ancestor of a Node as defined by DOM Range.
349  */
350 function getFurthestAncestor(node) {
351 352 	var root = node;
353 	while (root.parentNode != null) {
354 		root = root.parentNode;
355 	}
356 	return root;
357 }
358 
359 /**
360  * "contained" as defined by DOM Range: "A Node node is contained in a range
361  * range if node's furthest ancestor is the same as range's root, and (node, 0)
362  * is after range's start, and (node, length of node) is before range's end."
363  */
364 function isContained(node, range) {
365 	var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
366 	if (pos1 !== "after") {
367 		return false;
368 	}
369 	var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
370 	if (pos2 !== "before") {
371 		return false;
372 	}
373 	return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer);
374 }
375 
376 /**
377  * Return all nodes contained in range that the provided function returns true
378  * for, omitting any with an ancestor already being returned.
379  */
380 function getContainedNodes(range, condition) {
381 	if (typeof condition == "undefined") {
382 		condition = function() { return true };
383 	}
384 	var node = range.startContainer;
385 	if (node.hasChildNodes()
386 	&& range.startOffset < node.childNodes.length) {
387 		// A child is contained
388 		node = node.childNodes[range.startOffset];
389 	} else if (range.startOffset == getNodeLength(node)) {
390 		// No descendant can be contained
391 		node = nextNodeDescendants(node);
392 	} else {
393 		// No children; this node at least can't be contained
394 		node = nextNode(node);
395 	}
396 
397 	var stop = range.endContainer;
398 	if (stop.hasChildNodes()
399 	&& range.endOffset < stop.childNodes.length) {
400 		// The node after the last contained node is a child
401 		stop = stop.childNodes[range.endOffset];
402 	} else {
403 		// This node and/or some of its children might be contained
404 		stop = nextNodeDescendants(stop);
405 	}
406 
407 	var nodeList = [];
408 	while (isBefore(node, stop)) {
409 		if (isContained(node, range)
410 		&& condition(node)) {
411 			nodeList.push(node);
412 			node = nextNodeDescendants(node);
413 			continue;
414 		}
415 		node = nextNode(node);
416 	}
417 	return nodeList;
418 }
419 
420 /**
421  * As above, but includes nodes with an ancestor that's already been returned.
422  */
423 function getAllContainedNodes(range, condition) {
424 	if (typeof condition == "undefined") {
425 		condition = function() { return true };
426 	}
427 	var node = range.startContainer;
428 	if (node.hasChildNodes()
429 	&& range.startOffset < node.childNodes.length) {
430 		// A child is contained
431 		node = node.childNodes[range.startOffset];
432 	} else if (range.startOffset == getNodeLength(node)) {
433 		// No descendant can be contained
434 		node = nextNodeDescendants(node);
435 	} else {
436 		// No children; this node at least can't be contained
437 		node = nextNode(node);
438 	}
439 
440 	var stop = range.endContainer;
441 	if (stop.hasChildNodes()
442 	&& range.endOffset < stop.childNodes.length) {
443 		// The node after the last contained node is a child
444 		stop = stop.childNodes[range.endOffset];
445 	} else {
446 		// This node and/or some of its children might be contained
447 		stop = nextNodeDescendants(stop);
448 	}
449 
450 	var nodeList = [];
451 	while (isBefore(node, stop)) {
452 		if (isContained(node, range)
453 		&& condition(node)) {
454 			nodeList.push(node);
455 		}
456 		node = nextNode(node);
457 	}
458 	return nodeList;
459 }
460 
461 // Returns either null, or something of the form rgb(x, y, z), or something of
462 // the form rgb(x, y, z, w) with w != 0.
463 function normalizeColor(color) {
464 	if (color.toLowerCase() == "currentcolor") {
465 		return null;
466 	}
467 
468 	var outerSpan = document.createElement("span");
469 	document.body.appendChild(outerSpan);
470 	outerSpan.style.color = "black";
471 
472 	var innerSpan = document.createElement("span");
473 	outerSpan.appendChild(innerSpan);
474 	innerSpan.style.color = color;
475 	color = $_.getComputedStyle(innerSpan).color;
476 
477 	if (color == "rgb(0, 0, 0)") {
478 		// Maybe it's really black, maybe it's invalid.
479 		outerSpan.color = "white";
480 		color = $_.getComputedStyle(innerSpan).color;
481 		if (color != "rgb(0, 0, 0)") {
482 			return null;
483 		}
484 	}
485 
486 	document.body.removeChild(outerSpan);
487 
488 	// I rely on the fact that browsers generally provide consistent syntax for
489 	// getComputedStyle(), although it's not standardized.  There are only two
490 	// exceptions I found:
491 	if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
492 		// IE10PP2 seems to do this sometimes.
493 		return color.replace("rgba", "rgb").replace(", 1)", ")");
494 	}
495 	if (color == "transparent") {
496 		// IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
497 		// the specified value is "transparent".
498 		return "rgba(0, 0, 0, 0)";
499 	}
500 	return color;
501 }
502 
503 // Returns either null, or something of the form #xxxxxx, or the color itself
504 // if it's a valid keyword.
505 function parseSimpleColor(color) {
506 	color = color.toLowerCase();
507 	if ($_(["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
508 	"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
509 510 	"burlywood", "cadetblue", "chartreuse", "chocolate", "coral",
511 	"cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan",
512 	"darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki",
513 	"darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
514 	"darksalmon", "darkseagreen", "darkslateblue", "darkslategray",
515 	"darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue",
516 	"dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite",
517 	"forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod",
518 	"gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred",
519 	"indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen",
520 	"lemonchiffon", "lightblue", "lightcoral", "lightcyan",
521 	"lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey",
522 	"lightpink", "lightsalmon", "lightseagreen", "lightskyblue",
523 	"lightslategray", "lightslategrey", "lightsteelblue", "lightyellow",
524 	"lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine",
525 	"mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen",
526 	"mediumslateblue", "mediumspringgreen", "mediumturquoise",
527 	"mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
528 	"navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange",
529 	"orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise",
530 	"palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum",
531 	"powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown",
532 	"salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver",
533 	"skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen",
534 	"steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
535 	"wheat", "white", "whitesmoke", "yellow", "yellowgreen"]).indexOf(color) != -1) {
536 		return color;
537 	}
538 
539 	color = normalizeColor(color);
540 	var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
541 	if (matches) {
542 		return "#"
543 			+ parseInt(matches[1]).toString(16).replace(/^.$/, "0$&")
544 			+ parseInt(matches[2]).toString(16).replace(/^.$/, "0$&")
545 			+ parseInt(matches[3]).toString(16).replace(/^.$/, "0$&");
546 	}
547 	return null;
548 }
549 
550 //@}
551 
552 //////////////////////////////////////////////////////////////////////////////
553 /////////////////////////// Edit command functions ///////////////////////////
554 //////////////////////////////////////////////////////////////////////////////
555 
556 /////////////////////////////////////////////////
557 ///// Methods of the HTMLDocument interface /////
558 /////////////////////////////////////////////////
559 //@{
560 
561 var executionStackDepth = 0;
562 
563 // Helper function for common behavior.
564 function editCommandMethod(command, prop, range, callback) {
565 	// Set up our global range magic, but only if we're the outermost function
566 	if (executionStackDepth == 0 && typeof range != "undefined") {
567 		globalRange = range;
568 	} else if (executionStackDepth == 0) {
569 		globalRange = null;
570 		globalRange = range;
571 	}
572 
573 	// "If command is not supported, raise a NOT_SUPPORTED_ERR exception."
574 	//
575 	// We can't throw a real one, but a string will do for our purposes.
576 	if (!(command in commands)) {
577 		throw "NOT_SUPPORTED_ERR";
578 	}
579 
580 	// "If command has no action, raise an INVALID_ACCESS_ERR exception."
581 	// "If command has no indeterminacy, raise an INVALID_ACCESS_ERR
582 	// exception."
583 	// "If command has no state, raise an INVALID_ACCESS_ERR exception."
584 	// "If command has no value, raise an INVALID_ACCESS_ERR exception."
585 	if (prop != "enabled"
586 	&& !(prop in commands[command])) {
587 		throw "INVALID_ACCESS_ERR";
588 	}
589 
590 	executionStackDepth++;
591 	try {
592 		var ret = callback();
593 	} catch(e) {
594 		executionStackDepth--;
595 		throw e;
596 	}
597 	executionStackDepth--;
598 	return ret;
599 }
600 
601 function myExecCommand(command, showUi, value, range) {
602 	// "All of these methods must treat their command argument ASCII
603 	// case-insensitively."
604 	command = command.toLowerCase();
605 
606 	// "If only one argument was provided, let show UI be false."
607 	//
608 	// If range was passed, I can't actually detect how many args were passed
609 	// . . .
610 	if (arguments.length == 1
611 	|| (arguments.length >=4 && typeof showUi == "undefined")) {
612 		showUi = false;
613 	}
614 
615 	// "If only one or two arguments were provided, let value be the empty
616 	// string."
617 	if (arguments.length <= 2
618 	|| (arguments.length >=4 && typeof value == "undefined")) {
619 		value = "";
620 	}
621 
622 	// "If command is not supported, raise a NOT_SUPPORTED_ERR exception."
623 	//
624 	// "If command has no action, raise an INVALID_ACCESS_ERR exception."
625 	return editCommandMethod(command, "action", range, (function(command, showUi, value) { return function() {
626 		// "If command is not enabled, return false."
627 		if (!myQueryCommandEnabled(command)) {
628 			return false;
629 		}
630 
631 		// "Take the action for command, passing value to the instructions as an
632 		// argument."
633 		commands[command].action(value, range);
634 
635 		// always fix the range after the command is complete
636 		setActiveRange(range);
637 		
638 		// "Return true."
639 		return true;
640 	}})(command, showUi, value));
641 }
642 
643 function myQueryCommandEnabled(command, range) {
644 	// "All of these methods must treat their command argument ASCII
645 	// case-insensitively."
646 	command = command.toLowerCase();
647 
648 	// "If command is not supported, raise a NOT_SUPPORTED_ERR exception."
649 	return editCommandMethod(command, "action", range, (function(command) { return function() {
650 		// "Among commands defined in this specification, those listed in
651 		// Miscellaneous commands are always enabled. The other commands defined
652 		// here are enabled if the active range is not null, and disabled
653 		// otherwise."
654 		return jQuery.inArray(command, ["copy", "cut", "paste", "selectall", "stylewithcss", "usecss"]) != -1
655 			|| range !== null;
656 	}})(command));
657 }
658 
659 function myQueryCommandIndeterm(command, range) {
660 	// "All of these methods must treat their command argument ASCII
661 	// case-insensitively."
662 	command = command.toLowerCase();
663 
664 	// "If command is not supported, raise a NOT_SUPPORTED_ERR exception."
665 	//
666 	// "If command has no indeterminacy, raise an INVALID_ACCESS_ERR
667 	// exception."
668 	return editCommandMethod(command, "indeterm", range, (function(command) { return function() {
669 		// "If command is not enabled, return false."
670 		if (!myQueryCommandEnabled(command, range)) {
671 			return false;
672 		}
673 
674 		// "Return true if command is indeterminate, otherwise false."
675 		return commands[command].indeterm( range );
676 	}})(command));
677 }
678 
679 function myQueryCommandState(command, range) {
680 	// "All of these methods must treat their command argument ASCII
681 	// case-insensitively."
682 	command = command.toLowerCase();
683 
684 	// "If command is not supported, raise a NOT_SUPPORTED_ERR exception."
685 	//
686 	// "If command has no state, raise an INVALID_ACCESS_ERR exception."
687 	return editCommandMethod(command, "state", range, (function(command) { return function() {
688 		// "If command is not enabled, return false."
689 		if (!myQueryCommandEnabled(command, range)) {
690 			return false;
691 		}
692 
693 		// "If the state override for command is set, return it."
694 		if (typeof getStateOverride(command, range) != "undefined") {
695 			return getStateOverride(command, range);
696 		}
697 
698 		// "Return true if command's state is true, otherwise false."
699 		return commands[command].state( range );
700 	}})(command));
701 }
702 
703 // "When the queryCommandSupported(command) method on the HTMLDocument
704 // interface is invoked, the user agent must return true if command is
705 // supported, and false otherwise."
706 function myQueryCommandSupported(command) {
707 	// "All of these methods must treat their command argument ASCII
708 	// case-insensitively."
709 	command = command.toLowerCase();
710 
711 	return command in commands;
712 }
713 
714 function myQueryCommandValue(command, range) {
715 	// "All of these methods must treat their command argument ASCII
716 	// case-insensitively."
717 	command = command.toLowerCase();
718 
719 	// "If command is not supported, raise a NOT_SUPPORTED_ERR exception."
720 	//
721 	// "If command has no value, raise an INVALID_ACCESS_ERR exception."
722 	return editCommandMethod(command, "value", range, function() {
723 		// "If command is not enabled, return the empty string."
724 		if (!myQueryCommandEnabled(command, range)) {
725 			return "";
726 		}
727 
728 		// "If command is "fontSize" and its value override is set, convert the
729 		// value override to an integer number of pixels and return the legacy
730 		// font size for the result."
731 		if (command == "fontsize"
732 		&& getValueOverride("fontsize", range) !== undefined) {
733 			return getLegacyFontSize(getValueOverride("fontsize", range));
734 		}
735 
736 		// "If the value override for command is set, return it."
737 		if (typeof getValueOverride(command, range) != "undefined") {
738 			return getValueOverride(command, range);
739 740 		}
741 
742 		// "Return command's value."
743 		return commands[command].value(range);
744 	});
745 }
746 //@}
747 
748 //////////////////////////////
749 ///// Common definitions /////
750 //////////////////////////////
751 //@{
752 
753 // "An HTML element is an Element whose namespace is the HTML namespace."
754 //
755 // I allow an extra argument to more easily check whether something is a
756 // particular HTML element, like isNamedHtmlElement(node, 'OL').  It accepts arrays
757 // too, like isHtmlElementInArray(node, ["OL", "UL"]) to check if it's an ol or ul.
758 // TODO This function was prominent during profiling. Remove it
759 //      and replace with calls to isAnyHtmlElement, isNamedHtmlElement
760 //      and is isMappedHtmlElement.
761 function isHtmlElement_obsolete(node, tags) {
762 	if (typeof tags == "string") {
763 		tags = [tags];
764 	}
765 	if (typeof tags == "object") {
766 		tags = $_( tags ).map(function(tag) { return tag.toUpperCase() });
767 	}
768 	return node
769 		&& node.nodeType == 1
770 		&& isHtmlNamespace(node.namespaceURI)
771 		&& (typeof tags == "undefined" || $_( tags ).indexOf(node.tagName) != -1);
772 }
773 
774 function isAnyHtmlElement(node) {
775 	return node
776 		&& node.nodeType == 1
777 		&& isHtmlNamespace(node.namespaceURI);
778 }
779 
780 // name should be uppercase
781 function isNamedHtmlElement(node, name) {
782 	return node
783 		&& node.nodeType == 1
784 		&& isHtmlNamespace(node.namespaceURI)
785 	    // This function is passed in a mix of upper and lower case names
786 		&& name.toUpperCase() === node.nodeName;
787 }
788 
789 // TODO remove when isHtmlElementInArray is removed
790 function arrayContainsInsensitive(array, str) {
791 	var i, len;
792 	str = str.toUpperCase();
793 	for (i = 0, len = array.length; i < len; i++) {
794 		if (array[i].toUpperCase() === str) {
795 			return true;
796 		}
797 	}
798 	return false;
799 }
800 // TODO replace calls to this function with calls to isMappedHtmlElement()
801 function isHtmlElementInArray(node, array) {
802 	return node
803 		&& node.nodeType == 1
804 		&& isHtmlNamespace(node.namespaceURI)
805 	    // This function is passed in a mix of upper and lower case names
806 		&& arrayContainsInsensitive(array, node.nodeName);
807 }
808 
809 // map must have all-uppercase keys
810 function isMappedHtmlElement(node, map) {
811 	return node
812 		&& node.nodeType == 1
813 		&& isHtmlNamespace(node.namespaceURI)
814 		&& map[node.nodeName];
815 }
816 
817 // "A prohibited paragraph child name is "address", "article", "aside",
818 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details",
819 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer",
820 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li",
821 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section",
822 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or
823 // "xmp"."
824 var prohibitedParagraphChildNamesMap = {
825 	"ADDRESS": true, "ARTICLE": true, "ASIDE": true,
826 	"BLOCKQUOTE": true, "CAPTION": true, "CENTER": true, "COL": true, "COLGROUP": true, "DD": true, "DETAILS": true,
827 	"DIR": true, "DIV": true, "DL": true, "DT": true, "FIELDSET": true, "FIGCAPTION": true, "FIGURE": true, "FOOTER": true,
828 	"FORM": true, "H1": true, "H2": true, "H3": true, "H4": true, "H5": true, "H6": true, "HEADER": true, "HGROUP": true, "HR": true, "LI": true,
829 	"LISTING": true, "MENU": true, "NAV": true, "OL": true, "P": true, "PLAINTEXT": true, "PRE": true, "SECTION": true,
830 	"SUMMARY": true, "TABLE": true, "TBODY": true, "TD": true, "TFOOT": true, "TH": true, "THEAD": true, "TR": true, "UL": true,
831 	"XMP": true
832 };
833 
834 // "A prohibited paragraph child is an HTML element whose local name is a
835 // prohibited paragraph child name."
836 function isProhibitedParagraphChild(node) {
837 	return isMappedHtmlElement(node, prohibitedParagraphChildNamesMap);
838 }
839 
840 var nonBlockDisplayValuesMap = {
841 	"inline": true,
842 	"inline-block": true,
843 	"inline-table": true,
844 	"none": true
845 };
846 
847 // "A block node is either an Element whose "display" property does not have
848 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
849 // Document, or a DocumentFragment."
850 function isBlockNode(node) {
851 	return node
852 		&& ((node.nodeType == $_.Node.ELEMENT_NODE && !nonBlockDisplayValuesMap[$_.getComputedStyle(node).display])
853 		|| node.nodeType == $_.Node.DOCUMENT_NODE
854 		|| node.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE);
855 }
856 
857 // "An inline node is a node that is not a block node."
858 function isInlineNode(node) {
859 	return node && !isBlockNode(node);
860 }
861 
862 // "An editing host is a node that is either an Element with a contenteditable
863 // attribute set to the true state, or the Element child of a Document whose
864 // designMode is enabled."
865 function isEditingHost(node) {
866 	return node
867 		&& node.nodeType == $_.Node.ELEMENT_NODE
868 		&& (node.contentEditable == "true"
869 		|| (node.parentNode
870 		&& node.parentNode.nodeType == $_.Node.DOCUMENT_NODE
871 		&& node.parentNode.designMode == "on"));
872 }
873 
874 // "Something is editable if it is a node which is not an editing host, does
875 // not have a contenteditable attribute set to the false state, and whose
876 // parent is an editing host or editable."
877 function isEditable(node) {
878 	// This is slightly a lie, because we're excluding non-HTML elements with
879 	// contentEditable attributes.
880 	return node
881 		&& !isEditingHost(node)
882 		&& (node.nodeType != $_.Node.ELEMENT_NODE || node.contentEditable != "false" || jQuery(node).hasClass('aloha-table-wrapper'))
883 		&& (isEditingHost(node.parentNode) || isEditable(node.parentNode));
884 }
885 
886 // Helper function, not defined in the spec
887 function hasEditableDescendants(node) {
888 	for (var i = 0; i < node.childNodes.length; i++) {
889 		if (isEditable(node.childNodes[i])
890 		|| hasEditableDescendants(node.childNodes[i])) {
891 			return true;
892 		}
893 	}
894 	return false;
895 }
896 
897 // "The editing host of node is null if node is neither editable nor an editing
898 // host; node itself, if node is an editing host; or the nearest ancestor of
899 // node that is an editing host, if node is editable."
900 function getEditingHostOf(node) {
901 	if (isEditingHost(node)) {
902 		return node;
903 	} else if (isEditable(node)) {
904 		var ancestor = node.parentNode;
905 		while (!isEditingHost(ancestor)) {
906 			ancestor = ancestor.parentNode;
907 		}
908 		return ancestor;
909 	} else {
910 		return null;
911 	}
912 }
913 
914 // "Two nodes are in the same editing host if the editing host of the first is
915 // non-null and the same as the editing host of the second."
916 function inSameEditingHost(node1, node2) {
917 	return getEditingHostOf(node1)
918 		&& getEditingHostOf(node1) == getEditingHostOf(node2);
919 }
920 
921 // "A collapsed line break is a br that begins a line box which has nothing
922 // else in it, and therefore has zero height."
923 function isCollapsedLineBreak(br) {
924 	if (!isNamedHtmlElement(br, 'br')) {
925 		return false;
926 	}
927 
928 	// Add a zwsp after it and see if that changes the height of the nearest
929 	// non-inline parent.  Note: this is not actually reliable, because the
930 	// parent might have a fixed height or something.
931 	var ref = br.parentNode;
932 	while ($_.getComputedStyle(ref).display == "inline") {
933 		ref = ref.parentNode;
934 	}
935 
936 	var origStyle = {
937 		height: ref.style.height,
938 		maxHeight: ref.style.maxHeight,
939 		minHeight: ref.style.minHeight
940 	};
941 
942 	ref.style.height = 'auto';
943 	ref.style.maxHeight = 'none';
944 	if (!(jQuery.browser.msie && jQuery.browser.version < 8)) {
945 		ref.style.minHeight = '0';
946 	}
947 	var space = document.createTextNode('\u200b');
948 	var origHeight = ref.offsetHeight;
949 	if (origHeight == 0) {
950 		throw 'isCollapsedLineBreak: original height is zero, bug?';
951 	}
952 	br.parentNode.insertBefore(space, br.nextSibling);
953 	var finalHeight = ref.offsetHeight;
954 	space.parentNode.removeChild(space);
955 
956 	ref.style.height = origStyle.height;
957 	ref.style.maxHeight = origStyle.maxHeight;
958 	if (!(jQuery.browser.msie && jQuery.browser.version < 8)) {
959 		ref.style.minHeight = origStyle.minHeight;
960 	}
961 
962 	// Allow some leeway in case the zwsp didn't create a whole new line, but
963 	// only made an existing line slightly higher.  Firefox 6.0a2 shows this
964 	// behavior when the first line is bold.
965 	return origHeight < finalHeight - 5;
966 }
967 
968 // "An extraneous line break is a br that has no visual effect, in that
969 // removing it from the DOM would not change layout, except that a br that is
970 // the sole child of an li is not extraneous."
971 function isExtraneousLineBreak(br) {
972 
973 	if (!isNamedHtmlElement(br, 'br')) {
974 		return false;
975 	}
976 
977 	if (isNamedHtmlElement(br.parentNode, "li")
978 	&& br.parentNode.childNodes.length == 1) {
979 		return false;
980 	}
981 
982 	// Make the line break disappear and see if that changes the block's
983 	// height.  Yes, this is an absurd hack.  We have to reset height etc. on
984 	// the reference node because otherwise its height won't change if it's not
985 	// auto.
986 	var ref = br.parentNode;
987 	while ($_.getComputedStyle(ref).display == "inline") {
988 		ref = ref.parentNode;
989 	}
990 
991 	var origStyle = {
992 		height: ref.style.height,
993 		maxHeight: ref.style.maxHeight,
994 		minHeight: ref.style.minHeight,
995 		contentEditable: ref.contentEditable
996 	};
997 
998 	ref.style.height = 'auto';
999 	ref.style.maxHeight = 'none';
1000 	ref.style.minHeight = '0';
1001 	// IE7 would ignore display:none in contentEditable, so we temporarily set it to false
1002 	if (jQuery.browser.msie && jQuery.browser.version <= 7) {
1003 		ref.contentEditable = 'false';
1004 	}
1005 
1006 	var origHeight = ref.offsetHeight;
1007 	if (origHeight == 0) {
1008 		throw "isExtraneousLineBreak: original height is zero, bug?";
1009 	}
1010 
1011 	var origBrDisplay = br.style.display;
1012 	br.style.display = 'none';
1013 	var finalHeight = ref.offsetHeight;
1014 
1015 	// Restore original styles to the touched elements.
1016 	ref.style.height = origStyle.height;
1017 	ref.style.maxHeight = origStyle.maxHeight;
1018 	ref.style.minHeight = origStyle.minHeight;
1019 	// reset contentEditable for IE7
1020 	if (jQuery.browser.msie && jQuery.browser.version <= 7) {
1021 		ref.contentEditable = origStyle.contentEditable;
1022 	}
1023 	br.style.display = origBrDisplay;
1024 
1025 	// https://github.com/alohaeditor/Aloha-Editor/issues/516
1026 	// look like it works in msie > 7
1027 1028 	/* if (jQuery.browser.msie && jQuery.browser.version < 8) {
1029 		br.removeAttribute("style");
1030 		ref.removeAttribute("style");
1031 	} */
1032 
1033 	return origHeight == finalHeight;
1034 }
1035 
1036 // "A whitespace node is either a Text node whose data is the empty string; or
1037 // a Text node whose data consists only of one or more tabs (0x0009), line
1038 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
1039 // parent is an Element whose resolved value for "white-space" is "normal" or
1040 // "nowrap"; or a Text node whose data consists only of one or more tabs
1041 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
1042 // parent is an Element whose resolved value for "white-space" is "pre-line"."
1043 function isWhitespaceNode(node) {
1044 	return node
1045 		&& node.nodeType == $_.Node.TEXT_NODE
1046 		&& (node.data == ""
1047 		|| (
1048 			/^[\t\n\r ]+$/.test(node.data)
1049 			&& node.parentNode
1050 			&& node.parentNode.nodeType == $_.Node.ELEMENT_NODE
1051 			&& jQuery.inArray($_.getComputedStyle(node.parentNode).whiteSpace, ["normal", "nowrap"]) != -1
1052 		) || (
1053 			/^[\t\r ]+$/.test(node.data)
1054 			&& node.parentNode
1055 1056 			&& node.parentNode.nodeType == $_.Node.ELEMENT_NODE
1057 			&& $_.getComputedStyle(node.parentNode).whiteSpace == "pre-line"
1058 		) || (
1059 			/^[\t\n\r ]+$/.test(node.data)
1060 			&& node.parentNode
1061 			&& node.parentNode.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE
1062 		));
1063 }
1064 
1065 /**
1066  * Collapse sequences of ignorable whitespace (tab (0x0009), line feed (0x000A), carriage return (0x000D), space (0x0020)) to only one space.
1067  * Preserve the given range if necessary.
1068  * @param node text node
1069  * @param range range
1070  */
1071 function collapseWhitespace(node, range) {
1072 	// "If node is neither editable nor an editing host, abort these steps."
1073 	if (!isEditable(node) && !isEditingHost(node)) {
1074 		return;
1075 	}
1076 
1077 	// if the given node is not a text node, return
1078 	if (!node || node.nodeType !== $_.Node.TEXT_NODE) {
1079 		return;
1080 	}
1081 
1082 	// if the node is in a pre or pre-wrap node, return
1083 	if (jQuery.inArray($_.getComputedStyle(node.parentNode).whiteSpace, ["pre", "pre-wrap"]) != -1) {
1084 		return;
1085 	}
1086 
1087 	// if the given node does not contain sequences of at least two consecutive ignorable whitespace characters, return
1088 	if (!/[\t\n\r ]{2,}/.test(node.data)) {
1089 		return;
1090 	}
1091 
1092 	var newData = '';
1093 	var correctStart = range.startContainer == node;
1094 	var correctEnd = range.endContainer == node;
1095 	var wsFound = false;
1096 
1097 	// iterate through the node data
1098 	for (var i = 0; i < node.data.length; ++i) {
1099 		if (/[\t\n\r ]/.test(node.data.substr(i, 1))) {
1100 			// found a whitespace
1101 			if (!wsFound) {
1102 				// this is the first whitespace in the current sequence
1103 				// add a whitespace to the new data sequence
1104 				newData += ' ';
1105 				// remember that we found a whitespace
1106 				wsFound = true;
1107 			} else {
1108 				// this is not the first whitespace in the sequence, so omit this character
1109 				if (correctStart && newData.length < range.startOffset) {
1110 					range.startOffset--;
1111 				}
1112 				if (correctEnd && newData.length < range.endOffset) {
1113 					range.endOffset--;
1114 				}
1115 			}
1116 		} else {
1117 			newData += node.data.substr(i, 1);
1118 			wsFound = false;
1119 		}
1120 	}
1121 
1122 	// set the new data
1123 	node.data = newData;
1124 }
1125 
1126 // "node is a collapsed whitespace node if the following algorithm returns
1127 // true:"
1128 function isCollapsedWhitespaceNode(node) {
1129 	// "If node is not a whitespace node, return false."
1130 	if (!isWhitespaceNode(node)) {
1131 		return false;
1132 	}
1133 
1134 	// "If node's data is the empty string, return true."
1135 	if (node.data == "") {
1136 		return true;
1137 	}
1138 
1139 	// "Let ancestor be node's parent."
1140 	var ancestor = node.parentNode;
1141 
1142 	// "If ancestor is null, return true."
1143 	if (!ancestor) {
1144 		return true;
1145 	}
1146 
1147 	// "If the "display" property of some ancestor of node has resolved value
1148 	// "none", return true."
1149 	if ($_( getAncestors(node) ).some(function(ancestor) {
1150 		return ancestor.nodeType == $_.Node.ELEMENT_NODE
1151 			&& $_.getComputedStyle(ancestor).display == "none";
1152 	})) {
1153 		return true;
1154 	}
1155 
1156 	// "While ancestor is not a block node and its parent is not null, set
1157 	// ancestor to its parent."
1158 	while (!isBlockNode(ancestor)
1159 	&& ancestor.parentNode) {
1160 		ancestor = ancestor.parentNode;
1161 	}
1162 
1163 	// "Let reference be node."
1164 	var reference = node;
1165 
1166 	// "While reference is a descendant of ancestor:"
1167 	while (reference != ancestor) {
1168 		// "Let reference be the node before it in tree order."
1169 		reference = previousNode(reference);
1170 
1171 		// "If reference is a block node or a br, return true."
1172 		if (isBlockNode(reference)
1173 		|| isNamedHtmlElement(reference, 'br')) {
1174 			return true;
1175 		}
1176 
1177 		// "If reference is a Text node that is not a whitespace node, or is an
1178 		// img, break from this loop."
1179 		if ((reference.nodeType == $_.Node.TEXT_NODE && !isWhitespaceNode(reference))
1180 		|| isNamedHtmlElement(reference, 'img')) {
1181 			break;
1182 		}
1183 	}
1184 
1185 	// "Let reference be node."
1186 	reference = node;
1187 
1188 	// "While reference is a descendant of ancestor:"
1189 	var stop = nextNodeDescendants(ancestor);
1190 	while (reference != stop) {
1191 		// "Let reference be the node after it in tree order, or null if there
1192 		// is no such node."
1193 		reference = nextNode(reference);
1194 
1195 		// "If reference is a block node or a br, return true."
1196 		if (isBlockNode(reference)
1197 		|| isNamedHtmlElement(reference, 'br')) {
1198 			return true;
1199 		}
1200 
1201 		// "If reference is a Text node that is not a whitespace node, or is an
1202 		// img, break from this loop."
1203 		if ((reference && reference.nodeType == $_.Node.TEXT_NODE && !isWhitespaceNode(reference))
1204 		|| isNamedHtmlElement(reference, 'img')) {
1205 			break;
1206 		}
1207 	}
1208 
1209 	// "Return false."
1210 	return false;
1211 }
1212 
1213 // "Something is visible if it is a node that either is a block node, or a Text
1214 // node that is not a collapsed whitespace node, or an img, or a br that is not
1215 // an extraneous line break, or any node with a visible descendant; excluding
1216 // any node with an ancestor container Element whose "display" property has
1217 // resolved value "none"."
1218 function isVisible(node) {
1219 	if (!node) {
1220 		return false;
1221 	}
1222 
1223 	if ($_( getAncestors(node).concat(node) )
1224 	.filter(function(node) { return node.nodeType == $_.Node.ELEMENT_NODE }, true)
1225 	.some(function(node) { return $_.getComputedStyle(node).display == "none" })) {
1226 		return false;
1227 	}
1228 
1229 	if (isBlockNode(node)
1230 	|| (node.nodeType == $_.Node.TEXT_NODE && !isCollapsedWhitespaceNode(node))
1231 	|| isNamedHtmlElement(node, 'img')
1232 	|| (isNamedHtmlElement(node, 'br') && !isExtraneousLineBreak(node))) {
1233 		return true;
1234 	}
1235 
1236 	for (var i = 0; i < node.childNodes.length; i++) {
1237 		if (isVisible(node.childNodes[i])) {
1238 			return true;
1239 		}
1240 	}
1241 
1242 	return false;
1243 }
1244 
1245 // "Something is invisible if it is a node that is not visible."
1246 function isInvisible(node) {
1247 	return node && !isVisible(node);
1248 }
1249 
1250 // "A collapsed block prop is either a collapsed line break that is not an
1251 // extraneous line break, or an Element that is an inline node and whose
1252 // children are all either invisible or collapsed block props and that has at
1253 // least one child that is a collapsed block prop."
1254 function isCollapsedBlockProp(node) {
1255 	if (isCollapsedLineBreak(node)
1256 	&& !isExtraneousLineBreak(node)) {
1257 		return true;
1258 	}
1259 
1260 	if (!isInlineNode(node)
1261 	|| node.nodeType != $_.Node.ELEMENT_NODE) {
1262 		return false;
1263 	}
1264 
1265 	var hasCollapsedBlockPropChild = false;
1266 	for (var i = 0; i < node.childNodes.length; i++) {
1267 		if (!isInvisible(node.childNodes[i])
1268 		&& !isCollapsedBlockProp(node.childNodes[i])) {
1269 			return false;
1270 		}
1271 		if (isCollapsedBlockProp(node.childNodes[i])) {
1272 			hasCollapsedBlockPropChild = true;
1273 		}
1274 	}
1275 
1276 	return hasCollapsedBlockPropChild;
1277 }
1278 
1279 function setActiveRange( range ) {
1280 	var rangeObject = new window.GENTICS.Utils.RangeObject();
1281 	
1282 	rangeObject.startContainer = range.startContainer;
1283 	rangeObject.startOffset = range.startOffset;
1284 	rangeObject.endContainer = range.endContainer;
1285 	rangeObject.endOffset = range.endOffset;
1286 	
1287 	rangeObject.select();
1288 }
1289 
1290 // Please note: This method is deprecated and will be removed. 
1291 // Every command should use the value and range parameter. 
1292 // 
1293 // "The active range is the first range in the Selection given by calling
1294 // getSelection() on the context object, or null if there is no such range."
1295 //
1296 // We cheat and return globalRange if that's defined.  We also ensure that the
1297 // active range meets the requirements that selection boundary points are
1298 // supposed to meet, i.e., that the nodes are both Text or Element nodes that
1299 // descend from a Document.
1300 function getActiveRange() {
1301 	var ret;
1302 	if (globalRange) {
1303 		ret = globalRange;
1304 	} else if (Aloha.getSelection().rangeCount) {
1305 		ret = Aloha.getSelection().getRangeAt(0);
1306 	} else {
1307 		return null;
1308 	}
1309 	if (jQuery.inArray(ret.startContainer.nodeType, [$_.Node.TEXT_NODE, $_.Node.ELEMENT_NODE]) == -1
1310 	|| jQuery.inArray(ret.endContainer.nodeType, [$_.Node.TEXT_NODE, $_.Node.ELEMENT_NODE]) == -1
1311 	|| !ret.startContainer.ownerDocument
1312 	|| !ret.endContainer.ownerDocument
1313 	|| !isDescendant(ret.startContainer, ret.startContainer.ownerDocument)
1314 	|| !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) {
1315 		throw "Invalid active range; test bug?";
1316 	}
1317 	return ret;
1318 }
1319 
1320 // "For some commands, each HTMLDocument must have a boolean state override
1321 // and/or a string value override. These do not change the command's state or
1322 // value, but change the way some algorithms behave, as specified in those
1323 // algorithms' definitions. Initially, both must be unset for every command.
1324 // Whenever the number of ranges in the Selection changes to something
1325 // different, and whenever a boundary point of the range at a given index in
1326 // the Selection changes to something different, the state override and value
1327 // override must be unset for every command."
1328 //
1329 // We implement this crudely by using setters and getters.  To verify that the
1330 // selection hasn't changed, we copy the active range and just check the
1331 // endpoints match.  This isn't really correct, but it's good enough for us.
1332 // Unset state/value overrides are undefined.  We put everything in a function
1333 // so no one can access anything except via the provided functions, since
1334 // otherwise callers might mistakenly use outdated overrides (if the selection
1335 // has changed).
1336 var getStateOverride, setStateOverride, unsetStateOverride,
1337 	getValueOverride, setValueOverride, unsetValueOverride;
1338 (function() {
1339 	var stateOverrides = {};
1340 	var valueOverrides = {};
1341 	var storedRange = null;
1342 
1343 	function resetOverrides(range) {
1344 		if (!storedRange
1345 		|| storedRange.startContainer != range.startContainer
1346 		|| storedRange.endContainer != range.endContainer
1347 		|| storedRange.startOffset != range.startOffset
1348 		|| storedRange.endOffset != range.endOffset) {
1349 			stateOverrides = {};
1350 			valueOverrides = {};
1351 			storedRange = range.cloneRange();
1352 		}
1353 	}
1354 
1355 	getStateOverride = function(command, range) {
1356 		resetOverrides(range);
1357 		return stateOverrides[command];
1358 	};
1359 
1360 	setStateOverride = function(command, newState, range) {
1361 		resetOverrides(range);
1362 		stateOverrides[command] = newState;
1363 	};
1364 
1365 	unsetStateOverride = function(command, range) {
1366 		resetOverrides(range);
1367 		delete stateOverrides[command];
1368 	}
1369 
1370 	getValueOverride = function(command, range) {
1371 		resetOverrides(range);
1372 		return valueOverrides[command];
1373 	}
1374 
1375 	// "The value override for the backColor command must be the same as the
1376 	// value override for the hiliteColor command, such that setting one sets
1377 	// the other to the same thing and unsetting one unsets the other."
1378 	setValueOverride = function(command, newValue, range) {
1379 		resetOverrides(range);
1380 		valueOverrides[command] = newValue;
1381 		if (command == "backcolor") {
1382 			valueOverrides.hilitecolor = newValue;
1383 		} else if (command == "hilitecolor") {
1384 			valueOverrides.backcolor = newValue;
1385 		}
1386 	}
1387 
1388 	unsetValueOverride = function(command, range) {
1389 		resetOverrides(range);
1390 		delete valueOverrides[command];
1391 		if (command == "backcolor") {
1392 			delete valueOverrides.hilitecolor;
1393 		} else if (command == "hilitecolor") {
1394 			delete valueOverrides.backcolor;
1395 		}
1396 	}
1397 })();
1398 
1399 //@}
1400 
1401 /////////////////////////////
1402 ///// Common algorithms /////
1403 /////////////////////////////
1404 
1405 ///// Assorted common algorithms /////
1406 //@{
1407 
1408 function movePreservingRanges(node, newParent, newIndex, range) {
1409 	// For convenience, I allow newIndex to be -1 to mean "insert at the end".
1410 	if (newIndex == -1) {
1411 		newIndex = newParent.childNodes.length;
1412 	}
1413 
1414 	// "When the user agent is to move a Node to a new location, preserving
1415 	// ranges, it must remove the Node from its original parent (if any), then
1416 	// insert it in the new location. In doing so, however, it must ignore the
1417 	// regular range mutation rules, and instead follow these rules:"
1418 
1419 	// "Let node be the moved Node, old parent and old index be the old parent
1420 	// (which may be null) and index, and new parent and new index be the new
1421 	// parent and index."
1422 	var oldParent = node.parentNode;
1423 	var oldIndex = getNodeIndex(node);
1424 
1425 	// We only even attempt to preserve the global range object and the ranges
1426 	// in the selection, not every range out there (the latter is probably
1427 	// impossible).
1428 	var ranges = [range];
1429 	for (var i = 0; i < Aloha.getSelection().rangeCount; i++) {
1430 		ranges.push(Aloha.getSelection().getRangeAt(i));
1431 	}
1432 	var boundaryPoints = [];
1433 	$_( ranges ).forEach(function(range) {
1434 		boundaryPoints.push([range.startContainer, range.startOffset]);
1435 		boundaryPoints.push([range.endContainer, range.endOffset]);
1436 	});
1437 
1438 	$_( boundaryPoints ).forEach(function(boundaryPoint) {
1439 		// "If a boundary point's node is the same as or a descendant of node,
1440 		// leave it unchanged, so it moves to the new location."
1441 		//
1442 		// No modifications necessary.
1443 
1444 		// "If a boundary point's node is new parent and its offset is greater
1445 		// than new index, add one to its offset."
1446 		if (boundaryPoint[0] == newParent
1447 		&& boundaryPoint[1] > newIndex) {
1448 			boundaryPoint[1]++;
1449 		}
1450 
1451 		// "If a boundary point's node is old parent and its offset is old index or
1452 		// old index + 1, set its node to new parent and add new index − old index
1453 		// to its offset."
1454 		if (boundaryPoint[0] == oldParent
1455 		&& (boundaryPoint[1] == oldIndex
1456 		|| boundaryPoint[1] == oldIndex + 1)) {
1457 			boundaryPoint[0] = newParent;
1458 			boundaryPoint[1] += newIndex - oldIndex;
1459 		}
1460 
1461 		// "If a boundary point's node is old parent and its offset is greater than
1462 		// old index + 1, subtract one from its offset."
1463 		if (boundaryPoint[0] == oldParent
1464 		&& boundaryPoint[1] > oldIndex + 1) {
1465 			boundaryPoint[1]--;
1466 		}
1467 	});
1468 
1469 	// Now actually move it and preserve the ranges.
1470 	if (newParent.childNodes.length == newIndex) {
1471 		newParent.appendChild(node);
1472 	} else {
1473 		newParent.insertBefore(node, newParent.childNodes[newIndex]);
1474 	}
1475 
1476 	// if we're off actual node boundaries this implies that the move was
1477 	// part of a deletion process (backspace). If that's the case we 
1478 	// attempt to fix this by restoring the range to the first index of
1479 	// the node that has been moved
1480 	if (boundaryPoints[0][1] > boundaryPoints[0][0].childNodes.length
1481 	&& boundaryPoints[1][1] > boundaryPoints[1][0].childNodes.length) {
1482 		range.setStart(node, 0);
1483 		range.setEnd(node, 0);
1484 	} else {
1485 		range.setStart(boundaryPoints[0][0], boundaryPoints[0][1]);
1486 		range.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]);
1487 
1488 		Aloha.getSelection().removeAllRanges();
1489 		for (var i = 1; i < ranges.length; i++) {
1490 			var newRange = Aloha.createRange();
1491 			newRange.setStart(boundaryPoints[2*i][0], boundaryPoints[2*i][1]);
1492 			newRange.setEnd(boundaryPoints[2*i + 1][0], boundaryPoints[2*i + 1][1]);
1493 			Aloha.getSelection().addRange(newRange);
1494 		}
1495 		if (newRange) {
1496 			range = newRange;
1497 		}
1498 	}
1499 }
1500 
1501 /**
1502  * Copy all non empty attributes from an existing to a new element
1503  * 
1504  * @param {dom} element The source DOM element
1505  * @param {dom} newElement The new DOM element which will get the attributes of the source DOM element
1506  * @return void
1507  */
1508 function copyAttributes( element, newElement ) {
1509 
1510 	// This is an IE7 workaround. We identified three places that were connected 
1511 	// to the mysterious ie7 crash:
1512 	// 1. Add attribute to dom element (Initialization of jquery-ui sortable)
1513 	// 2. Access the jquery expando attribute. Just reading the name is 
1514 	//    sufficient to make the browser vulnerable for the crash (Press enter)
1515 	// 3. On editable blur the Aloha.editables[0].getContents(); gets invoked.
1516 	//    This invokation somehow crashes the ie7. We assume that the access of 
1517 	//    shared expando attribute updates internal references which are not 
1518 	//    correclty handled during clone(); 
1519 	if ( jQuery.browser.msie && jQuery.browser.version >=7 && typeof element.attributes[jQuery.expando] !== 'undefined' ) {
1520 		jQuery(element).removeAttr(jQuery.expando);
1521 	}
1522 
1523 	var attrs = element.attributes;
1524 	for ( var i = 0; i < attrs.length; i++ ) {
1525 		var attr = attrs[i];
1526 		// attr.specified is an IE specific check to exclude attributes that were never really set.
1527 		if (typeof attr.specified === "undefined" || attr.specified) {
1528 			if ( typeof newElement.setAttributeNS === 'function' ) {
1529 				newElement.setAttributeNS( attr.namespaceURI, attr.name, attr.value );
1530 			} else {
1531 				// fixes https://github.com/alohaeditor/Aloha-Editor/issues/515 
1532 				newElement.setAttribute( attr.name, attr.value );
1533 			}
1534 		}
1535 	}
1536 }
1537 
1538 function setTagName(element, newName, range) {
1539 	// "If element is an HTML element with local name equal to new name, return
1540 	// element."
1541 	if (isNamedHtmlElement(element, newName)) {
1542 1543 		return element;
1544 	}
1545 
1546 1547 	// "If element's parent is null, return element."
1548 	if (!element.parentNode) {
1549 		return element;
1550 	}
1551 
1552 	// "Let replacement element be the result of calling createElement(new
1553 	// name) on the ownerDocument of element."
1554 	var replacementElement = element.ownerDocument.createElement(newName);
1555 
1556 	// "Insert replacement element into element's parent immediately before
1557 	// element."
1558 	element.parentNode.insertBefore(replacementElement, element);
1559 
1560 	// "Copy all attributes of element to replacement element, in order."
1561 	copyAttributes( element,  replacementElement );
1562 	
1563 	// "While element has children, append the first child of element as the
1564 	// last child of replacement element, preserving ranges."
1565 	while (element.childNodes.length) {
1566 		movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length, range);
1567 	}
1568 
1569 	// "Remove element from its parent."
1570 	element.parentNode.removeChild(element);
1571 
1572 	// if the range still uses the old element, we modify it to the new one
1573 	if (range.startContainer === element) {
1574 		range.startContainer = replacementElement;
1575 	}
1576 	if (range.endContainer === element) {
1577 		range.endContainer = replacementElement;
1578 	}
1579 
1580 	// "Return replacement element."
1581 	return replacementElement;
1582 }
1583 
1584 function removeExtraneousLineBreaksBefore(node) {
1585 	// "Let ref be the previousSibling of node."
1586 	var ref = node.previousSibling;
1587 
1588 	// "If ref is null, abort these steps."
1589 	if (!ref) {
1590 		return;
1591 	}
1592 
1593 	// "While ref has children, set ref to its lastChild."
1594 	while (ref.hasChildNodes()) {
1595 		ref = ref.lastChild;
1596 	}
1597 
1598 	// "While ref is invisible but not an extraneous line break, and ref does
1599 	// not equal node's parent, set ref to the node before it in tree order."
1600 	while (isInvisible(ref)
1601 	&& !isExtraneousLineBreak(ref)
1602 	&& ref != node.parentNode) {
1603 		ref = previousNode(ref);
1604 	}
1605 
1606 	// "If ref is an editable extraneous line break, remove it from its
1607 	// parent."
1608 1609 	if (isEditable(ref)
1610 	&& isExtraneousLineBreak(ref)) {
1611 		ref.parentNode.removeChild(ref);
1612 	}
1613 }
1614 
1615 function removeExtraneousLineBreaksAtTheEndOf(node) {
1616 	// "Let ref be node."
1617 	var ref = node;
1618 
1619 	// "While ref has children, set ref to its lastChild."
1620 	while (ref.hasChildNodes()) {
1621 		ref = ref.lastChild;
1622 	}
1623 
1624 	// "While ref is invisible but not an extraneous line break, and ref does
1625 	// not equal node, set ref to the node before it in tree order."
1626 	while (isInvisible(ref)
1627 	&& !isExtraneousLineBreak(ref)
1628 	&& ref != node) {
1629 		ref = previousNode(ref);
1630 	}
1631 
1632 	// "If ref is an editable extraneous line break, remove it from its
1633 	// parent."
1634 	if (isEditable(ref)
1635 	&& isExtraneousLineBreak(ref)) {
1636 		ref.parentNode.removeChild(ref);
1637 	}
1638 }
1639 
1640 // "To remove extraneous line breaks from a node, first remove extraneous line
1641 // breaks before it, then remove extraneous line breaks at the end of it."
1642 function removeExtraneousLineBreaksFrom(node) {
1643 	removeExtraneousLineBreaksBefore(node);
1644 	removeExtraneousLineBreaksAtTheEndOf(node);
1645 }
1646 
1647 //@}
1648 ///// Wrapping a list of nodes /////
1649 //@{
1650 
1651 function wrap(nodeList, siblingCriteria, newParentInstructions, range) {
1652 	// "If not provided, sibling criteria returns false and new parent
1653 	// instructions returns null."
1654 	if (typeof siblingCriteria == "undefined") {
1655 		siblingCriteria = function() { return false };
1656 	}
1657 	if (typeof newParentInstructions == "undefined") {
1658 		newParentInstructions = function() { return null };
1659 	}
1660 
1661 	// "If node list is empty, or the first member of node list is not
1662 	// editable, return null and abort these steps."
1663 	if (!nodeList.length
1664 	|| !isEditable(nodeList[0])) {
1665 		return null;
1666 	}
1667 
1668 	// "If node list's last member is an inline node that's not a br, and node
1669 	// list's last member's nextSibling is a br, append that br to node list."
1670 	if (isInlineNode(nodeList[nodeList.length - 1])
1671 	&& !isNamedHtmlElement(nodeList[nodeList.length - 1], "br")
1672 	&& isNamedHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) {
1673 		nodeList.push(nodeList[nodeList.length - 1].nextSibling);
1674 	}
1675 
1676 	// "If the previousSibling of the first member of node list is editable and
1677 	// running sibling criteria on it returns true, let new parent be the
1678 	// previousSibling of the first member of node list."
1679 	var newParent;
1680 	if (isEditable(nodeList[0].previousSibling)
1681 	&& siblingCriteria(nodeList[0].previousSibling)) {
1682 		newParent = nodeList[0].previousSibling;
1683 
1684 	// "Otherwise, if the nextSibling of the last member of node list is
1685 	// editable and running sibling criteria on it returns true, let new parent
1686 	// be the nextSibling of the last member of node list."
1687 	} else if (isEditable(nodeList[nodeList.length - 1].nextSibling)
1688 	&& siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) {
1689 		newParent = nodeList[nodeList.length - 1].nextSibling;
1690 
1691 	// "Otherwise, run new parent instructions, and let new parent be the
1692 	// result."
1693 	} else {
1694 		newParent = newParentInstructions();
1695 	}
1696 
1697 	// "If new parent is null, abort these steps and return null."
1698 	if (!newParent) {
1699 		return null;
1700 	}
1701 
1702 	// "If new parent's parent is null:"
1703 	if (!newParent.parentNode) {
1704 		// "Insert new parent into the parent of the first member of node list
1705 		// immediately before the first member of node list."
1706 		nodeList[0].parentNode.insertBefore(newParent, nodeList[0]);
1707 
1708 		// "If any range has a boundary point with node equal to the parent of
1709 		// new parent and offset equal to the index of new parent, add one to
1710 		// that boundary point's offset."
1711 		//
1712 		// Try to fix range
1713 		var startContainer = range.startContainer, startOffset = range.startOffset,
1714 			endContainer = range.endContainer, endOffset = range.endOffset;
1715 		if (startContainer == newParent.parentNode
1716 		&& startOffset >= getNodeIndex(newParent)) {
1717 			range.setStart(startContainer, startOffset + 1);
1718 		}
1719 		if (endContainer == newParent.parentNode
1720 		&& endOffset >= getNodeIndex(newParent)) {
1721 			range.setEnd(endContainer, endOffset + 1);
1722 		}
1723 
1724 		// Only try to fix the global range. TODO remove globalRange here
1725 		if (globalRange && globalRange !== range) {
1726 			startContainer = globalRange.startContainer, startOffset = globalRange.startOffset,
1727 				endContainer = globalRange.endContainer, endOffset = globalRange.endOffset;
1728 			if (startContainer == newParent.parentNode
1729 			&& startOffset >= getNodeIndex(newParent)) {
1730 				globalRange.setStart(startContainer, startOffset + 1);
1731 			}
1732 			if (endContainer == newParent.parentNode
1733 			&& endOffset >= getNodeIndex(newParent)) {
1734 				globalRange.setEnd(endContainer, endOffset + 1);
1735 			}
1736 		}
1737 	}
1738 
1739 	// "Let original parent be the parent of the first member of node list."
1740 	var originalParent = nodeList[0].parentNode;
1741 
1742 	// "If new parent is before the first member of node list in tree order:"
1743 	if (isBefore(newParent, nodeList[0])) {
1744 		// "If new parent is not an inline node, but the last child of new
1745 		// parent and the first member of node list are both inline nodes, and
1746 		// the last child of new parent is not a br, call createElement("br")
1747 		// on the ownerDocument of new parent and append the result as the last
1748 		// child of new parent."
1749 		if (!isInlineNode(newParent)
1750 		&& isInlineNode(newParent.lastChild)
1751 		&& isInlineNode(nodeList[0])
1752 		&& !isNamedHtmlElement(newParent.lastChild, "BR")) {
1753 			newParent.appendChild(newParent.ownerDocument.createElement("br"));
1754 		}
1755 
1756 		// "For each node in node list, append node as the last child of new
1757 		// parent, preserving ranges."
1758 		for (var i = 0; i < nodeList.length; i++) {
1759 			movePreservingRanges(nodeList[i], newParent, -1, range);
1760 		}
1761 
1762 	// "Otherwise:"
1763 	} else {
1764 		// "If new parent is not an inline node, but the first child of new
1765 		// parent and the last member of node list are both inline nodes, and
1766 		// the last member of node list is not a br, call createElement("br")
1767 		// on the ownerDocument of new parent and insert the result as the
1768 		// first child of new parent."
1769 		if (!isInlineNode(newParent)
1770 		&& isInlineNode(newParent.firstChild)
1771 		&& isInlineNode(nodeList[nodeList.length - 1])
1772 		&& !isNamedHtmlElement(nodeList[nodeList.length - 1], "BR")) {
1773 			newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild);
1774 		}
1775 
1776 		// "For each node in node list, in reverse order, insert node as the
1777 		// first child of new parent, preserving ranges."
1778 		for (var i = nodeList.length - 1; i >= 0; i--) {
1779 			movePreservingRanges(nodeList[i], newParent, 0, range);
1780 		}
1781 	}
1782 
1783 	// "If original parent is editable and has no children, remove it from its
1784 	// parent."
1785 	if (isEditable(originalParent) && !originalParent.hasChildNodes()) {
1786 		originalParent.parentNode.removeChild(originalParent);
1787 	}
1788 
1789 	// "If new parent's nextSibling is editable and running sibling criteria on
1790 	// it returns true:"
1791 	if (isEditable(newParent.nextSibling)
1792 	&& siblingCriteria(newParent.nextSibling)) {
1793 		// "If new parent is not an inline node, but new parent's last child
1794 		// and new parent's nextSibling's first child are both inline nodes,
1795 		// and new parent's last child is not a br, call createElement("br") on
1796 		// the ownerDocument of new parent and append the result as the last
1797 		// child of new parent."
1798 		if (!isInlineNode(newParent)
1799 		&& isInlineNode(newParent.lastChild)
1800 		&& isInlineNode(newParent.nextSibling.firstChild)
1801 		&& !isNamedHtmlElement(newParent.lastChild, "BR")) {
1802 			newParent.appendChild(newParent.ownerDocument.createElement("br"));
1803 		}
1804 
1805 		// "While new parent's nextSibling has children, append its first child
1806 		// as the last child of new parent, preserving ranges."
1807 		while (newParent.nextSibling.hasChildNodes()) {
1808 			movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1, range);
1809 		}
1810 
1811 		// "Remove new parent's nextSibling from its parent."
1812 		newParent.parentNode.removeChild(newParent.nextSibling);
1813 	}
1814 
1815 	// "Remove extraneous line breaks from new parent."
1816 	removeExtraneousLineBreaksFrom(newParent);
1817 
1818 	// "Return new parent."
1819 	return newParent;
1820 }
1821 
1822 
1823 //@}
1824 ///// Allowed children /////
1825 //@{
1826 
1827 // "A name of an element with inline contents is "a", "abbr", "b", "bdi",
1828 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i",
1829 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
1830 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike",
1831 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"."
1832 var namesOfElementsWithInlineContentsMap = {
1833 	"A": true, "ABBR": true, "B": true, "BDI": true, "BDO": true,
1834 	"CITE": true, "CODE": true, "DFN": true, "EM": true, "H1": true, "H2": true, "H3": true, "H4": true, "H5": true, "H6": true, "I": true,
1835 	"KBD": true, "MARK": true, "P": true, "PRE": true, "Q": true, "RP": true, "RT": true, "RUBY": true, "S": true, "SAMP": true, "SMALL": true,
1836 	"SPAN": true, "STRONG": true, "SUB": true, "SUP": true, "U": true, "VAR": true, "ACRONYM": true, "LISTING": true, "STRIKE": true,
1837 	"XMP": true, "BIG": true, "BLINK": true, "FONT": true, "MARQUEE": true, "NOBR": true, "TT": true
1838 };
1839 
1840 
1841 var tableRelatedElements = {
1842 	"colgroup": true,
1843 	"table": true,
1844 	"tbody": true,
1845 	"tfoot": true,
1846 	"thead": true,
1847 	"tr": true
1848 };
1849 
1850 var scriptRelatedElements = {
1851 	"script": true,
1852 	"style": true,
1853 	"plaintext": true,
1854 	"xmp": true
1855 };
1856 
1857 var prohibitedHeadingNestingMap = jQuery.extend({
1858 	"H1": true,
1859 	"H2": true,
1860 	"H3": true,
1861 	"H4": true,
1862 	"H5": true,
1863 	"H6": true
1864 }, prohibitedParagraphChildNamesMap);
1865 var prohibitedTableNestingMap = {
1866 	"CAPTION": true,
1867 	"COL": true,
1868 	"COLGROUP": true,
1869 	"TBODY": true,
1870 	"TD": true,
1871 	"TFOOT": true,
1872 	"TH": true,
1873 	"THEAD": true,
1874 	"TR": true
1875 };
1876 var prohibitedDefNestingMap = {
1877 	"DD": true,
1878 	"DT": true
1879 };
1880 var prohibitedNestingCombinationsMap = {
1881 	"A": jQuery.extend({"A": true}, prohibitedParagraphChildNamesMap),
1882 	"DD": prohibitedDefNestingMap,
1883 	"DT": prohibitedDefNestingMap,
1884 	"LI": {"LI": true},
1885 	"NOBR": jQuery.extend({"NOBR": true}, prohibitedParagraphChildNamesMap),
1886 	"H1": prohibitedHeadingNestingMap,
1887 	"H2": prohibitedHeadingNestingMap,
1888 	"H3": prohibitedHeadingNestingMap,
1889 	"H4": prohibitedHeadingNestingMap,
1890 	"H5": prohibitedHeadingNestingMap,
1891 	"H6": prohibitedHeadingNestingMap,
1892 	"TD": prohibitedTableNestingMap,
1893 	"TH": prohibitedTableNestingMap,
1894 	// this is the same as namesOfElementsWithInlineContentsMap excluding a and h1-h6 elements above
1895 	"ABBR": prohibitedParagraphChildNamesMap,
1896 	"B": prohibitedParagraphChildNamesMap,
1897 	"BDI": prohibitedParagraphChildNamesMap,
1898 	"BDO": prohibitedParagraphChildNamesMap,
1899 	"CITE": prohibitedParagraphChildNamesMap,
1900 	"CODE": prohibitedParagraphChildNamesMap,
1901 	"DFN": prohibitedParagraphChildNamesMap,
1902 	"EM": prohibitedParagraphChildNamesMap,
1903 	"I": prohibitedParagraphChildNamesMap,
1904 	"KBD": prohibitedParagraphChildNamesMap,
1905 	"MARK": prohibitedParagraphChildNamesMap,
1906 	"P": prohibitedParagraphChildNamesMap,
1907 	"PRE": prohibitedParagraphChildNamesMap,
1908 	"Q": prohibitedParagraphChildNamesMap,
1909 	"RP": prohibitedParagraphChildNamesMap,
1910 	"RT": prohibitedParagraphChildNamesMap,
1911 	"RUBY": prohibitedParagraphChildNamesMap,
1912 	"S": prohibitedParagraphChildNamesMap,
1913 	"SAMP": prohibitedParagraphChildNamesMap,
1914 	"SMALL": prohibitedParagraphChildNamesMap,
1915 	"SPAN": prohibitedParagraphChildNamesMap,
1916 	"STRONG": prohibitedParagraphChildNamesMap,
1917 	"SUB": prohibitedParagraphChildNamesMap,
1918 	"SUP": prohibitedParagraphChildNamesMap,
1919 	"U": prohibitedParagraphChildNamesMap,
1920 	"VAR": prohibitedParagraphChildNamesMap,
1921 	"ACRONYM": prohibitedParagraphChildNamesMap,
1922 	"LISTING": prohibitedParagraphChildNamesMap,
1923 	"STRIKE": prohibitedParagraphChildNamesMap,
1924 	"XMP": prohibitedParagraphChildNamesMap,
1925 	"BIG": prohibitedParagraphChildNamesMap,
1926 	"BLINK": prohibitedParagraphChildNamesMap,
1927 	"FONT": prohibitedParagraphChildNamesMap,
1928 	"MARQUEE": prohibitedParagraphChildNamesMap,
1929 	"TT": prohibitedParagraphChildNamesMap
1930 };
1931 
1932 // "An element with inline contents is an HTML element whose local name is a
1933 // name of an element with inline contents."
1934 function isElementWithInlineContents(node) {
1935 	return isMappedHtmlElement(node, namesOfElementsWithInlineContentsMap);
1936 }
1937 
1938 function isAllowedChild(child, parent_) {
1939 	// "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or
1940 	// an HTML element with local name equal to one of those, and child is a
1941 	// Text node whose data does not consist solely of space characters, return
1942 	// false."
1943 	if ((tableRelatedElements[parent_]
1944 	|| isHtmlElementInArray(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"]))
1945 	&& typeof child == "object"
1946 	&& child.nodeType == $_.Node.TEXT_NODE
1947 	&& !/^[ \t\n\f\r]*$/.test(child.data)) {
1948 		return false;
1949 1950 	}
1951 
1952 	// "If parent is "script", "style", "plaintext", or "xmp", or an HTML
1953 	// element with local name equal to one of those, and child is not a Text
1954 	// node, return false."
1955 	if ((scriptRelatedElements[parent_]
1956 	|| isHtmlElementInArray(parent_, ["script", "style", "plaintext", "xmp"]))
1957 	&& (typeof child != "object" || child.nodeType != $_.Node.TEXT_NODE)) {
1958 		return false;
1959 	}
1960 
1961 	// "If child is a Document, DocumentFragment, or DocumentType, return
1962 	// false."
1963 	if (typeof child == "object"
1964 	&& (child.nodeType == $_.Node.DOCUMENT_NODE
1965 	|| child.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE
1966 	|| child.nodeType == $_.Node.DOCUMENT_TYPE_NODE)) {
1967 		return false;
1968 	}
1969 
1970 	// "If child is an HTML element, set child to the local name of child."
1971 	if (isAnyHtmlElement(child)) {
1972 		child = child.tagName.toLowerCase();
1973 	}
1974 
1975 	// "If child is not a string, return true."
1976 	if (typeof child != "string") {
1977 		return true;
1978 	}
1979 
1980 	// "If parent is an HTML element:"
1981 	if (isAnyHtmlElement(parent_)) {
1982 		// "If child is "a", and parent or some ancestor of parent is an a,
1983 		// return false."
1984 		//
1985 		// "If child is a prohibited paragraph child name and parent or some
1986 		// ancestor of parent is an element with inline contents, return
1987 		// false."
1988 		//
1989 		// "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or
1990 		// some ancestor of parent is an HTML element with local name "h1",
1991 		// "h2", "h3", "h4", "h5", or "h6", return false."
1992 		var ancestor = parent_;
1993 		while (ancestor) {
1994 			if (child == "a" && isNamedHtmlElement(ancestor, 'a')) {
1995 				return false;
1996 1997 			}
1998 			if (prohibitedParagraphChildNamesMap[child.toUpperCase()]
1999 			&& isElementWithInlineContents(ancestor)) {
2000 				return false;
2001 			}
2002 			if (/^h[1-6]$/.test(child)
2003 			&& isAnyHtmlElement(ancestor)
2004 			&& /^H[1-6]$/.test(ancestor.tagName)) {
2005 				return false;
2006 			}
2007 			ancestor = ancestor.parentNode;
2008 		}
2009 
2010 		// "Let parent be the local name of parent."
2011 		parent_ = parent_.tagName.toLowerCase();
2012 	}
2013 
2014 	// "If parent is an Element or DocumentFragment, return true."
2015 	if (typeof parent_ == "object"
2016 	&& (parent_.nodeType == $_.Node.ELEMENT_NODE
2017 	|| parent_.nodeType == $_.Node.DOCUMENT_FRAGMENT_NODE)) {
2018 		return true;
2019 	}
2020 
2021 	// "If parent is not a string, return false."
2022 	if (typeof parent_ != "string") {
2023 		return false;
2024 	}
2025 
2026 	// "If parent is on the left-hand side of an entry on the following list,
2027 	// then return true if child is listed on the right-hand side of that
2028 	// entry, and false otherwise."
2029 	switch (parent_) {
2030 		case "colgroup":
2031 			return child == "col";
2032 		case "table":
2033 			return jQuery.inArray(child, ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]) != -1;
2034 		case "tbody":
2035 		case "thead":
2036 		case "tfoot":
2037 			return jQuery.inArray(child, ["td", "th", "tr"]) != -1;
2038 		case "tr":
2039 			return jQuery.inArray(child, ["td", "th"]) != -1;
2040 		case "dl":
2041 			return jQuery.inArray(child, ["dt", "dd"]) != -1;
2042 		case "dir":
2043 		case "ol":
2044 		case "ul":
2045 			return jQuery.inArray(child, ["dir", "li", "ol", "ul"]) != -1;
2046 		case "hgroup":
2047 			return /^h[1-6]$/.test(child);
2048 	}
2049 
2050 	// "If child is "body", "caption", "col", "colgroup", "frame", "frameset",
2051 	// "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return
2052 	// false."
2053 	if (jQuery.inArray(child, ["body", "caption", "col", "colgroup", "frame", "frameset", "head",
2054 	"html", "tbody", "td", "tfoot", "th", "thead", "tr"]) != -1) {
2055 		return false;
2056 	}
2057 
2058 	// "If child is "dd" or "dt" and parent is not "dl", return false."
2059 	if (jQuery.inArray(child, ["dd", "dt"]) != -1
2060 	&& parent_ != "dl") {
2061 		return false;
2062 	}
2063 
2064 	// "If child is "li" and parent is not "ol" or "ul", return false."
2065 	if (child == "li"
2066 	&& parent_ != "ol"
2067 	&& parent_ != "ul") {
2068 		return false;
2069 	}
2070 
2071 	// "If parent is on the left-hand side of an entry on the following list
2072 	// and child is listed on the right-hand side of that entry, return false."
2073 	var leftSide = prohibitedNestingCombinationsMap[parent_.toUpperCase()];
2074 	if (leftSide) {
2075 		var rightSide = leftSide[child.toUpperCase()];
2076 		if (rightSide) {
2077 			return false;
2078 		}
2079 	}
2080 
2081 	// "Return true."
2082 	return true;
2083 }
2084 
2085 
2086 //@}
2087 
2088 //////////////////////////////////////
2089 ///// Inline formatting commands /////
2090 //////////////////////////////////////
2091 
2092 ///// Inline formatting command definitions /////
2093 //@{
2094 
2095 // "A node node is effectively contained in a range range if range is not
2096 // collapsed, and at least one of the following holds:"
2097 function isEffectivelyContained(node, range) {
2098 	if (range.collapsed) {
2099 		return false;
2100 	}
2101 
2102 	// "node is contained in range."
2103 	if (isContained(node, range)) {
2104 		return true;
2105 	}
2106 
2107 	// "node is range's start node, it is a Text node, and its length is
2108 	// different from range's start offset."
2109 	if (node == range.startContainer
2110 	&& node.nodeType == $_.Node.TEXT_NODE
2111 	&& getNodeLength(node) != range.startOffset) {
2112 		return true;
2113 	}
2114 
2115 	// "node is range's end node, it is a Text node, and range's end offset is
2116 	// not 0."
2117 	if (node == range.endContainer
2118 	&& node.nodeType == $_.Node.TEXT_NODE
2119 	&& range.endOffset != 0) {
2120 		return true;
2121 	}
2122 
2123 	// "node has at least one child; and all its children are effectively
2124 	// contained in range; and either range's start node is not a descendant of
2125 	// node or is not a Text node or range's start offset is zero; and either
2126 	// range's end node is not a descendant of node or is not a Text node or
2127 	// range's end offset is its end node's length."
2128 	if (node.hasChildNodes()
2129 	&& $_(node.childNodes).every(function(child) { return isEffectivelyContained(child, range) })
2130 	&& (!isDescendant(range.startContainer, node)
2131 	|| range.startContainer.nodeType != $_.Node.TEXT_NODE
2132 	|| range.startOffset == 0)
2133 	&& (!isDescendant(range.endContainer, node)
2134 	|| range.endContainer.nodeType != $_.Node.TEXT_NODE
2135 	|| range.endOffset == getNodeLength(range.endContainer))) {
2136 		return true;
2137 	}
2138 
2139 	return false;
2140 }
2141 
2142 // Like get(All)ContainedNodes(), but for effectively contained nodes.
2143 function getEffectivelyContainedNodes(range, condition) {
2144 	if (typeof condition == "undefined") {
2145 		condition = function() { return true };
2146 	}
2147 	var node = range.startContainer;
2148 	while (isEffectivelyContained(node.parentNode, range)) {
2149 		node = node.parentNode;
2150 	}
2151 
2152 	var stop = nextNodeDescendants(range.endContainer);
2153 
2154 	var nodeList = [];
2155 	while (isBefore(node, stop)) {
2156 		if (isEffectivelyContained(node, range)
2157 		&& condition(node)) {
2158 			nodeList.push(node);
2159 			node = nextNodeDescendants(node);
2160 			continue;
2161 		}
2162 		node = nextNode(node);
2163 	}
2164 	return nodeList;
2165 }
2166 
2167 function getAllEffectivelyContainedNodes(range, condition) {
2168 	if (typeof condition == "undefined") {
2169 		condition = function() { return true };
2170 	}
2171 	var node = range.startContainer;
2172 	while (isEffectivelyContained(node.parentNode, range)) {
2173 		node = node.parentNode;
2174 	}
2175 
2176 	var stop = nextNodeDescendants(range.endContainer);
2177 
2178 	var nodeList = [];
2179 	while (isBefore(node, stop)) {
2180 		if (isEffectivelyContained(node, range)
2181 		&& condition(node)) {
2182 			nodeList.push(node);
2183 		}
2184 		node = nextNode(node);
2185 	}
2186 	return nodeList;
2187 }
2188 
2189 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element
2190 // with no attributes except possibly style; or a font element with no
2191 // attributes except possibly style, color, face, and/or size; or an a element
2192 // with no attributes except possibly style and/or href."
2193 function isModifiableElement(node) {
2194 	if (!isAnyHtmlElement(node)) {
2195 		return false;
2196 	}
2197 
2198 	if (jQuery.inArray(node.tagName, ["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"]) != -1) {
2199 		if (node.attributes.length == 0) {
2200 			return true;
2201 		}
2202 
2203 		if (node.attributes.length == 1
2204 		&& hasAttribute(node, "style")) {
2205 			return true;
2206 		}
2207 	}
2208 
2209 	if (node.tagName == "FONT" || node.tagName == "A") {
2210 		var numAttrs = node.attributes.length;
2211 
2212 		if (hasAttribute(node, "style")) {
2213 			numAttrs--;
2214 		}
2215 
2216 		if (node.tagName == "FONT") {
2217 			if (hasAttribute(node, "color")) {
2218 				numAttrs--;
2219 			}
2220 
2221 			if (hasAttribute(node, "face")) {
2222 				numAttrs--;
2223 			}
2224 
2225 			if (hasAttribute(node, "size")) {
2226 				numAttrs--;
2227 			}
2228 		}
2229 
2230 		if (node.tagName == "A"
2231 		&& hasAttribute(node, "href")) {
2232 			numAttrs--;
2233 		}
2234 
2235 		if (numAttrs == 0) {
2236 			return true;
2237 		}
2238 	}
2239 
2240 	return false;
2241 }
2242 
2243 function isSimpleModifiableElement(node) {
2244 	// "A simple modifiable element is an HTML element for which at least one
2245 	// of the following holds:"
2246 	if (!isAnyHtmlElement(node)) {
2247 		return false;
2248 	}
2249 
2250 	// Only these elements can possibly be a simple modifiable element.
2251 	if (jQuery.inArray(node.tagName, ["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"]) == -1) {
2252 		return false;
2253 	}
2254 
2255 	// "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
2256 	// element with no attributes."
2257 	if (node.attributes.length == 0) {
2258 		return true;
2259 	}
2260 
2261 	// If it's got more than one attribute, everything after this fails.
2262 	if (node.attributes.length > 1) {
2263 		return false;
2264 	}
2265 
2266 	// "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u
2267 	// element with exactly one attribute, which is style, which sets no CSS
2268 	// properties (including invalid or unrecognized properties)."
2269 	//
2270 	// Not gonna try for invalid or unrecognized.
2271 	if (hasAttribute(node, "style")
2272 	&& getStyleLength(node) == 0) {
2273 		return true;
2274 	}
2275 
2276 	// "It is an a element with exactly one attribute, which is href."
2277 	if (node.tagName == "A"
2278 	&& hasAttribute(node, "href")) {
2279 		return true;
2280 	}
2281 
2282 	// "It is a font element with exactly one attribute, which is either color,
2283 	// face, or size."
2284 	if (node.tagName == "FONT"
2285 	&& (hasAttribute(node, "color")
2286 	|| hasAttribute(node, "face")
2287 	|| hasAttribute(node, "size")
2288 	)) {
2289 		return true;
2290 	}
2291 
2292 	// "It is a b or strong element with exactly one attribute, which is style,
2293 	// and the style attribute sets exactly one CSS property (including invalid
2294 	// or unrecognized properties), which is "font-weight"."
2295 	if ((node.tagName == "B" || node.tagName == "STRONG")
2296 	&& hasAttribute(node, "style")
2297 	&& getStyleLength(node) == 1
2298 	&& node.style.fontWeight != "") {
2299 		return true;
2300 	}
2301 
2302 	// "It is an i or em element with exactly one attribute, which is style,
2303 	// and the style attribute sets exactly one CSS property (including invalid
2304 	// or unrecognized properties), which is "font-style"."
2305 	if ((node.tagName == "I" || node.tagName == "EM")
2306 	&& hasAttribute(node, "style")
2307 	&& getStyleLength(node) == 1
2308 	&& node.style.fontStyle != "") {
2309 		return true;
2310 	}
2311 
2312 	// "It is an a, font, or span element with exactly one attribute, which is
2313 	// style, and the style attribute sets exactly one CSS property (including
2314 	// invalid or unrecognized properties), and that property is not
2315 	// "text-decoration"."
2316 	if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN")
2317 	&& hasAttribute(node, "style")
2318 	&& getStyleLength(node) == 1
2319 	&& node.style.textDecoration == "") {
2320 		return true;
2321 	}
2322 
2323 	// "It is an a, font, s, span, strike, or u element with exactly one
2324 	// attribute, which is style, and the style attribute sets exactly one CSS
2325 	// property (including invalid or unrecognized properties), which is
2326 	// "text-decoration", which is set to "line-through" or "underline" or
2327 	// "overline" or "none"."
2328 	if (jQuery.inArray(node.tagName, ["A", "FONT", "S", "SPAN", "STRIKE", "U"]) != -1
2329 	&& hasAttribute(node, "style")
2330 	&& getStyleLength(node) == 1
2331 	&& (node.style.textDecoration == "line-through"
2332 	|| node.style.textDecoration == "underline"
2333 	|| node.style.textDecoration == "overline"
2334 	|| node.style.textDecoration == "none")) {
2335 		return true;
2336 	}
2337 
2338 	return false;
2339 }
2340 
2341 // "Two quantities are equivalent values for a command if either both are null,
2342 // or both are strings and they're equal and the command does not define any
2343 // equivalent values, or both are strings and the command defines equivalent
2344 // values and they match the definition."
2345 function areEquivalentValues(command, val1, val2) {
2346 	if (val1 === null && val2 === null) {
2347 		return true;
2348 	}
2349 
2350 	if (typeof val1 == "string"
2351 	&& typeof val2 == "string"
2352 	&& val1 == val2
2353 	&& !("equivalentValues" in commands[command])) {
2354 		return true;
2355 	}
2356 
2357 	if (typeof val1 == "string"
2358 	&& typeof val2 == "string"
2359 	&& "equivalentValues" in commands[command]
2360 	&& commands[command].equivalentValues(val1, val2)) {
2361 		return true;
2362 	}
2363 
2364 	return false;
2365 }
2366 
2367 // "Two quantities are loosely equivalent values for a command if either they
2368 // are equivalent values for the command, or if the command is the fontSize
2369 // command; one of the quantities is one of "xx-small", "small", "medium",
2370 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
2371 // the resolved value of "font-size" on a font element whose size attribute has
2372 // the corresponding value set ("1" through "7" respectively)."
2373 function areLooselyEquivalentValues(command, val1, val2) {
2374 	if (areEquivalentValues(command, val1, val2)) {
2375 		return true;
2376 	}
2377 
2378 	if (command != "fontsize"
2379 	|| typeof val1 != "string"
2380 	|| typeof val2 != "string") {
2381 		return false;
2382 	}
2383 
2384 	// Static variables in JavaScript?
2385 	var callee = areLooselyEquivalentValues;
2386 	if (callee.sizeMap === undefined) {
2387 		callee.sizeMap = {};
2388 		var font = document.createElement("font");
2389 		document.body.appendChild(font);
2390 		$_( ["xx-small", "small", "medium", "large", "x-large", "xx-large",
2391 		"xxx-large"] ).forEach(function(keyword) {
2392 			font.size = cssSizeToLegacy(keyword);
2393 			callee.sizeMap[keyword] = $_.getComputedStyle(font).fontSize;
2394 		});
2395 		document.body.removeChild(font);
2396 	}
2397 
2398 	return val1 === callee.sizeMap[val2]
2399 		|| val2 === callee.sizeMap[val1];
2400 }
2401 
2402 //@}
2403 ///// Assorted inline formatting command algorithms /////
2404 //@{
2405 
2406 function getEffectiveCommandValue(node, command) {
2407 	// "If neither node nor its parent is an Element, return null."
2408 	if (node.nodeType != $_.Node.ELEMENT_NODE
2409 	&& (!node.parentNode || node.parentNode.nodeType != $_.Node.ELEMENT_NODE)) {
2410 		return null;
2411 	}
2412 
2413 	// "If node is not an Element, return the effective command value of its
2414 	// parent for command."
2415 	if (node.nodeType != $_.Node.ELEMENT_NODE) {
2416 		return getEffectiveCommandValue(node.parentNode, command);
2417 	}
2418 
2419 	// "If command is "createLink" or "unlink":"
2420 	if (command == "createlink" || command == "unlink") {
2421 		// "While node is not null, and is not an a element that has an href
2422 		// attribute, set node to its parent."
2423 		while (node
2424 		&& (!isAnyHtmlElement(node)
2425 		|| node.tagName != "A"
2426 		|| !hasAttribute(node, "href"))) {
2427 			node = node.parentNode;
2428 		}
2429 
2430 		// "If node is null, return null."
2431 		if (!node) {
2432 			return null;
2433 		}
2434 
2435 		// "Return the value of node's href attribute."
2436 		return node.getAttribute("href");
2437 	}
2438 
2439 	// "If command is "backColor" or "hiliteColor":"
2440 	if (command == "backcolor"
2441 	|| command == "hilitecolor") {
2442 		// "While the resolved value of "background-color" on node is any
2443 		// fully transparent value, and node's parent is an Element, set
2444 		// node to its parent."
2445 		//
2446 		// Another lame hack to avoid flawed APIs.
2447 		while (($_.getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
2448 		|| $_.getComputedStyle(node).backgroundColor === ""
2449 		|| $_.getComputedStyle(node).backgroundColor == "transparent")
2450 		&& node.parentNode
2451 		&& node.parentNode.nodeType == $_.Node.ELEMENT_NODE) {
2452 			node = node.parentNode;
2453 		}
2454 
2455 		// "If the resolved value of "background-color" on node is a fully
2456 		// transparent value, return "rgb(255, 255, 255)"."
2457 		if ($_.getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)"
2458         || $_.getComputedStyle(node).backgroundColor === ""
2459         || $_.getComputedStyle(node).backgroundColor == "transparent") {
2460 			return "rgb(255, 255, 255)";
2461 		}
2462 
2463 		// "Otherwise, return the resolved value of "background-color" for
2464 		// node."
2465 		return $_.getComputedStyle(node).backgroundColor;
2466 	}
2467 
2468 	// "If command is "subscript" or "superscript":"
2469 	if (command == "subscript" || command == "superscript") {
2470 		// "Let affected by subscript and affected by superscript be two
2471 		// boolean variables, both initially false."
2472 		var affectedBySubscript = false;
2473 		var affectedBySuperscript = false;
2474 
2475 		// "While node is an inline node:"
2476 		while (isInlineNode(node)) {
2477 			var verticalAlign = $_.getComputedStyle(node).verticalAlign;
2478 
2479 			// "If node is a sub, set affected by subscript to true."
2480 			if (isNamedHtmlElement(node, 'sub')) {
2481 				affectedBySubscript = true;
2482 			// "Otherwise, if node is a sup, set affected by superscript to
2483 			// true."
2484 			} else if (isNamedHtmlElement(node, 'sup')) {
2485 				affectedBySuperscript = true;
2486 			}
2487 
2488 			// "Set node to its parent."
2489 			node = node.parentNode;
2490 		}
2491 
2492 		// "If affected by subscript and affected by superscript are both true,
2493 		// return the string "mixed"."
2494 		if (affectedBySubscript && affectedBySuperscript) {
2495 			return "mixed";
2496 		}
2497 
2498 		// "If affected by subscript is true, return "subscript"."
2499 		if (affectedBySubscript) {
2500 			return "subscript";
2501 		}
2502 
2503 		// "If affected by superscript is true, return "superscript"."
2504 		if (affectedBySuperscript) {
2505 			return "superscript";
2506 		}
2507 
2508 		// "Return null."
2509 		return null;
2510 	}
2511 
2512 	// "If command is "strikethrough", and the "text-decoration" property of
2513 	// node or any of its ancestors has resolved value containing
2514 	// "line-through", return "line-through". Otherwise, return null."
2515 	if (command == "strikethrough") {
2516 		do {
2517 			if ($_.getComputedStyle(node).textDecoration.indexOf("line-through") != -1) {
2518 				return "line-through";
2519 			}
2520 			node = node.parentNode;
2521 		} while (node && node.nodeType == $_.Node.ELEMENT_NODE);
2522 		return null;
2523 	}
2524 
2525 	// "If command is "underline", and the "text-decoration" property of node
2526 	// or any of its ancestors has resolved value containing "underline",
2527 	// return "underline". Otherwise, return null."
2528 	if (command == "underline") {
2529 		do {
2530 			if ($_.getComputedStyle(node).textDecoration.indexOf("underline") != -1) {
2531 				return "underline";
2532 			}
2533 			node = node.parentNode;
2534 		} while (node && node.nodeType == $_.Node.ELEMENT_NODE);
2535 		return null;
2536 	}
2537 
2538 	if (!("relevantCssProperty" in commands[command])) {
2539 		throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue";
2540 	}
2541 
2542 	// "Return the resolved value for node of the relevant CSS property for
2543 	// command."
2544 	return $_.getComputedStyle(node)[commands[command].relevantCssProperty].toString();
2545 }
2546 
2547 function getSpecifiedCommandValue(element, command) {
2548 	// "If command is "backColor" or "hiliteColor" and element's display
2549 	// property does not have resolved value "inline", return null."
2550 	if ((command == "backcolor" || command == "hilitecolor")
2551 	&& $_.getComputedStyle(element).display != "inline") {
2552 		return null;
2553 	}
2554 
2555 	// "If command is "createLink" or "unlink":"
2556 	if (command == "createlink" || command == "unlink") {
2557 		// "If element is an a element and has an href attribute, return the
2558 		// value of that attribute."
2559 		if (isAnyHtmlElement(element)
2560 		&& element.tagName == "A"
2561 		&& hasAttribute(element, "href")) {
2562 			return element.getAttribute("href");
2563 		}
2564 
2565 		// "Return null."
2566 		return null;
2567 	}
2568 
2569 	// "If command is "subscript" or "superscript":"
2570 	if (command == "subscript" || command == "superscript") {
2571 		// "If element is a sup, return "superscript"."
2572 		if (isNamedHtmlElement(element, 'sup')) {
2573 			return "superscript";
2574 		}
2575 
2576 		// "If element is a sub, return "subscript"."
2577 		if (isNamedHtmlElement(element, 'sub')) {
2578 			return "subscript";
2579 		}
2580 
2581 		// "Return null."
2582 		return null;
2583 	}
2584 
2585 	// "If command is "strikethrough", and element has a style attribute set,
2586 	// and that attribute sets "text-decoration":"
2587 	if (command == "strikethrough"
2588 	&& element.style.textDecoration != "") {
2589 		// "If element's style attribute sets "text-decoration" to a value
2590 		// containing "line-through", return "line-through"."
2591 		if (element.style.textDecoration.indexOf("line-through") != -1) {
2592 			return "line-through";
2593 		}
2594 
2595 		// "Return null."
2596 		return null;
2597 	}
2598 
2599 	// "If command is "strikethrough" and element is a s or strike element,
2600 	// return "line-through"."
2601 	if (command == "strikethrough"
2602 	&& isHtmlElementInArray(element, ["S", "STRIKE"])) {
2603 		return "line-through";
2604 	}
2605 
2606 	// "If command is "underline", and element has a style attribute set, and
2607 	// that attribute sets "text-decoration":"
2608 	if (command == "underline"
2609 	&& element.style.textDecoration != "") {
2610 		// "If element's style attribute sets "text-decoration" to a value
2611 		// containing "underline", return "underline"."
2612 		if (element.style.textDecoration.indexOf("underline") != -1) {
2613 			return "underline";
2614 		}
2615 
2616 		// "Return null."
2617 		return null;
2618 	}
2619 
2620 	// "If command is "underline" and element is a u element, return
2621 	// "underline"."
2622 	if (command == "underline"
2623 	&& isNamedHtmlElement(element, 'U')) {
2624 		return "underline";
2625 	}
2626 
2627 	// "Let property be the relevant CSS property for command."
2628 	var property = commands[command].relevantCssProperty;
2629 
2630 	// "If property is null, return null."
2631 	if (property === null) {
2632 		return null;
2633 	}
2634 
2635 	// "If element has a style attribute set, and that attribute has the
2636 	// effect of setting property, return the value that it sets property to."
2637 	if (element.style[property] != "") {
2638 		return element.style[property];
2639 	}
2640 
2641 	// "If element is a font element that has an attribute whose effect is
2642 	// to create a presentational hint for property, return the value that the
2643 	// hint sets property to.  (For a size of 7, this will be the non-CSS value
2644 	// "xxx-large".)"
2645 	if (isHtmlNamespace(element.namespaceURI)
2646 	&& element.tagName == "FONT") {
2647 		if (property == "color" && hasAttribute(element, "color")) {
2648 			return element.color;
2649 		}
2650 		if (property == "fontFamily" && hasAttribute(element, "face")) {
2651 			return element.face;
2652 		}
2653 		if (property == "fontSize" && hasAttribute(element, "size")) {
2654 			// This is not even close to correct in general.
2655 			var size = parseInt(element.size);
2656 			if (size < 1) {
2657 				size = 1;
2658 			}
2659 			if (size > 7) {
2660 				size = 7;
2661 			}
2662 			return {
2663 				1: "xx-small",
2664 				2: "small",
2665 				3: "medium",
2666 				4: "large",
2667 				5: "x-large",
2668 				6: "xx-large",
2669 				7: "xxx-large"
2670 			}[size];
2671 		}
2672 	}
2673 
2674 	// "If element is in the following list, and property is equal to the
2675 	// CSS property name listed for it, return the string listed for it."
2676 	//
2677 	// A list follows, whose meaning is copied here.
2678 	if (property == "fontWeight"
2679 	&& (element.tagName == "B" || element.tagName == "STRONG")) {
2680 		return "bold";
2681 	}
2682 	if (property == "fontStyle"
2683 	&& (element.tagName == "I" || element.tagName == "EM")) {
2684 		return "italic";
2685 	}
2686 
2687 	// "Return null."
2688 	return null;
2689 }
2690 
2691 function reorderModifiableDescendants(node, command, newValue, range) {
2692 	// "Let candidate equal node."
2693 	var candidate = node;
2694 
2695 	// "While candidate is a modifiable element, and candidate has exactly one
2696 	// child, and that child is also a modifiable element, and candidate is not
2697 	// a simple modifiable element or candidate's specified command value for
2698 	// command is not equivalent to new value, set candidate to its child."
2699 	while (isModifiableElement(candidate)
2700 	&& candidate.childNodes.length == 1
2701 	&& isModifiableElement(candidate.firstChild)
2702 	&& (!isSimpleModifiableElement(candidate)
2703 	|| !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
2704 		candidate = candidate.firstChild;
2705 	}
2706 
2707 	// "If candidate is node, or is not a simple modifiable element, or its
2708 	// specified command value is not equivalent to new value, or its effective
2709 	// command value is not loosely equivalent to new value, abort these
2710 	// steps."
2711 	if (candidate == node
2712 	|| !isSimpleModifiableElement(candidate)
2713 	|| !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
2714 	|| !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
2715 		return;
2716 	}
2717 
2718 	// "While candidate has children, insert the first child of candidate into
2719 	// candidate's parent immediately before candidate, preserving ranges."
2720 	while (candidate.hasChildNodes()) {
2721 		movePreservingRanges(candidate.firstChild, candidate.parentNode, getNodeIndex(candidate), range);
2722 	}
2723 
2724 	// "Insert candidate into node's parent immediately after node."
2725 	node.parentNode.insertBefore(candidate, node.nextSibling);
2726 
2727 	// "Append the node as the last child of candidate, preserving ranges."
2728 	movePreservingRanges(node, candidate, -1, range);
2729 }
2730 
2731 var recordValuesCommands = [
2732 	"subscript", "bold", "fontname", "fontsize", "forecolor",
2733 	"hilitecolor", "italic", "strikethrough", "underline"
2734 ];
2735 
2736 function recordValues(nodeList) {
2737 	// "Let values be a list of (node, command, specified command value)
2738 	// triples, initially empty."
2739 	var values = [];
2740 
2741 	// "For each node in node list, for each command in the list "subscript",
2742 	// "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic",
2743 	// "strikethrough", and "underline" in that order:"
2744 
2745 	// Ensure we have a plain array to avoid the potential performance
2746 	// overhead of a NodeList
2747 	var nodes = jQuery.makeArray(nodeList);
2748 	for (var i = 0; i < nodes.length; i++) {
2749 		var node = nodes[i];
2750 		for (var j = 0; j < recordValuesCommands.length; j++) {
2751 			var command = recordValuesCommands[j];
2752 
2753 			// "Let ancestor equal node."
2754 			var ancestor = node;
2755 
2756 			// "If ancestor is not an Element, set it to its parent."
2757 			if (ancestor.nodeType != 1) {
2758 				ancestor = ancestor.parentNode;
2759 			}
2760 
2761 			// "While ancestor is an Element and its specified command value
2762 			// for command is null, set it to its parent."
2763 			var specifiedCommandValue = null;
2764 			while (ancestor
2765 			&& ancestor.nodeType == 1
2766 			&& (specifiedCommandValue = getSpecifiedCommandValue(ancestor, command)) === null) {
2767 				ancestor = ancestor.parentNode;
2768 			}
2769 
2770 			// "If ancestor is an Element, add (node, command, ancestor's
2771 			// specified command value for command) to values. Otherwise add
2772 			// (node, command, null) to values."
2773 			values.push([node, command, specifiedCommandValue]);
2774 		}
2775 	}
2776 
2777 	// "Return values."
2778 	return values;
2779 }
2780 
2781 function restoreValues(values, range) {
2782 	// "For each (node, command, value) triple in values:"
2783 	$_( values ).forEach(function(triple) {
2784 		var node = triple[0];
2785 		var command = triple[1];
2786 		var value = triple[2];
2787 
2788 		// "Let ancestor equal node."
2789 		var ancestor = node;
2790 
2791 		// "If ancestor is not an Element, set it to its parent."
2792 		if (!ancestor || ancestor.nodeType != $_.Node.ELEMENT_NODE) {
2793 			ancestor = ancestor.parentNode;
2794 		}
2795 
2796 		// "While ancestor is an Element and its specified command value for
2797 		// command is null, set it to its parent."
2798 		while (ancestor
2799 		&& ancestor.nodeType == $_.Node.ELEMENT_NODE
2800 		&& getSpecifiedCommandValue(ancestor, command) === null) {
2801 			ancestor = ancestor.parentNode;
2802 		}
2803 
2804 		// "If value is null and ancestor is an Element, push down values on
2805 		// node for command, with new value null."
2806 		if (value === null
2807 		&& ancestor
2808 		&& ancestor.nodeType == $_.Node.ELEMENT_NODE) {
2809 			pushDownValues(node, command, null, range);
2810 
2811 		// "Otherwise, if ancestor is an Element and its specified command
2812 		// value for command is not equivalent to value, or if ancestor is not
2813 		// an Element and value is not null, force the value of command to
2814 		// value on node."
2815 		} else if ((ancestor
2816 		&& ancestor.nodeType == $_.Node.ELEMENT_NODE
2817 		&& !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
2818 		|| ((!ancestor || ancestor.nodeType != $_.Node.ELEMENT_NODE)
2819 		&& value !== null)) {
2820 			forceValue(node, command, value, range);
2821 		}
2822 	});
2823 }
2824 
2825 
2826 //@}
2827 ///// Clearing an element's value /////
2828 //@{
2829 
2830 function clearValue(element, command, range) {
2831 	// "If element is not editable, return the empty list."
2832 	if (!isEditable(element)) {
2833 		return [];
2834 	}
2835 
2836 	// "If element's specified command value for command is null, return the
2837 	// empty list."
2838 	if (getSpecifiedCommandValue(element, command) === null) {
2839 		return [];
2840 	}
2841 
2842 	// "If element is a simple modifiable element:"
2843 	if (isSimpleModifiableElement(element)) {
2844 		// "Let children be the children of element."
2845 		var children = Array.prototype.slice.call(toArray(element.childNodes));
2846 
2847 		// "For each child in children, insert child into element's parent
2848 		// immediately before element, preserving ranges."
2849 		for (var i = 0; i < children.length; i++) {
2850 			movePreservingRanges(children[i], element.parentNode, getNodeIndex(element), range);
2851 		}
2852 
2853 		// "Remove element from its parent."
2854 		element.parentNode.removeChild(element);
2855 
2856 		// "Return children."
2857 		return children;
2858 	}
2859 
2860 	// "If command is "strikethrough", and element has a style attribute that
2861 	// sets "text-decoration" to some value containing "line-through", delete
2862 	// "line-through" from the value."
2863 	if (command == "strikethrough"
2864 	&& element.style.textDecoration.indexOf("line-through") != -1) {
2865 		if (element.style.textDecoration == "line-through") {
2866 			element.style.textDecoration = "";
2867 		} else {
2868 			element.style.textDecoration = element.style.textDecoration.replace("line-through", "");
2869 		}
2870 		if (element.getAttribute("style") == "") {
2871 			element.removeAttribute("style");
2872 		}
2873 	}
2874 
2875 	// "If command is "underline", and element has a style attribute that sets
2876 	// "text-decoration" to some value containing "underline", delete
2877 	// "underline" from the value."
2878 	if (command == "underline"
2879 	&& element.style.textDecoration.indexOf("underline") != -1) {
2880 		if (element.style.textDecoration == "underline") {
2881 			element.style.textDecoration = "";
2882 		} else {
2883 			element.style.textDecoration = element.style.textDecoration.replace("underline", "");
2884 		}
2885 		if (element.getAttribute("style") == "") {
2886 			element.removeAttribute("style");
2887 		}
2888 	}
2889 
2890 	// "If the relevant CSS property for command is not null, unset the CSS
2891 	// property property of element."
2892 	if (commands[command].relevantCssProperty !== null) {
2893 		element.style[commands[command].relevantCssProperty] = '';
2894 		if (element.getAttribute("style") == "") {
2895 			element.removeAttribute("style");
2896 		}
2897 	}
2898 
2899 	// "If element is a font element:"
2900 	if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") {
2901 		// "If command is "foreColor", unset element's color attribute, if set."
2902 		if (command == "forecolor") {
2903 			element.removeAttribute("color");
2904 		}
2905 
2906 		// "If command is "fontName", unset element's face attribute, if set."
2907 		if (command == "fontname") {
2908 			element.removeAttribute("face");
2909 2910 		}
2911 
2912 		// "If command is "fontSize", unset element's size attribute, if set."
2913 		if (command == "fontsize") {
2914 			element.removeAttribute("size");
2915 		}
2916 	}
2917 
2918 	// "If element is an a element and command is "createLink" or "unlink",
2919 	// unset the href property of element."
2920 	if (isNamedHtmlElement(element, 'A')
2921 	&& (command == "createlink" || command == "unlink")) {
2922 		element.removeAttribute("href");
2923 	}
2924 
2925 	// "If element's specified command value for command is null, return the
2926 	// empty list."
2927 	if (getSpecifiedCommandValue(element, command) === null) {
2928 		return [];
2929 	}
2930 
2931 	// "Set the tag name of element to "span", and return the one-node list
2932 	// consisting of the result."
2933 	return [setTagName(element, "span", range)];
2934 }
2935 
2936 
2937 //@}
2938 ///// Pushing down values /////
2939 //@{
2940 
2941 function pushDownValues(node, command, newValue, range) {
2942 	// "If node's parent is not an Element, abort this algorithm."
2943 	if (!node.parentNode
2944 	|| node.parentNode.nodeType != $_.Node.ELEMENT_NODE) {
2945 		return;
2946 	}
2947 
2948 	// "If the effective command value of command is loosely equivalent to new
2949 	// value on node, abort this algorithm."
2950 	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
2951 		return;
2952 	}
2953 
2954 	// "Let current ancestor be node's parent."
2955 	var currentAncestor = node.parentNode;
2956 
2957 	// "Let ancestor list be a list of Nodes, initially empty."
2958 	var ancestorList = [];
2959 
2960 	// "While current ancestor is an editable Element and the effective command
2961 	// value of command is not loosely equivalent to new value on it, append
2962 	// current ancestor to ancestor list, then set current ancestor to its
2963 	// parent."
2964 	while (isEditable(currentAncestor)
2965 	&& currentAncestor.nodeType == $_.Node.ELEMENT_NODE
2966 	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
2967 		ancestorList.push(currentAncestor);
2968 		currentAncestor = currentAncestor.parentNode;
2969 	}
2970 
2971 	// "If ancestor list is empty, abort this algorithm."
2972 	if (!ancestorList.length) {
2973 		return;
2974 	}
2975 
2976 	// "Let propagated value be the specified command value of command on the
2977 	// last member of ancestor list."
2978 	var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command);
2979 
2980 	// "If propagated value is null and is not equal to new value, abort this
2981 	// algorithm."
2982 	if (propagatedValue === null && propagatedValue != newValue) {
2983 		return;
2984 2985 	}
2986 
2987 	// "If the effective command value for the parent of the last member of
2988 	// ancestor list is not loosely equivalent to new value, and new value is
2989 	// not null, abort this algorithm."
2990 	if (newValue !== null
2991 	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
2992 		return;
2993 	}
2994 
2995 	// "While ancestor list is not empty:"
2996 	while (ancestorList.length) {
2997 		// "Let current ancestor be the last member of ancestor list."
2998 		// "Remove the last member from ancestor list."
2999 		var currentAncestor = ancestorList.pop();
3000 
3001 		// "If the specified command value of current ancestor for command is
3002 		// not null, set propagated value to that value."
3003 		if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
3004 			propagatedValue = getSpecifiedCommandValue(currentAncestor, command);
3005 		}
3006 
3007 		// "Let children be the children of current ancestor."
3008 		var children = Array.prototype.slice.call(toArray(currentAncestor.childNodes));
3009 
3010 		// "If the specified command value of current ancestor for command is
3011 		// not null, clear the value of current ancestor."
3012 		if (getSpecifiedCommandValue(currentAncestor, command) !== null) {
3013 			clearValue(currentAncestor, command, range);
3014 		}
3015 
3016 		// "For every child in children:"
3017 		for (var i = 0; i < children.length; i++) {
3018 			var child = children[i];
3019 
3020 			// "If child is node, continue with the next child."
3021 			if (child == node) {
3022 				continue;
3023 			}
3024 
3025 			// "If child is an Element whose specified command value for
3026 			// command is neither null nor equivalent to propagated value,
3027 			// continue with the next child."
3028 			if (child.nodeType == $_.Node.ELEMENT_NODE
3029 			&& getSpecifiedCommandValue(child, command) !== null
3030 			&& !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
3031 				continue;
3032 			}
3033 
3034 			// "If child is the last member of ancestor list, continue with the
3035 			// next child."
3036 			if (child == ancestorList[ancestorList.length - 1]) {
3037 				continue;
3038 			}
3039 
3040 			// "Force the value of child, with command as in this algorithm
3041 			// and new value equal to propagated value."
3042 			forceValue(child, command, propagatedValue, range);
3043 		}
3044 	}
3045 }
3046 
3047 
3048 //@}
3049 ///// Forcing the value of a node /////
3050 //@{
3051 
3052 function forceValue(node, command, newValue, range) {
3053 	// "If node's parent is null, abort this algorithm."
3054 	if (!node.parentNode) {
3055 		return;
3056 	}
3057 
3058 	// "If new value is null, abort this algorithm."
3059 	if (newValue === null) {
3060 		return;
3061 	}
3062 
3063 	// "If node is an allowed child of "span":"
3064 	if (isAllowedChild(node, "span")) {
3065 		// "Reorder modifiable descendants of node's previousSibling."
3066 		reorderModifiableDescendants(node.previousSibling, command, newValue, range);
3067 
3068 		// "Reorder modifiable descendants of node's nextSibling."
3069 		reorderModifiableDescendants(node.nextSibling, command, newValue, range);
3070 
3071 		// "Wrap the one-node list consisting of node, with sibling criteria
3072 		// returning true for a simple modifiable element whose specified
3073 		// command value is equivalent to new value and whose effective command
3074 		// value is loosely equivalent to new value and false otherwise, and
3075 		// with new parent instructions returning null."
3076 		wrap([node],
3077 			function(node) {
3078 				return isSimpleModifiableElement(node)
3079 					&& areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
3080 					&& areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
3081 			},
3082 			function() { return null },
3083 			range
3084 		);
3085 	}
3086 
3087 	// "If the effective command value of command is loosely equivalent to new
3088 	// value on node, abort this algorithm."
3089 	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
3090 		return;
3091 	}
3092 
3093 	// "If node is not an allowed child of "span":"
3094 	if (!isAllowedChild(node, "span")) {
3095 		// "Let children be all children of node, omitting any that are
3096 		// Elements whose specified command value for command is neither null
3097 		// nor equivalent to new value."
3098 		var children = [];
3099 		for (var i = 0; i < node.childNodes.length; i++) {
3100 			if (node.childNodes[i].nodeType == $_.Node.ELEMENT_NODE) {
3101 				var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
3102 
3103 				if (specifiedValue !== null
3104 				&& !areEquivalentValues(command, newValue, specifiedValue)) {
3105 					continue;
3106 				}
3107 			}
3108 			children.push(node.childNodes[i]);
3109 		}
3110 
3111 		// "Force the value of each Node in children, with command and new
3112 		// value as in this invocation of the algorithm."
3113 		for (var i = 0; i < children.length; i++) {
3114 			forceValue(children[i], command, newValue, range);
3115 		}
3116 
3117 		// "Abort this algorithm."
3118 		return;
3119 	}
3120 
3121 	// "If the effective command value of command is loosely equivalent to new
3122 	// value on node, abort this algorithm."
3123 	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
3124 		return;
3125 	}
3126 
3127 	// "Let new parent be null."
3128 	var newParent = null;
3129 
3130 	// "If the CSS styling flag is false:"
3131 	if (!cssStylingFlag) {
3132 		// "If command is "bold" and new value is "bold", let new parent be the
3133 		// result of calling createElement("b") on the ownerDocument of node."
3134 		if (command == "bold" && (newValue == "bold" || newValue == "700")) {
3135 			newParent = node.ownerDocument.createElement("b");
3136 		}
3137 
3138 		// "If command is "italic" and new value is "italic", let new parent be
3139 		// the result of calling createElement("i") on the ownerDocument of
3140 		// node."
3141 		if (command == "italic" && newValue == "italic") {
3142 			newParent = node.ownerDocument.createElement("i");
3143 		}
3144 
3145 		// "If command is "strikethrough" and new value is "line-through", let
3146 		// new parent be the result of calling createElement("s") on the
3147 		// ownerDocument of node."
3148 		if (command == "strikethrough" && newValue == "line-through") {
3149 			newParent = node.ownerDocument.createElement("s");
3150 		}
3151 
3152 		// "If command is "underline" and new value is "underline", let new
3153 		// parent be the result of calling createElement("u") on the
3154 		// ownerDocument of node."
3155 		if (command == "underline" && newValue == "underline") {
3156 			newParent = node.ownerDocument.createElement("u");
3157 		}
3158 
3159 		// "If command is "foreColor", and new value is fully opaque with red,
3160 		// green, and blue components in the range 0 to 255:"
3161 		if (command == "forecolor" && parseSimpleColor(newValue)) {
3162 			// "Let new parent be the result of calling createElement("span")
3163 			// on the ownerDocument of node."
3164 			// NOTE: modified this process to create span elements with style attributes
3165 			// instead of oldschool font tags with color attributes
3166 			newParent = node.ownerDocument.createElement("span");
3167 
3168 			// "If new value is an extended color keyword, set the color
3169 			// attribute of new parent to new value."
3170 			//
3171 			// "Otherwise, set the color attribute of new parent to the result
3172 			// of applying the rules for serializing simple color values to new
3173 			// value (interpreted as a simple color)."
3174 			jQuery(newParent).css('color', parseSimpleColor(newValue));
3175 		}
3176 
3177 		// "If command is "fontName", let new parent be the result of calling
3178 		// createElement("font") on the ownerDocument of node, then set the
3179 		// face attribute of new parent to new value."
3180 		if (command == "fontname") {
3181 			newParent = node.ownerDocument.createElement("font");
3182 			newParent.face = newValue;
3183 		}
3184 	}
3185 
3186 	// "If command is "createLink" or "unlink":"
3187 	if (command == "createlink" || command == "unlink") {
3188 		// "Let new parent be the result of calling createElement("a") on the
3189 		// ownerDocument of node."
3190 		newParent = node.ownerDocument.createElement("a");
3191 
3192 		// "Set the href attribute of new parent to new value."
3193 		newParent.setAttribute("href", newValue);
3194 
3195 		// "Let ancestor be node's parent."
3196 		var ancestor = node.parentNode;
3197 
3198 		// "While ancestor is not null:"
3199 		while (ancestor) {
3200 			// "If ancestor is an a, set the tag name of ancestor to "span",
3201 			// and let ancestor be the result."
3202 			if (isNamedHtmlElement(ancestor, 'A')) {
3203 				ancestor = setTagName(ancestor, "span", range);
3204 			}
3205 
3206 			// "Set ancestor to its parent."
3207 			ancestor = ancestor.parentNode;
3208 		}
3209 	}
3210 
3211 	// "If command is "fontSize"; and new value is one of "xx-small", "small",
3212 	// "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the
3213 	// CSS styling flag is false, or new value is "xxx-large": let new parent
3214 	// be the result of calling createElement("font") on the ownerDocument of
3215 	// node, then set the size attribute of new parent to the number from the
3216 	// following table based on new value: [table omitted]"
3217 	if (command == "fontsize"
3218 	&& jQuery.inArray(newValue, ["xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) != -1
3219 	&& (!cssStylingFlag || newValue == "xxx-large")) {
3220 		newParent = node.ownerDocument.createElement("font");
3221 		newParent.size = cssSizeToLegacy(newValue);
3222 	}
3223 
3224 	// "If command is "subscript" or "superscript" and new value is
3225 	// "subscript", let new parent be the result of calling
3226 	// createElement("sub") on the ownerDocument of node."
3227 	if ((command == "subscript" || command == "superscript")
3228 	&& newValue == "subscript") {
3229 		newParent = node.ownerDocument.createElement("sub");
3230 	}
3231 
3232 	// "If command is "subscript" or "superscript" and new value is
3233 	// "superscript", let new parent be the result of calling
3234 	// createElement("sup") on the ownerDocument of node."
3235 	if ((command == "subscript" || command == "superscript")
3236 	&& newValue == "superscript") {
3237 		newParent = node.ownerDocument.createElement("sup");
3238 	}
3239 
3240 	// "If new parent is null, let new parent be the result of calling
3241 	// createElement("span") on the ownerDocument of node."
3242 	if (!newParent) {
3243 		newParent = node.ownerDocument.createElement("span");
3244 	}
3245 
3246 	// "Insert new parent in node's parent before node."
3247 	node.parentNode.insertBefore(newParent, node);
3248 
3249 	// "If the effective command value of command for new parent is not loosely
3250 	// equivalent to new value, and the relevant CSS property for command is
3251 	// not null, set that CSS property of new parent to new value (if the new
3252 	// value would be valid)."
3253 	var property = commands[command].relevantCssProperty;
3254 	if (property !== null
3255 	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
3256 		newParent.style[property] = newValue;
3257 	}
3258 
3259 	// "If command is "strikethrough", and new value is "line-through", and the
3260 	// effective command value of "strikethrough" for new parent is not
3261 	// "line-through", set the "text-decoration" property of new parent to
3262 	// "line-through"."
3263 	if (command == "strikethrough"
3264 	&& newValue == "line-through"
3265 	&& getEffectiveCommandValue(newParent, "strikethrough") != "line-through") {
3266 		newParent.style.textDecoration = "line-through";
3267 	}
3268 
3269 	// "If command is "underline", and new value is "underline", and the
3270 	// effective command value of "underline" for new parent is not
3271 	// "underline", set the "text-decoration" property of new parent to
3272 	// "underline"."
3273 	if (command == "underline"
3274 	&& newValue == "underline"
3275 	&& getEffectiveCommandValue(newParent, "underline") != "underline") {
3276 		newParent.style.textDecoration = "underline";
3277 	}
3278 
3279 	// "Append node to new parent as its last child, preserving ranges."
3280 	movePreservingRanges(node, newParent, newParent.childNodes.length, range);
3281 
3282 	// "If node is an Element and the effective command value of command for
3283 	// node is not loosely equivalent to new value:"
3284 	if (node.nodeType == $_.Node.ELEMENT_NODE
3285 	&& !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
3286 		// "Insert node into the parent of new parent before new parent,
3287 		// preserving ranges."
3288 		movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent), range);
3289 
3290 		// "Remove new parent from its parent."
3291 		newParent.parentNode.removeChild(newParent);
3292 
3293 		// "Let children be all children of node, omitting any that are
3294 		// Elements whose specified command value for command is neither null
3295 		// nor equivalent to new value."
3296 		var children = [];
3297 		for (var i = 0; i < node.childNodes.length; i++) {
3298 			if (node.childNodes[i].nodeType == $_.Node.ELEMENT_NODE) {
3299 				var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
3300 
3301 				if (specifiedValue !== null
3302 				&& !areEquivalentValues(command, newValue, specifiedValue)) {
3303 					continue;
3304 				}
3305 			}
3306 			children.push(node.childNodes[i]);
3307 		}
3308 
3309 		// "Force the value of each Node in children, with command and new
3310 		// value as in this invocation of the algorithm."
3311 		for (var i = 0; i < children.length; i++) {
3312 			forceValue(children[i], command, newValue, range);
3313 		}
3314 	}
3315 }
3316 
3317 
3318 //@}
3319 ///// Setting the selection's value /////
3320 //@{
3321 
3322 function setSelectionValue(command, newValue, range) {
3323 	
3324 	// Use current selected range if no range passed
3325 	range = range || getActiveRange();
3326 	
3327 	// "If there is no editable text node effectively contained in the active
3328 	// range:"
3329 	if (!$_( getAllEffectivelyContainedNodes(range) )
3330 	.filter(function(node) { return node.nodeType == $_.Node.TEXT_NODE}, true)
3331 	.some(isEditable)) {
3332 		// "If command has inline command activated values, set the state
3333 		// override to true if new value is among them and false if it's not."
3334 		if ("inlineCommandActivatedValues" in commands[command]) {
3335 			setStateOverride(command, 
3336       $_(commands[command].inlineCommandActivatedValues).indexOf(newValue) != -1,
3337       range);
3338 		}
3339 
3340 		// "If command is "subscript", unset the state override for
3341 		// "superscript"."
3342 		if (command == "subscript") {
3343 			unsetStateOverride("superscript", range);
3344 		}
3345 
3346 		// "If command is "superscript", unset the state override for
3347 		// "subscript"."
3348 		if (command == "superscript") {
3349 			unsetStateOverride("subscript", range);
3350 		}
3351 
3352 		// "If new value is null, unset the value override (if any)."
3353 		if (newValue === null) {
3354 			unsetValueOverride(command, range);
3355 
3356 		// "Otherwise, if command has a value specified, set the value override
3357 		// to new value."
3358 		} else if ("value" in commands[command]) {
3359 			setValueOverride(command, newValue, range);
3360 		}
3361 
3362 		// "Abort these steps."
3363 		return;
3364 	}
3365 
3366 	// "If the active range's start node is an editable Text node, and its
3367 	// start offset is neither zero nor its start node's length, call
3368 	// splitText() on the active range's start node, with argument equal to the
3369 	// active range's start offset. Then set the active range's start node to
3370 	// the result, and its start offset to zero."
3371 	if (isEditable(range.startContainer)
3372 	&& range.startContainer.nodeType == $_.Node.TEXT_NODE
3373 	&& range.startOffset != 0
3374 	&& range.startOffset != getNodeLength(range.startContainer)) {
3375 		// Account for browsers not following range mutation rules
3376 		var newNode = range.startContainer.splitText(range.startOffset);
3377 		var newActiveRange = Aloha.createRange();
3378 		if (range.startContainer == range.endContainer) {
3379 			var newEndOffset = range.endOffset - range.startOffset;
3380 			newActiveRange.setEnd(newNode, newEndOffset);
3381 			range.setEnd(newNode, newEndOffset);
3382 		}
3383 		newActiveRange.setStart(newNode, 0);
3384 		Aloha.getSelection().removeAllRanges();
3385 		Aloha.getSelection().addRange(newActiveRange);
3386 
3387 		range.setStart(newNode, 0);
3388 	}
3389 
3390 	// "If the active range's end node is an editable Text node, and its end
3391 	// offset is neither zero nor its end node's length, call splitText() on
3392 	// the active range's end node, with argument equal to the active range's
3393 	// end offset."
3394 	if (isEditable(range.endContainer)
3395 	&& range.endContainer.nodeType == $_.Node.TEXT_NODE
3396 	&& range.endOffset != 0
3397 	&& range.endOffset != getNodeLength(range.endContainer)) {
3398 		// IE seems to mutate the range incorrectly here, so we need correction
3399 		// here as well.  The active range will be temporarily in orphaned
3400 		// nodes, so calling getActiveRange() after splitText() but before
3401 		// fixing the range will throw an exception.
3402 		// TODO: check if this is still neccessary 
3403 		var activeRange = range;
3404 		var newStart = [activeRange.startContainer, activeRange.startOffset];
3405 		var newEnd = [activeRange.endContainer, activeRange.endOffset];
3406 		activeRange.endContainer.splitText(activeRange.endOffset);
3407 		activeRange.setStart(newStart[0], newStart[1]);
3408 		activeRange.setEnd(newEnd[0], newEnd[1]);
3409 
3410 		Aloha.getSelection().removeAllRanges();
3411 		Aloha.getSelection().addRange(activeRange);
3412 	}
3413 
3414 	// "Let element list be all editable Elements effectively contained in the
3415 	// active range.
3416 	//
3417 	// "For each element in element list, clear the value of element."
3418 	$_( getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3419 		return isEditable(node) && node.nodeType == $_.Node.ELEMENT_NODE;
3420 	}) ).forEach(function(element) {
3421 		clearValue(element, command, range);
3422 	});
3423 
3424 	// "Let node list be all editable nodes effectively contained in the active
3425 	// range.
3426 	//
3427 	// "For each node in node list:"
3428 	$_( getAllEffectivelyContainedNodes(range, isEditable) ).forEach(function(node) {
3429 		// "Push down values on node."
3430 		pushDownValues(node, command, newValue, range);
3431 
3432 		// "Force the value of node."
3433 		forceValue(node, command, newValue, range);
3434 	});
3435 }
3436 
3437 /**
3438  * attempt to retrieve a block like a table or an Aloha Block
3439  * which is located one step right of the current caret position.
3440  * If an appropriate element is found it will be returned or
3441  * false otherwise
3442  * 
3443  * @param {element} node current node we're in
3444  * @param {number} offset current offset within that node
3445  * 
3446  * @return the dom node if found or false if no appropriate
3447  * element was found
3448  */
3449 function getBlockAtNextPosition(node, offset) {
3450 	// if we're inside a text node we first have to check
3451 	// if there is nothing but tabs, newlines or the like
3452 	// after our current cursor position
3453 	if (node.nodeType === $_.Node.TEXT_NODE &&
3454 	offset < node.length) {
3455 		for (var i = offset; i < node.length; i++) {
3456 			if ((node.data.charAt(i) !== '\t' &&
3457 			node.data.charAt(i) !== '\r' &&
3458 			node.data.charAt(i) !== '\n') ||
3459 			node.data.charCodeAt(i) === 160) { //  
3460 				// this is a character that has to be deleted first
3461 				return false;
3462 			}
3463 		}
3464 	}
3465 
3466 	// try the most simple approach first: the next sibling
3467 	// is a table
3468 	if (node.nextSibling &&
3469 	node.nextSibling.className &&
3470 	node.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) {
3471 		return node.nextSibling;
3472 	}
3473 	
3474 	// since we got only ignorable whitespace here determine if
3475 	// our nodes parents next sibling is a table
3476 	if (node.parentNode &&
3477 	node.parentNode.nextSibling &&
3478 	node.parentNode.nextSibling.className &&
3479 	node.parentNode.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) {
3480 		return node.parentNode.nextSibling;
3481 	}
3482 
3483 	// our parents nextsibling is a pure whitespace node such as
3484 	// generated by sourcecode indentation so we'll check for
3485 	// the next next sibling
3486 	if (node.parentNode &&
3487 	node.parentNode.nextSibling &&
3488 	isWhitespaceNode(node.parentNode.nextSibling) &&
3489 	node.parentNode.nextSibling.nextSibling &&
3490 	node.parentNode.nextSibling.nextSibling.className &&
3491 	node.parentNode.nextSibling.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) {
3492 		return node.parentNode.nextSibling.nextSibling;
3493 	}
3494 
3495 	// Note: the search above works for tables, since they cannot be
3496 	// nested deeply in paragraphs and other formatting tags. If this code
3497 	// is extended to work also for other blocks, the search probably needs to be adapted
3498 }
3499 
3500 /**
3501  * Attempt to retrieve a block like a table or an Aloha Block
3502  * which is located right before the current position.
3503  * If an appropriate element is found, it will be returned or
3504  * false otherwise
3505  * 
3506  * @param {element} node current node
3507  * @param {offset} offset current offset
3508  * 
3509  * @return dom node of found or false if no appropriate
3510  * element was found
3511  */
3512 function getBlockAtPreviousPosition(node, offset) {
3513 	if (node.nodeType === $_.Node.TEXT_NODE && offset > 0) {
3514 		for (var i = offset-1; i >= 0; i--) {
3515 			if ((node.data.charAt(i) !== '\t' &&
3516 			node.data.charAt(i) !== '\r' &&
3517 			node.data.charAt(i) !== '\n') ||
3518 			node.data.charCodeAt(i) === 160) { //  
3519 				// this is a character that has to be deleted first
3520 				return false;
3521 			}
3522 		}
3523 	}
3524 
3525 	// try the previous sibling
3526 	if (node.previousSibling &&
3527 	node.previousSibling.className &&
3528 	node.previousSibling.className.indexOf("aloha-table-wrapper") >= 0) {
3529 		return node.previousSibling;
3530 	}
3531 
3532 	// try the parent's previous sibling
3533 	if (node.parentNode &&
3534 	node.parentNode.previousSibling &&
3535 	node.parentNode.previousSibling.className &&
3536 	node.parentNode.previousSibling.className.indexOf("aloha-table-wrapper") >= 0) {
3537 		return node.parentNode.previousSibling;
3538 	}
3539 
3540 	// the parent's previous sibling might be a whitespace node
3541 	if (node.parentNode &&
3542 	node.parentNode.previousSibling &&
3543 	isWhitespaceNode(node.parentNode.previousSibling) &&
3544 	node.parentNode.previousSibling.previousSibling &&
3545 	node.parentNode.previousSibling.previousSibling.className &&
3546 	node.parentNode.previousSibling.previousSibling.className.indexOf('aloha-table-wrapper') >= 0) {
3547 		return node.parentNode.previousSibling.previousSibling;
3548 	}
3549 
3550 	// Note: the search above works for tables, since they cannot be
3551 	// nested deeply in paragraphs and other formatting tags. If this code
3552 	// is extended to work also for other blocks, the search probably needs to be adapted
3553 
3554 	return false;
3555 }
3556 
3557 
3558 //@}
3559 ///// The backColor command /////
3560 //@{
3561 commands.backcolor = {
3562 	// Copy-pasted, same as hiliteColor
3563 	action: function(value) {
3564 		// Action is further copy-pasted, same as foreColor
3565 
3566 		// "If value is not a valid CSS color, prepend "#" to it."
3567 		//
3568 		// "If value is still not a valid CSS color, or if it is currentColor,
3569 		// abort these steps and do nothing."
3570 		//
3571 		// Cheap hack for testing, no attempt to be comprehensive.
3572 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3573 			value = "#" + value;
3574 		}
3575 		if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3576 		&& !parseSimpleColor(value)
3577 		&& value.toLowerCase() != "transparent") {
3578 			return;
3579 		}
3580 
3581 		// "Set the selection's value to value."
3582 		setSelectionValue("backcolor", value);
3583 	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3584 	equivalentValues: function(val1, val2) {
3585 		// "Either both strings are valid CSS colors and have the same red,
3586 		// green, blue, and alpha components, or neither string is a valid CSS
3587 		// color."
3588 		return normalizeColor(val1) === normalizeColor(val2);
3589 	}
3590 };
3591 
3592 //@}
3593 ///// The bold command /////
3594 //@{
3595 commands.bold = {
3596 	action: function(value, range) {
3597 		// "If queryCommandState("bold") returns true, set the selection's
3598 		// value to "normal". Otherwise set the selection's value to "bold"."
3599 		if (myQueryCommandState("bold", range)) {
3600 			setSelectionValue("bold", "normal", range);
3601 		} else {
3602 			setSelectionValue("bold", "bold", range);
3603 		}
3604 	}, 
3605 	inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
3606 	relevantCssProperty: "fontWeight",
3607 	equivalentValues: function(val1, val2) {
3608 		// "Either the two strings are equal, or one is "bold" and the other is
3609 		// "700", or one is "normal" and the other is "400"."
3610 		return val1 == val2
3611 			|| (val1 == "bold" && val2 == "700")
3612 			|| (val1 == "700" && val2 == "bold")
3613 			|| (val1 == "normal" && val2 == "400")
3614 			|| (val1 == "400" && val2 == "normal");
3615 	}
3616 };
3617 
3618 //@}
3619 ///// The createLink command /////
3620 //@{
3621 commands.createlink = {
3622 	action: function(value) {
3623 		// "If value is the empty string, abort these steps and do nothing."
3624 		if (value === "") {
3625 			return;
3626 		}
3627 
3628 		// "For each editable a element that has an href attribute and is an
3629 		// ancestor of some node effectively contained in the active range, set
3630 		// that a element's href attribute to value."
3631 		//
3632 		// TODO: We don't actually do this in tree order, not that it matters
3633 		// unless you're spying with mutation events.
3634 		$_( getAllEffectivelyContainedNodes(getActiveRange()) ).forEach(function(node) {
3635 			$_( getAncestors(node) ).forEach(function(ancestor) {
3636 				if (isEditable(ancestor)
3637 				&& isNamedHtmlElement(ancestor, 'a')
3638 				&& hasAttribute(ancestor, "href")) {
3639 					ancestor.setAttribute("href", value);
3640 				}
3641 			});
3642 		});
3643 
3644 		// "Set the selection's value to value."
3645 		setSelectionValue("createlink", value);
3646 	}, standardInlineValueCommand: true
3647 };
3648 
3649 //@}
3650 ///// The fontName command /////
3651 //@{
3652 commands.fontname = {
3653 	action: function(value) {
3654 		// "Set the selection's value to value."
3655 		setSelectionValue("fontname", value);
3656 	}, standardInlineValueCommand: true, relevantCssProperty: "fontFamily"
3657 };
3658 
3659 //@}
3660 ///// The fontSize command /////
3661 //@{
3662 
3663 // Helper function for fontSize's action plus queryOutputHelper.  It's just the
3664 // middle of fontSize's action, ripped out into its own function.
3665 function normalizeFontSize(value) {
3666 	// "Strip leading and trailing whitespace from value."
3667 	//
3668 	// Cheap hack, not following the actual algorithm.
3669 	value = $_(value).trim();
3670 
3671 	// "If value is a valid floating point number, or would be a valid
3672 	// floating point number if a single leading "+" character were
3673 	// stripped:"
3674 	if (/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
3675 		var mode;
3676 
3677 		// "If the first character of value is "+", delete the character
3678 		// and let mode be "relative-plus"."
3679 		if (value[0] == "+") {
3680 			value = value.slice(1);
3681 			mode = "relative-plus";
3682 		// "Otherwise, if the first character of value is "-", delete the
3683 		// character and let mode be "relative-minus"."
3684 		} else if (value[0] == "-") {
3685 			value = value.slice(1);
3686 			mode = "relative-minus";
3687 		// "Otherwise, let mode be "absolute"."
3688 		} else {
3689 			mode = "absolute";
3690 		}
3691 
3692 		// "Apply the rules for parsing non-negative integers to value, and
3693 		// let number be the result."
3694 		//
3695 		// Another cheap hack.
3696 		var num = parseInt(value);
3697 
3698 		// "If mode is "relative-plus", add three to number."
3699 		if (mode == "relative-plus") {
3700 			num += 3;
3701 		}
3702 
3703 		// "If mode is "relative-minus", negate number, then add three to
3704 		// it."
3705 		if (mode == "relative-minus") {
3706 			num = 3 - num;
3707 		}
3708 
3709 		// "If number is less than one, let number equal 1."
3710 		if (num < 1) {
3711 			num = 1;
3712 		}
3713 
3714 		// "If number is greater than seven, let number equal 7."
3715 		if (num > 7) {
3716 			num = 7;
3717 		}
3718 
3719 		// "Set value to the string here corresponding to number:" [table
3720 		// omitted]
3721 		value = {
3722 			1: "xx-small",
3723 			2: "small",
3724 			3: "medium",
3725 			4: "large",
3726 			5: "x-large",
3727 			6: "xx-large",
3728 			7: "xxx-large"
3729 		}[num];
3730 	}
3731 
3732 	return value;
3733 }
3734 
3735 commands.fontsize = {
3736 	action: function(value) {
3737 		// "If value is the empty string, abort these steps and do nothing."
3738 		if (value === "") {
3739 			return;
3740 		}
3741 
3742 		value = normalizeFontSize(value);
3743 
3744 		// "If value is not one of the strings "xx-small", "x-small", "small",
3745 		// "medium", "large", "x-large", "xx-large", "xxx-large", and is not a
3746 		// valid CSS absolute length, then abort these steps and do nothing."
3747 		//
3748 		// More cheap hacks to skip valid CSS absolute length checks.
3749 		if (jQuery.inArray(value, ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) == -1
3750 		&& !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc)$/.test(value)) {
3751 			return;
3752 		}
3753 
3754 		// "Set the selection's value to value."
3755 		setSelectionValue("fontsize", value);
3756 	}, 
3757 	indeterm: function() {
3758 		// "True if among editable Text nodes that are effectively contained in
3759 		// the active range, there are two that have distinct effective command
3760 		// values.  Otherwise false."
3761 		return $_( getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3762 			return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE;
3763 		}) ).map(function(node) {
3764 			return getEffectiveCommandValue(node, "fontsize");
3765 		}, true).filter(function(value, i, arr) {
3766 			return $_(arr.slice(0, i)).indexOf(value) == -1;
3767 		}).length >= 2;
3768 	}, 
3769 	value: function(range) {
3770 		// "Let pixel size be the effective command value of the first editable
3771 		// Text node that is effectively contained in the active range, or if
3772 		// there is no such node, the effective command value of the active
3773 		// range's start node, in either case interpreted as a number of
3774 		// pixels."
3775 		var node = getAllEffectivelyContainedNodes(range, function(node) {
3776 			return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE;
3777 		})[0];
3778 		if (node === undefined) {
3779 			node = range.startContainer;
3780 		}
3781 		var pixelSize = getEffectiveCommandValue(node, "fontsize");
3782 
3783 		// "Return the legacy font size for pixel size."
3784 		return getLegacyFontSize(pixelSize);
3785 	}, relevantCssProperty: "fontSize"
3786 };
3787 
3788 function getLegacyFontSize(size) {
3789 	// For convenience in other places in my code, I handle all sizes, not just
3790 	// pixel sizes as the spec says.  This means pixel sizes have to be passed
3791 	// in suffixed with "px", not as plain numbers.
3792 	size = normalizeFontSize(size);
3793 
3794 	if (jQuery.inArray(size, ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) == -1
3795 	&& !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) {
3796 		// There is no sensible legacy size for things like "2em".
3797 		return null;
3798 	}
3799 
3800 	var font = document.createElement("font");
3801 	document.body.appendChild(font);
3802 	if (size == "xxx-large") {
3803 		font.size = 7;
3804 	} else {
3805 		font.style.fontSize = size;
3806 	}
3807 	var pixelSize = parseInt($_.getComputedStyle(font).fontSize);
3808 	document.body.removeChild(font);
3809 
3810 	// "Let returned size be 1."
3811 	var returnedSize = 1;
3812 
3813 	// "While returned size is less than 7:"
3814 	while (returnedSize < 7) {
3815 3816 		// "Let lower bound be the resolved value of "font-size" in pixels
3817 		// of a font element whose size attribute is set to returned size."
3818 		var font = document.createElement("font");
3819 		font.size = returnedSize;
3820 		document.body.appendChild(font);
3821 		var lowerBound = parseInt($_.getComputedStyle(font).fontSize);
3822 
3823 		// "Let upper bound be the resolved value of "font-size" in pixels
3824 		// of a font element whose size attribute is set to one plus
3825 		// returned size."
3826 		font.size = 1 + returnedSize;
3827 		var upperBound = parseInt($_.getComputedStyle(font).fontSize);
3828 		document.body.removeChild(font);
3829 
3830 		// "Let average be the average of upper bound and lower bound."
3831 		var average = (upperBound + lowerBound)/2;
3832 
3833 		// "If pixel size is less than average, return the one-element
3834 		// string consisting of the digit returned size."
3835 		if (pixelSize < average) {
3836 			return String(returnedSize);
3837 		}
3838 
3839 		// "Add one to returned size."
3840 		returnedSize++;
3841 	}
3842 
3843 	// "Return "7"."
3844 	return "7";
3845 }
3846 
3847 //@}
3848 ///// The foreColor command /////
3849 //@{
3850 commands.forecolor = {
3851 	action: function(value) {
3852 		// Copy-pasted, same as backColor and hiliteColor
3853 
3854 		// "If value is not a valid CSS color, prepend "#" to it."
3855 		//
3856 		// "If value is still not a valid CSS color, or if it is currentColor,
3857 		// abort these steps and do nothing."
3858 		//
3859 		// Cheap hack for testing, no attempt to be comprehensive.
3860 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3861 			value = "#" + value;
3862 		}
3863 		if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3864 		&& !parseSimpleColor(value)
3865 		&& value.toLowerCase() != "transparent") {
3866 			return;
3867 		}
3868 
3869 		// "Set the selection's value to value."
3870 		setSelectionValue("forecolor", value);
3871 	}, standardInlineValueCommand: true, relevantCssProperty: "color",
3872 	equivalentValues: function(val1, val2) {
3873 		// "Either both strings are valid CSS colors and have the same red,
3874 		// green, blue, and alpha components, or neither string is a valid CSS
3875 		// color."
3876 		return normalizeColor(val1) === normalizeColor(val2);
3877 	}
3878 };
3879 
3880 //@}
3881 ///// The hiliteColor command /////
3882 //@{
3883 commands.hilitecolor = {
3884 	// Copy-pasted, same as backColor
3885 	action: function(value) {
3886 		// Action is further copy-pasted, same as foreColor
3887 
3888 		// "If value is not a valid CSS color, prepend "#" to it."
3889 		//
3890 		// "If value is still not a valid CSS color, or if it is currentColor,
3891 		// abort these steps and do nothing."
3892 		//
3893 		// Cheap hack for testing, no attempt to be comprehensive.
3894 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
3895 			value = "#" + value;
3896 		}
3897 		if (!/^(rgba?|hsla?)\(.*\)$/.test(value)
3898 		&& !parseSimpleColor(value)
3899 		&& value.toLowerCase() != "transparent") {
3900 			return;
3901 		}
3902 
3903 		// "Set the selection's value to value."
3904 		setSelectionValue("hilitecolor", value);
3905 	}, indeterm: function() {
3906 		// "True if among editable Text nodes that are effectively contained in
3907 		// the active range, there are two that have distinct effective command
3908 		// values.  Otherwise false."
3909 		return $_( getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
3910 			return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE;
3911 		}) ).map(function(node) {
3912 			return getEffectiveCommandValue(node, "hilitecolor");
3913 		}, true).filter(function(value, i, arr) {
3914 			return $_(arr.slice(0, i)).indexOf(value) == -1;
3915 		}).length >= 2;
3916 	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
3917 	equivalentValues: function(val1, val2) {
3918 		// "Either both strings are valid CSS colors and have the same red,
3919 		// green, blue, and alpha components, or neither string is a valid CSS
3920 		// color."
3921 		return normalizeColor(val1) === normalizeColor(val2);
3922 	}
3923 };
3924 
3925 //@}
3926 ///// The italic command /////
3927 //@{
3928 commands.italic = {
3929 	action: function( value, range ) {
3930 		// "If queryCommandState("italic") returns true, set the selection's
3931 		// value to "normal". Otherwise set the selection's value to "italic"."
3932 		if (myQueryCommandState("italic", range)) {
3933 			setSelectionValue("italic", "normal", range);
3934 		} else {
3935 			setSelectionValue("italic", "italic", range);
3936 		}
3937 	}, inlineCommandActivatedValues: ["italic", "oblique"],
3938 	relevantCssProperty: "fontStyle"
3939 };
3940 
3941 //@}
3942 ///// The removeFormat command /////
3943 //@{
3944 commands.removeformat = {
3945 	action: function() {
3946 		// "A removeFormat candidate is an editable HTML element with local
3947 		// name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite",
3948 		// "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q",
3949 		// "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
3950 		// "tt", "u", or "var"."
3951 		function isRemoveFormatCandidate(node) {
3952 			return isEditable(node)
3953 				&& isHtmlElementInArray(node, ["abbr", "acronym", "b", "bdi", "bdo",
3954 				"big", "blink", "cite", "code", "dfn", "em", "font", "i",
3955 				"ins", "kbd", "mark", "nobr", "q", "s", "samp", "small",
3956 				"span", "strike", "strong", "sub", "sup", "tt", "u", "var"]);
3957 		}
3958 
3959 		// "Let elements to remove be a list of every removeFormat candidate
3960 		// effectively contained in the active range."
3961 		var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate);
3962 
3963 		// "For each element in elements to remove:"
3964 		$_( elementsToRemove ).forEach(function(element) {
3965 			// "While element has children, insert the first child of element
3966 			// into the parent of element immediately before element,
3967 			// preserving ranges."
3968 			while (element.hasChildNodes()) {
3969 				movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element), range);
3970 			}
3971 
3972 			// "Remove element from its parent."
3973 			element.parentNode.removeChild(element);
3974 		});
3975 
3976 		// "If the active range's start node is an editable Text node, and its
3977 		// start offset is neither zero nor its start node's length, call
3978 		// splitText() on the active range's start node, with argument equal to
3979 		// the active range's start offset. Then set the active range's start
3980 		// node to the result, and its start offset to zero."
3981 		if (isEditable(getActiveRange().startContainer)
3982 		&& getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE
3983 		&& getActiveRange().startOffset != 0
3984 		&& getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) {
3985 			// Account for browsers not following range mutation rules
3986 			if (getActiveRange().startContainer == getActiveRange().endContainer) {
3987 				var newEnd = getActiveRange().endOffset - getActiveRange().startOffset;
3988 				var newNode = getActiveRange().startContainer.splitText(getActiveRange().startOffset);
3989 				getActiveRange().setStart(newNode, 0);
3990 				getActiveRange().setEnd(newNode, newEnd);
3991 			} else {
3992 				getActiveRange().setStart(getActiveRange().startContainer.splitText(getActiveRange().startOffset), 0);
3993 			}
3994 		}
3995 
3996 		// "If the active range's end node is an editable Text node, and its
3997 		// end offset is neither zero nor its end node's length, call
3998 		// splitText() on the active range's end node, with argument equal to
3999 		// the active range's end offset."
4000 		if (isEditable(getActiveRange().endContainer)
4001 		&& getActiveRange().endContainer.nodeType == $_.Node.TEXT_NODE
4002 		&& getActiveRange().endOffset != 0
4003 		&& getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) {
4004 			// IE seems to mutate the range incorrectly here, so we need
4005 			// correction here as well.  Have to be careful to set the range to
4006 			// something not including the text node so that getActiveRange()
4007 			// doesn't throw an exception due to a temporarily detached
4008 			// endpoint.
4009 			var newStart = [getActiveRange().startContainer, getActiveRange().startOffset];
4010 			var newEnd = [getActiveRange().endContainer, getActiveRange().endOffset];
4011 			getActiveRange().setEnd(document.documentElement, 0);
4012 			newEnd[0].splitText(newEnd[1]);
4013 			getActiveRange().setStart(newStart[0], newStart[1]);
4014 			getActiveRange().setEnd(newEnd[0], newEnd[1]);
4015 		}
4016 
4017 		// "Let node list consist of all editable nodes effectively contained
4018 		// in the active range."
4019 		//
4020 		// "For each node in node list, while node's parent is a removeFormat
4021 		// candidate in the same editing host as node, split the parent of the
4022 		// one-node list consisting of node."
4023 		$_( getAllEffectivelyContainedNodes(getActiveRange(), isEditable) ).forEach(function(node) {
4024 			while (isRemoveFormatCandidate(node.parentNode)
4025 			&& inSameEditingHost(node.parentNode, node)) {
4026 				splitParent([node], range);
4027 			}
4028 		});
4029 
4030 		// "For each of the entries in the following list, in the given order,
4031 		// set the selection's value to null, with command as given."
4032 		$_( [
4033 			"subscript",
4034 			"bold",
4035 			"fontname",
4036 			"fontsize",
4037 			"forecolor",
4038 			"hilitecolor",
4039 			"italic",
4040 			"strikethrough",
4041 			"underline"
4042 		] ).forEach(function(command) {
4043 			setSelectionValue(command, null);
4044 		});
4045 	}
4046 };
4047 
4048 //@}
4049 ///// The strikethrough command /////
4050 //@{
4051 commands.strikethrough = {
4052 	action: function() {
4053 		// "If queryCommandState("strikethrough") returns true, set the
4054 		// selection's value to null. Otherwise set the selection's value to
4055 		// "line-through"."
4056 		if (myQueryCommandState("strikethrough")) {
4057 			setSelectionValue("strikethrough", null);
4058 		} else {
4059 			setSelectionValue("strikethrough", "line-through");
4060 		}
4061 	}, inlineCommandActivatedValues: ["line-through"]
4062 };
4063 
4064 //@}
4065 ///// The subscript command /////
4066 //@{
4067 commands.subscript = {
4068 	action: function() {
4069 		// "Call queryCommandState("subscript"), and let state be the result."
4070 		var state = myQueryCommandState("subscript");
4071 
4072 		// "Set the selection's value to null."
4073 		setSelectionValue("subscript", null);
4074 
4075 		// "If state is false, set the selection's value to "subscript"."
4076 		if (!state) {
4077 			setSelectionValue("subscript", "subscript");
4078 		}
4079 	}, indeterm: function() {
4080 		// "True if either among editable Text nodes that are effectively
4081 		// contained in the active range, there is at least one with effective
4082 		// command value "subscript" and at least one with some other effective
4083 		// command value; or if there is some editable Text node effectively
4084 		// contained in the active range with effective command value "mixed".
4085 		// Otherwise false."
4086 		var nodes = getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
4087 			return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE;
4088 		});
4089 		return ($_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "subscript") == "subscript" })
4090 			&& $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "subscript") != "subscript" }))
4091 			|| $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "subscript") == "mixed" });
4092 	}, inlineCommandActivatedValues: ["subscript"]
4093 };
4094 
4095 //@}
4096 ///// The superscript command /////
4097 //@{
4098 commands.superscript = {
4099 	action: function() {
4100 		// "Call queryCommandState("superscript"), and let state be the
4101 		// result."
4102 		var state = myQueryCommandState("superscript");
4103 
4104 		// "Set the selection's value to null."
4105 		setSelectionValue("superscript", null);
4106 
4107 		// "If state is false, set the selection's value to "superscript"."
4108 		if (!state) {
4109 			setSelectionValue("superscript", "superscript");
4110 		}
4111 	}, indeterm: function() {
4112 		// "True if either among editable Text nodes that are effectively
4113 		// contained in the active range, there is at least one with effective
4114 		// command value "superscript" and at least one with some other
4115 		// effective command value; or if there is some editable Text node
4116 		// effectively contained in the active range with effective command
4117 		// value "mixed".  Otherwise false."
4118 		var nodes = getAllEffectivelyContainedNodes(getActiveRange(),
4119 				function(node) {
4120 			return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE;
4121 		});
4122 		return ($_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "superscript") == "superscript" })
4123 			&& $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "superscript") != "superscript" }))
4124 			|| $_( nodes ).some(function(node) { return getEffectiveCommandValue(node, "superscript") == "mixed" });
4125 	}, inlineCommandActivatedValues: ["superscript"]
4126 };
4127 
4128 //@}
4129 ///// The underline command /////
4130 //@{
4131 commands.underline = {
4132 	action: function() {
4133 		// "If queryCommandState("underline") returns true, set the selection's
4134 		// value to null. Otherwise set the selection's value to "underline"."
4135 		if (myQueryCommandState("underline")) {
4136 			setSelectionValue("underline", null);
4137 4138 		} else {
4139 			setSelectionValue("underline", "underline");
4140 		}
4141 	}, inlineCommandActivatedValues: ["underline"]
4142 };
4143 
4144 //@}
4145 ///// The unlink command /////
4146 //@{
4147 commands.unlink = {
4148 	action: function() {
4149 		// "Let hyperlinks be a list of every a element that has an href
4150 		// attribute and is contained in the active range or is an ancestor of
4151 		// one of its boundary points."
4152 		//
4153 		// As usual, take care to ensure it's tree order.  The correctness of
4154 		// the following is left as an exercise for the reader.
4155 		var range = getActiveRange();
4156 		var hyperlinks = [];
4157 		for (
4158 			var node = range.startContainer;
4159 			node;
4160 			node = node.parentNode
4161 		) {
4162 			if (isNamedHtmlElement(node, 'A')
4163 			&& hasAttribute(node, "href")) {
4164 				hyperlinks.unshift(node);
4165 			}
4166 		}
4167 		for (
4168 			var node = range.startContainer;
4169 			node != nextNodeDescendants(range.endContainer);
4170 			node = nextNode(node)
4171 		) {
4172 			if (isNamedHtmlElement(node, 'A')
4173 			&& hasAttribute(node, "href")
4174 			&& (isContained(node, range)
4175 			|| isAncestor(node, range.endContainer)
4176 			|| node == range.endContainer)) {
4177 				hyperlinks.push(node);
4178 			}
4179 		}
4180 
4181 		// "Clear the value of each member of hyperlinks."
4182 		for (var i = 0; i < hyperlinks.length; i++) {
4183 			clearValue(hyperlinks[i], "unlink", range);
4184 		}
4185 	}, standardInlineValueCommand: true
4186 };
4187 
4188 //@}
4189 
4190 /////////////////////////////////////
4191 ///// Block formatting commands /////
4192 /////////////////////////////////////
4193 
4194 ///// Block formatting command definitions /////
4195 //@{
4196 
4197 // "An indentation element is either a blockquote, or a div that has a style
4198 // attribute that sets "margin" or some subproperty of it."
4199 function isIndentationElement(node) {
4200 	if (!isAnyHtmlElement(node)) {
4201 		return false;
4202 	}
4203 
4204 	if (node.tagName == "BLOCKQUOTE") {
4205 		return true;
4206 	}
4207 
4208 	if (node.tagName != "DIV") {
4209 		return false;
4210 	}
4211 
4212 	if (typeof node.style.length !== 'undefined') {
4213 		for (var i = 0; i < node.style.length; i++) {
4214 			// Approximate check
4215 			if (/^(-[a-z]+-)?margin/.test(node.style[i])) {
4216 				return true;
4217 			}
4218 		}
4219 	} else {
4220 		for (var s in node.style) {
4221 			if (/^(-[a-z]+-)?margin/.test(s) && node.style[s] && node.style[s] !== 0) {
4222 				return true;
4223 			}
4224 		}
4225 	}
4226 
4227 	return false;
4228 }
4229 
4230 // "A simple indentation element is an indentation element that has no
4231 // attributes other than one or more of
4232 //
4233 //   * "a style attribute that sets no properties other than "margin", "border",
4234 //     "padding", or subproperties of those;
4235 //   * "a class attribute;
4236 //   * "a dir attribute."
4237 function isSimpleIndentationElement(node) {
4238 	if (!isIndentationElement(node)) {
4239 		return false;
4240 	}
4241 
4242 	if (node.tagName != "BLOCKQUOTE" && node.tagName != "DIV") {
4243 		return false;
4244 	}
4245 
4246 	for (var i = 0; i < node.attributes.length; i++) {
4247 		if (!isHtmlNamespace(node.attributes[i].namespaceURI)
4248 		|| jQuery.inArray(node.attributes[i].name, ["style", "class", "dir"]) == -1) {
4249 			return false;
4250 		}
4251 	}
4252 
4253 	if (typeof node.style.length !== 'undefined') {
4254 		for (var i = 0; i < node.style.length; i++) {
4255 			// This is approximate, but it works well enough for my purposes.
4256 			if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) {
4257 				return false;
4258 			}
4259 		}
4260 	} else {
4261 		for (var s in node.style) {
4262 			// This is approximate, but it works well enough for my purposes.
4263 			if (!/^(-[a-z]+-)?(margin|border|padding)/.test(s) && node.style[s] && node.style[s] !== 0 && node.style[s] !== 'false') {
4264 				return false;
4265 			}
4266 		}
4267 	}
4268 
4269 	return true;
4270 }
4271 
4272 // "A non-list single-line container is an HTML element with local name
4273 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre",
4274 // or "xmp"."
4275 function isNonListSingleLineContainer(node) {
4276 	return isHtmlElementInArray(node, ["address", "div", "h1", "h2", "h3", "h4", "h5",
4277 		"h6", "listing", "p", "pre", "xmp"]);
4278 }
4279 
4280 // "A single-line container is either a non-list single-line container, or an
4281 // HTML element with local name "li", "dt", or "dd"."
4282 function isSingleLineContainer(node) {
4283 	return isNonListSingleLineContainer(node)
4284 		|| isHtmlElementInArray(node, ["li", "dt", "dd"]);
4285 }
4286 
4287 // "The default single-line container name is "p"."
4288 var defaultSingleLineContainerName = "p";
4289 
4290 
4291 //@}
4292 ///// Assorted block formatting command algorithms /////
4293 //@{
4294 
4295 function fixDisallowedAncestors(node, range) {
4296 	// "If node is not editable, abort these steps."
4297 	if (!isEditable(node)) {
4298 		return;
4299 	}
4300 
4301 	// "If node is not an allowed child of any of its ancestors in the same
4302 	// editing host, and is not an HTML element with local name equal to the
4303 	// default single-line container name:"
4304 	if ($_(getAncestors(node)).every(function(ancestor) {
4305 		return !inSameEditingHost(node, ancestor)
4306 			|| !isAllowedChild(node, ancestor)
4307 	})
4308 	&& !isHtmlElement_obsolete(node, defaultSingleLineContainerName)) {
4309 		// "If node is a dd or dt, wrap the one-node list consisting of node,
4310 		// with sibling criteria returning true for any dl with no attributes
4311 		// and false otherwise, and new parent instructions returning the
4312 		// result of calling createElement("dl") on the context object. Then
4313 		// abort these steps."
4314 		if (isHtmlElementInArray(node, ["dd", "dt"])) {
4315 			wrap([node],
4316 				function(sibling) { return isNamedHtmlElement(sibling, 'dl') && !sibling.attributes.length },
4317 				function() { return document.createElement("dl") },
4318 				range
4319 			);
4320 			return;
4321 		}
4322 
4323 		// "If node is not a prohibited paragraph child, abort these steps."
4324 		if (!isProhibitedParagraphChild(node)) {
4325 			return;
4326 		}
4327 
4328 		// "Set the tag name of node to the default single-line container name,
4329 		// and let node be the result."
4330 		node = setTagName(node, defaultSingleLineContainerName, range);
4331 
4332 		ensureContainerEditable(node);
4333 
4334 		// "Fix disallowed ancestors of node."
4335 		fixDisallowedAncestors(node, range);
4336 
4337 		// "Let descendants be all descendants of node."
4338 		var descendants = getDescendants(node);
4339 
4340 		// "Fix disallowed ancestors of each member of descendants."
4341 		for (var i = 0; i < descendants.length; i++) {
4342 			fixDisallowedAncestors(descendants[i], range);
4343 		}
4344 
4345 		// "Abort these steps."
4346 		return;
4347 	}
4348 
4349 	// "Record the values of the one-node list consisting of node, and let
4350 	// values be the result."
4351 	var values = recordValues([node]);
4352 
4353 	// "While node is not an allowed child of its parent, split the parent of
4354 	// the one-node list consisting of node."
4355 	while (!isAllowedChild(node, node.parentNode)) {
4356 		// If the parent contains only this node and possibly empty text nodes, we rather want to unwrap the node, instead of splitting.
4357 		// With splitting, we would get empty nodes, like:
4358 		// split: <p><p>foo</p></p> -> <p></p><p>foo</p> (bad)
4359 		// unwrap: <p><p>foo</p></p> -> <p>foo</p> (good)
4360 
4361 		// First remove empty text nodes that are children of the parent and correct the range if necessary
4362 		// we do this to have the node being the only child of its parent, so that we can replace the parent with the node
4363 		for (var i = node.parentNode.childNodes.length - 1; i >= 0; --i) {
4364 			if (node.parentNode.childNodes[i].nodeType == 3 && node.parentNode.childNodes[i].data.length == 0) {
4365 				// we remove the empty text node
4366 				node.parentNode.removeChild(node.parentNode.childNodes[i]);
4367 
4368 				// if the range points to somewhere behind the removed text node, we reduce the offset
4369 				if (range.startContainer == node.parentNode && range.startOffset > i) {
4370 					range.startOffset--;
4371 				}
4372 				if (range.endContainer == node.parentNode && range.endOffset > i) {
4373 					range.endOffset--;
4374 				}
4375 			}
4376 		}
4377 
4378 		// now that the parent has only the node as child (because we
4379 		// removed any existing empty text nodes), we can safely unwrap the
4380 		// node's contents, and correct the range if necessary
4381 		if (node.parentNode.childNodes.length == 1) {
4382 			var newStartOffset = range.startOffset;
4383 			var newEndOffset = range.endOffset;
4384 
4385 			if (range.startContainer === node.parentNode && range.startOffset > getNodeIndex(node)) {
4386 				// the node (1 element) will be replaced by its contents (contents().length elements)
4387 				newStartOffset = range.startOffset + (jQuery(node).contents().length - 1);
4388 			}
4389 			if (range.endContainer === node.parentNode && range.endOffset > getNodeIndex(node)) {
4390 				// the node (1 element) will be replaced by its contents (contents().length elements)
4391 				newEndOffset = range.endOffset + (jQuery(node).contents().length - 1);
4392 			}
4393 			jQuery(node).contents().unwrap();
4394 			range.startOffset = newStartOffset;
4395 			range.endOffset = newEndOffset;
4396 			// after unwrapping, we are done
4397 			break;
4398 		} else {
4399 			// store the original parent
4400 			var originalParent = node.parentNode;
4401 			splitParent([node], range);
4402 			// check whether the parent did not change, so the split did not work, e.g.
4403 			// because we already reached the editing host itself.
4404 			// this situation can occur, e.g. when we insert a paragraph into an contenteditable span
4405 			// in such cases, we just unwrap the contents of the paragraph
4406 			if (originalParent === node.parentNode) {
4407 				// so we unwrap now
4408 				var newStartOffset = range.startOffset;
4409 				var newEndOffset = range.endOffset;
4410 
4411 				if (range.startContainer === node.parentNode && range.startOffset > getNodeIndex(node)) {
4412 					// the node (1 element) will be replaced by its contents (contents().length elements)
4413 					newStartOffset = range.startOffset + (jQuery(node).contents().length - 1);
4414 				}
4415 				if (range.endContainer === node.parentNode && range.endOffset > getNodeIndex(node)) {
4416 					// the node (1 element) will be replaced by its contents (contents().length elements)
4417 					newEndOffset = range.endOffset + (jQuery(node).contents().length - 1);
4418 				}
4419 				jQuery(node).contents().unwrap();
4420 				range.startOffset = newStartOffset;
4421 				range.endOffset = newEndOffset;
4422 				// after unwrapping, we are done
4423 				break;
4424 			}
4425 		}
4426 	}
4427 
4428 	// "Restore the values from values."
4429 	restoreValues(values, range);
4430 }
4431 
4432 /**
4433  * This method "normalizes" sublists of the given item (which is supposed to be a LI):
4434  * If sublists are found in the LI element, they are moved directly into the outer list.
4435  * @param item item
4436  * @param range range, which will be modified if necessary
4437  */
4438 function normalizeSublists(item, range) {
4439 	// "If item is not an li or it is not editable or its parent is not
4440 	// editable, abort these steps."
4441 4442 	if (!isNamedHtmlElement(item, 'LI')
4443 	|| !isEditable(item)
4444 	|| !isEditable(item.parentNode)) {
4445 		return;
4446 	}
4447 
4448 	// "Let new item be null."
4449 	var newItem = null;
4450 
4451 	// "While item has an ol or ul child:"
4452 	while ($_(item.childNodes).some( function (node) { return isHtmlElementInArray(node, ["OL", "UL"]) })) {
4453 		// "Let child be the last child of item."
4454 		var child = item.lastChild;
4455 
4456 		// "If child is an ol or ul, or new item is null and child is a Text
4457 		// node whose data consists of zero of more space characters:"
4458 		if (isHtmlElementInArray(child, ["OL", "UL"])
4459 		|| (!newItem && child.nodeType == $_.Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) {
4460 			// "Set new item to null."
4461 			newItem = null;
4462 
4463 			// "Insert child into the parent of item immediately following
4464 			// item, preserving ranges."
4465 			movePreservingRanges(child, item.parentNode, 1 + getNodeIndex(item), range);
4466 
4467 		// "Otherwise:"
4468 		} else {
4469 			// "If new item is null, let new item be the result of calling
4470 			// createElement("li") on the ownerDocument of item, then insert
4471 			// new item into the parent of item immediately after item."
4472 			if (!newItem) {
4473 				newItem = item.ownerDocument.createElement("li");
4474 				item.parentNode.insertBefore(newItem, item.nextSibling);
4475 			}
4476 
4477 			// "Insert child into new item as its first child, preserving
4478 			// ranges."
4479 			movePreservingRanges(child, newItem, 0, range);
4480 		}
4481 	}
4482 }
4483 
4484 /**
4485  * This method is the exact opposite of normalizeSublists.
4486  * List nodes directly nested into each other are corrected to be nested in li elements (so that the resulting lists conform the html5 specification)
4487  * @param item list node
4488  * @param range range, which is preserved when modifying the list
4489  */
4490 function unNormalizeSublists(item, range) {
4491 	// "If item is not an ol or ol or it is not editable or its parent is not
4492 	// editable, abort these steps."
4493 	if (!isHtmlElementInArray(item, ["OL", "UL"])
4494 	|| !isEditable(item)) {
4495 		return;
4496 	}
4497 
4498 	var $list = jQuery(item);
4499 	$list.children("ol,ul").each(function(index, sublist) {
4500 		if (isNamedHtmlElement(sublist.previousSibling, "LI")) {
4501 			// move the sublist into the LI
4502 			movePreservingRanges(sublist, sublist.previousSibling, sublist.previousSibling.childNodes.length, range);
4503 		}
4504 	});
4505 }
4506 
4507 function getSelectionListState() {
4508 	// "Block-extend the active range, and let new range be the result."
4509 	var newRange = blockExtend(getActiveRange());
4510 
4511 	// "Let node list be a list of nodes, initially empty."
4512 	//
4513 	// "For each node contained in new range, append node to node list if the
4514 	// last member of node list (if any) is not an ancestor of node; node is
4515 	// editable; node is not an indentation element; and node is either an ol
4516 	// or ul, or the child of an ol or ul, or an allowed child of "li"."
4517 	var nodeList = getContainedNodes(newRange, function(node) {
4518 		return isEditable(node)
4519 			&& !isIndentationElement(node)
4520 			&& (isHtmlElementInArray(node, ["ol", "ul"])
4521 			|| isHtmlElementInArray(node.parentNode, ["ol", "ul"])
4522 			|| isAllowedChild(node, "li"));
4523 	});
4524 
4525 	// "If node list is empty, return "none"."
4526 	if (!nodeList.length) {
4527 		return "none";
4528 	}
4529 
4530 	// "If every member of node list is either an ol or the child of an ol or
4531 	// the child of an li child of an ol, and none is a ul or an ancestor of a
4532 	// ul, return "ol"."
4533 	if ($_(nodeList).every(function(node) {
4534 		return isNamedHtmlElement(node, 'ol')
4535 			|| isNamedHtmlElement(node.parentNode, "ol")
4536 			|| (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ol"));
4537 	})
4538 	&& !$_( nodeList ).some(function(node) { return isNamedHtmlElement(node, 'ul') || ("querySelector" in node && node.querySelector("ul")) })) {
4539 		return "ol";
4540 	}
4541 
4542 	// "If every member of node list is either a ul or the child of a ul or the
4543 4544 	// child of an li child of a ul, and none is an ol or an ancestor of an ol,
4545 	// return "ul"."
4546 	if ($_(nodeList).every(function(node) {
4547 		return isNamedHtmlElement(node, 'ul')
4548 			|| isNamedHtmlElement(node.parentNode, "ul")
4549 			|| (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ul"));
4550 	})
4551 	&& !$_( nodeList ).some(function(node) { return isNamedHtmlElement(node, 'ol') || ("querySelector" in node && node.querySelector("ol")) })) {
4552 		return "ul";
4553 	}
4554 
4555 	var hasOl = $_( nodeList ).some(function(node) {
4556 		return isNamedHtmlElement(node, 'ol')
4557 			|| isNamedHtmlElement(node.parentNode, "ol")
4558 			|| ("querySelector" in node && node.querySelector("ol"))
4559 			|| (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ol"));
4560 	});
4561 	var hasUl = $_( nodeList ).some(function(node) {
4562 		return isNamedHtmlElement(node, 'ul')
4563 			|| isNamedHtmlElement(node.parentNode, "ul")
4564 			|| ("querySelector" in node && node.querySelector("ul"))
4565 			|| (isNamedHtmlElement(node.parentNode, "li") && isNamedHtmlElement(node.parentNode.parentNode, "ul"));
4566 	});
4567 	// "If some member of node list is either an ol or the child or ancestor of
4568 	// an ol or the child of an li child of an ol, and some member of node list
4569 	// is either a ul or the child or ancestor of a ul or the child of an li
4570 	// child of a ul, return "mixed"."
4571 	if (hasOl && hasUl) {
4572 		return "mixed";
4573 	}
4574 
4575 	// "If some member of node list is either an ol or the child or ancestor of
4576 	// an ol or the child of an li child of an ol, return "mixed ol"."
4577 	if (hasOl) {
4578 		return "mixed ol";
4579 	}
4580 
4581 	// "If some member of node list is either a ul or the child or ancestor of
4582 	// a ul or the child of an li child of a ul, return "mixed ul"."
4583 	if (hasUl) {
4584 		return "mixed ul";
4585 	}
4586 
4587 	// "Return "none"."
4588 	return "none";
4589 }
4590 
4591 function getAlignmentValue(node) {
4592 	// "While node is neither null nor an Element, or it is an Element but its
4593 	// "display" property has resolved value "inline" or "none", set node to
4594 	// its parent."
4595 	while ((node && node.nodeType != $_.Node.ELEMENT_NODE)
4596 	|| (node.nodeType == $_.Node.ELEMENT_NODE
4597 	&& jQuery.inArray($_.getComputedStyle(node).display, ["inline", "none"]) != -1)) {
4598 		node = node.parentNode;
4599 	}
4600 
4601 	// "If node is not an Element, return "left"."
4602 	if (!node || node.nodeType != $_.Node.ELEMENT_NODE) {
4603 		return "left";
4604 	}
4605 
4606 	var resolvedValue = $_.getComputedStyle(node).textAlign
4607 		// Hack around browser non-standardness
4608 		.replace(/^-(moz|webkit)-/, "")
4609 		.replace(/^auto$/, "start");
4610 
4611 	// "If node's "text-align" property has resolved value "start", return
4612 	// "left" if the directionality of node is "ltr", "right" if it is "rtl"."
4613 	if (resolvedValue == "start") {
4614 		return getDirectionality(node) == "ltr" ? "left" : "right";
4615 	}
4616 
4617 	// "If node's "text-align" property has resolved value "end", return
4618 	// "right" if the directionality of node is "ltr", "left" if it is "rtl"."
4619 	if (resolvedValue == "end") {
4620 		return getDirectionality(node) == "ltr" ? "right" : "left";
4621 	}
4622 
4623 	// "If node's "text-align" property has resolved value "center", "justify",
4624 	// "left", or "right", return that value."
4625 	if (jQuery.inArray(resolvedValue, ["center", "justify", "left", "right"]) != -1) {
4626 		return resolvedValue;
4627 	}
4628 
4629 	// "Return "left"."
4630 	return "left";
4631 }
4632 
4633 //@}
4634 ///// Block-extending a range /////
4635 //@{
4636 
4637 // "A boundary point (node, offset) is a block start point if either node's
4638 // parent is null and offset is zero; or node has a child with index offset −
4639 // 1, and that child is either a visible block node or a visible br."
4640 function isBlockStartPoint(node, offset) {
4641 	return (!node.parentNode && offset == 0)
4642 		|| (0 <= offset - 1
4643 		&& offset - 1 < node.childNodes.length
4644 		&& isVisible(node.childNodes[offset - 1])
4645 		&& (isBlockNode(node.childNodes[offset - 1])
4646 		|| isNamedHtmlElement(node.childNodes[offset - 1], "br")));
4647 }
4648 
4649 // "A boundary point (node, offset) is a block end point if either node's
4650 // parent is null and offset is node's length; or node has a child with index
4651 // offset, and that child is a visible block node."
4652 function isBlockEndPoint(node, offset) {
4653 	return (!node.parentNode && offset == getNodeLength(node))
4654 		|| (offset < node.childNodes.length
4655 		&& isVisible(node.childNodes[offset])
4656 		&& isBlockNode(node.childNodes[offset]));
4657 }
4658 
4659 // "A boundary point is a block boundary point if it is either a block start
4660 // point or a block end point."
4661 function isBlockBoundaryPoint(node, offset) {
4662 	return isBlockStartPoint(node, offset)
4663 		|| isBlockEndPoint(node, offset);
4664 }
4665 
4666 function blockExtend(range) {
4667 	// "Let start node, start offset, end node, and end offset be the start
4668 	// and end nodes and offsets of the range."
4669 	var startNode = range.startContainer;
4670 	var startOffset = range.startOffset;
4671 	var endNode = range.endContainer;
4672 	var endOffset = range.endOffset;
4673 
4674 	// "If some ancestor container of start node is an li, set start offset to
4675 	// the index of the last such li in tree order, and set start node to that
4676 	// li's parent."
4677 	var liAncestors = $_( getAncestors(startNode).concat(startNode) )
4678 		.filter(function(ancestor) { return isNamedHtmlElement(ancestor, 'li') })
4679 		.slice(-1);
4680 	if (liAncestors.length) {
4681 		startOffset = getNodeIndex(liAncestors[0]);
4682 		startNode = liAncestors[0].parentNode;
4683 	}
4684 
4685 	// "If (start node, start offset) is not a block start point, repeat the
4686 	// following steps:"
4687 	if (!isBlockStartPoint(startNode, startOffset)) do {
4688 		// "If start offset is zero, set it to start node's index, then set
4689 		// start node to its parent."
4690 		if (startOffset == 0) {
4691 			startOffset = getNodeIndex(startNode);
4692 			startNode = startNode.parentNode;
4693 
4694 		// "Otherwise, subtract one from start offset."
4695 		} else {
4696 			startOffset--;
4697 		}
4698 
4699 		// "If (start node, start offset) is a block boundary point, break from
4700 		// this loop."
4701 	} while (!isBlockBoundaryPoint(startNode, startOffset));
4702 
4703 	// "While start offset is zero and start node's parent is not null, set
4704 	// start offset to start node's index, then set start node to its parent."
4705 	while (startOffset == 0
4706 	&& startNode.parentNode) {
4707 		startOffset = getNodeIndex(startNode);
4708 		startNode = startNode.parentNode;
4709 	}
4710 
4711 	// "If some ancestor container of end node is an li, set end offset to one
4712 	// plus the index of the last such li in tree order, and set end node to
4713 	// that li's parent."
4714 	var liAncestors = $_( getAncestors(endNode).concat(endNode) )
4715 		.filter(function(ancestor) { return isNamedHtmlElement(ancestor, 'li') })
4716 		.slice(-1);
4717 	if (liAncestors.length) {
4718 		endOffset = 1 + getNodeIndex(liAncestors[0]);
4719 		endNode = liAncestors[0].parentNode;
4720 	}
4721 
4722 	// "If (end node, end offset) is not a block end point, repeat the
4723 	// following steps:"
4724 	if (!isBlockEndPoint(endNode, endOffset)) do {
4725 		// "If end offset is end node's length, set it to one plus end node's
4726 		// index, then set end node to its parent."
4727 		if (endOffset == getNodeLength(endNode)) {
4728 			endOffset = 1 + getNodeIndex(endNode);
4729 			endNode = endNode.parentNode;
4730 
4731 		// "Otherwise, add one to end offset.
4732 		} else {
4733 			endOffset++;
4734 		}
4735 
4736 		// "If (end node, end offset) is a block boundary point, break from
4737 		// this loop."
4738 	} while (!isBlockBoundaryPoint(endNode, endOffset));
4739 
4740 	// "While end offset is end node's length and end node's parent is not
4741 	// null, set end offset to one plus end node's index, then set end node to
4742 	// its parent."
4743 	while (endOffset == getNodeLength(endNode)
4744 	&& endNode.parentNode) {
4745 		endOffset = 1 + getNodeIndex(endNode);
4746 		endNode = endNode.parentNode;
4747 	}
4748 
4749 	// "Let new range be a new range whose start and end nodes and offsets
4750 	// are start node, start offset, end node, and end offset."
4751 	var newRange = Aloha.createRange();
4752 	newRange.setStart(startNode, startOffset);
4753 	newRange.setEnd(endNode, endOffset);
4754 
4755 	// "Return new range."
4756 	return newRange;
4757 }
4758 
4759 function followsLineBreak(node) {
4760 	// "Let offset be zero."
4761 	var offset = 0;
4762 
4763 	// "While (node, offset) is not a block boundary point:"
4764 	while (!isBlockBoundaryPoint(node, offset)) {
4765 		// "If node has a visible child with index offset minus one, return
4766 		// false."
4767 		if (0 <= offset - 1
4768 		&& offset - 1 < node.childNodes.length
4769 		&& isVisible(node.childNodes[offset - 1])) {
4770 			return false;
4771 		}
4772 
4773 		// "If offset is zero or node has no children, set offset to node's
4774 		// index, then set node to its parent."
4775 		if (offset == 0
4776 		|| !node.hasChildNodes()) {
4777 			offset = getNodeIndex(node);
4778 			node = node.parentNode;
4779 
4780 		// "Otherwise, set node to its child with index offset minus one, then
4781 		// set offset to node's length."
4782 		} else {
4783 			node = node.childNodes[offset - 1];
4784 			offset = getNodeLength(node);
4785 		}
4786 	}
4787 
4788 	// "Return true."
4789 	return true;
4790 }
4791 
4792 function precedesLineBreak(node) {
4793 	// "Let offset be node's length."
4794 	var offset = getNodeLength(node);
4795 
4796 	// "While (node, offset) is not a block boundary point:"
4797 	while (!isBlockBoundaryPoint(node, offset)) {
4798 		// "If node has a visible child with index offset, return false."
4799 		if (offset < node.childNodes.length
4800 		&& isVisible(node.childNodes[offset])) {
4801 			return false;
4802 		}
4803 
4804 		// "If offset is node's length or node has no children, set offset to
4805 		// one plus node's index, then set node to its parent."
4806 		if (offset == getNodeLength(node)
4807 		|| !node.hasChildNodes()) {
4808 			offset = 1 + getNodeIndex(node);
4809 			node = node.parentNode;
4810 
4811 		// "Otherwise, set node to its child with index offset and set offset
4812 		// to zero."
4813 		} else {
4814 			node = node.childNodes[offset];
4815 			offset = 0;
4816 		}
4817 	}
4818 
4819 	// "Return true."
4820 	return true;
4821 }
4822 
4823 //@}
4824 ///// Recording and restoring overrides /////
4825 //@{
4826 
4827 function recordCurrentOverrides( range ) {
4828 	// "Let overrides be a list of (string, string or boolean) ordered pairs,
4829 	// initially empty."
4830 	var overrides = [];
4831 
4832 	// "If there is a value override for "createLink", add ("createLink", value
4833 	// override for "createLink") to overrides."
4834 	if (getValueOverride("createlink" ,range) !== undefined) {
4835 		overrides.push(["createlink", getValueOverride("createlink", range)]);
4836 	}
4837 
4838 	// "For each command in the list "bold", "italic", "strikethrough",
4839 	// "subscript", "superscript", "underline", in order: if there is a state
4840 	// override for command, add (command, command's state override) to
4841 	// overrides."
4842 	$_( ["bold", "italic", "strikethrough", "subscript", "superscript",
4843 	"underline"] ).forEach(function(command) {
4844 		if (getStateOverride(command, range) !== undefined) {
4845 			overrides.push([command, getStateOverride(command, range)]);
4846 		}
4847 	});
4848 
4849 	// "For each command in the list "fontName", "fontSize", "foreColor",
4850 	// "hiliteColor", in order: if there is a value override for command, add
4851 	// (command, command's value override) to overrides."
4852 	$_( ["fontname", "fontsize", "forecolor",
4853 	"hilitecolor"] ).forEach(function(command) {
4854 		if (getValueOverride(command, range) !== undefined) {
4855 			overrides.push([command, getValueOverride(command, range)]);
4856 		}
4857 	});
4858 
4859 	// "Return overrides."
4860 	return overrides;
4861 }
4862 
4863 function recordCurrentStatesAndValues(range) {
4864 	// "Let overrides be a list of (string, string or boolean) ordered pairs,
4865 	// initially empty."
4866 	var overrides = [];
4867 
4868 	// "Let node be the first editable Text node effectively contained in the
4869 	// active range, or null if there is none."
4870 	var node = $_( getAllEffectivelyContainedNodes(range) )
4871 		.filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE })[0];
4872 
4873 	// "If node is null, return overrides."
4874 	if (!node) {
4875 		return overrides;
4876 	}
4877 
4878 	// "Add ("createLink", value for "createLink") to overrides."
4879 	overrides.push(["createlink", commands.createlink.value(range)]);
4880 
4881 	// "For each command in the list "bold", "italic", "strikethrough",
4882 	// "subscript", "superscript", "underline", in order: if node's effective
4883 	// command value for command is one of its inline command activated values,
4884 	// add (command, true) to overrides, and otherwise add (command, false) to
4885 	// overrides."
4886 	$_( ["bold", "italic", "strikethrough", "subscript", "superscript",
4887 	"underline"] ).forEach(function(command) {
4888 		if ($_(commands[command].inlineCommandActivatedValues)
4889 		.indexOf(getEffectiveCommandValue(node, command)) != -1) {
4890 			overrides.push([command, true]);
4891 		} else {
4892 			overrides.push([command, false]);
4893 		}
4894 	});
4895 
4896 	// "For each command in the list "fontName", "foreColor", "hiliteColor", in
4897 	// order: add (command, command's value) to overrides."
4898 
4899 	$_( ["fontname", "fontsize", "forecolor", "hilitecolor"] ).forEach(function(command) {
4900 		overrides.push([command, commands[command].value(range)]);
4901 	});
4902 
4903 	// "Add ("fontSize", node's effective command value for "fontSize") to
4904 	// overrides."
4905 	overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]);
4906 
4907 	// "Return overrides."
4908 	return overrides;
4909 }
4910 
4911 function restoreStatesAndValues(overrides, range) {
4912 	// "Let node be the first editable Text node effectively contained in the
4913 	// active range, or null if there is none."
4914 	var node = $_( getAllEffectivelyContainedNodes(range) )
4915 		.filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE })[0];
4916 
4917 	// "If node is not null, then for each (command, override) pair in
4918 	// overrides, in order:"
4919 	if (node) {
4920 		for (var i = 0; i < overrides.length; i++) {
4921 4922 			var command = overrides[i][0];
4923 			var override = overrides[i][1];
4924 
4925 			// "If override is a boolean, and queryCommandState(command)
4926 			// returns something different from override, call
4927 			// execCommand(command)."
4928 			if (typeof override == "boolean"
4929 			&& myQueryCommandState(command, range) != override) {
4930 				myExecCommand(command);
4931 
4932 			// "Otherwise, if override is a string, and command is not
4933 			// "fontSize", and queryCommandValue(command) returns something not
4934 			// equivalent to override, call execCommand(command, false,
4935 			// override)."
4936 			} else if (typeof override == "string"
4937 			&& command != "fontsize"
4938 			&& !areEquivalentValues(command, myQueryCommandValue(command, range), override)) {
4939 				myExecCommand(command, false, override, range);
4940 
4941 			// "Otherwise, if override is a string; and command is "fontSize";
4942 			// and either there is a value override for "fontSize" that is not
4943 			// equal to override, or there is no value override for "fontSize"
4944 			// and node's effective command value for "fontSize" is not loosely
4945 			// equivalent to override: call execCommand("fontSize", false,
4946 			// override)."
4947 			} else if (typeof override == "string"
4948 			&& command == "fontsize"
4949 			&& (
4950 				(
4951 					getValueOverride("fontsize", range) !== undefined
4952 					&& getValueOverride("fontsize", range) !== override
4953 				) || (
4954 					getValueOverride("fontsize", range) === undefined
4955 					&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
4956 				)
4957 			)) {
4958 				myExecCommand("fontsize", false, override, range);
4959 
4960 			// "Otherwise, continue this loop from the beginning."
4961 			} else {
4962 				continue;
4963 			}
4964 
4965 			// "Set node to the first editable Text node effectively contained
4966 			// in the active range, if there is one."
4967 			node = $_( getAllEffectivelyContainedNodes(range) )
4968 				.filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE })[0]
4969 				|| node;
4970 		}
4971 
4972 	// "Otherwise, for each (command, override) pair in overrides, in order:"
4973 	} else {
4974 		for (var i = 0; i < overrides.length; i++) {
4975 			var command = overrides[i][0];
4976 			var override = overrides[i][1];
4977 
4978 			// "If override is a boolean, set the state override for command to
4979 			// override."
4980 			if (typeof override == "boolean") {
4981 				setStateOverride(command, override, range);
4982 			}
4983 
4984 			// "If override is a string, set the value override for command to
4985 			// override."
4986 			if (typeof override == "string") {
4987 				setValueOverride(command, override, range);
4988 			}
4989 		}
4990 	}
4991 }
4992 
4993 //@}
4994 ///// Deleting the contents of a range /////
4995 //@{
4996 
4997 function deleteContents() {
4998 	// We accept several different calling conventions:
4999 	//
5000 	// 1) A single argument, which is a range.
5001 	//
5002 	// 2) Two arguments, the first being a range and the second flags.
5003 	//
5004 	// 3) Four arguments, the start and end of a range.
5005 	//
5006 	// 4) Five arguments, the start and end of a range plus flags.
5007 	//
5008 	// The flags argument is a dictionary that can have up to two keys,
5009 	// blockMerging and stripWrappers, whose corresponding values are
5010 	// interpreted as boolean.  E.g., {stripWrappers: false}.
5011 	var range;
5012 	var flags = {};
5013 
5014 	if (arguments.length < 3) {
5015 		range = arguments[0];
5016 	} else {
5017 		range = Aloha.createRange();
5018 		range.setStart(arguments[0], arguments[1]);
5019 		range.setEnd(arguments[2], arguments[3]);
5020 	}
5021 	if (arguments.length == 2) {
5022 		flags = arguments[1];
5023 	}
5024 	if (arguments.length == 5) {
5025 		flags = arguments[4];
5026 	}
5027 
5028 	var blockMerging = "blockMerging" in flags ? !!flags.blockMerging : true;
5029 	var stripWrappers = "stripWrappers" in flags ? !!flags.stripWrappers : true;
5030 
5031 	// "If range is null, abort these steps and do nothing."
5032 	if (!range) {
5033 		return;
5034 	}
5035 
5036 	// "Let start node, start offset, end node, and end offset be range's start
5037 	// and end nodes and offsets."
5038 	var startNode = range.startContainer;
5039 	var startOffset = range.startOffset;
5040 	var endNode = range.endContainer;
5041 	var endOffset = range.endOffset;
5042 
5043 	// "While start node has at least one child:"
5044 	while (startNode.hasChildNodes()) {
5045 		// "If start offset is start node's length, and start node's parent is
5046 		// in the same editing host, and start node is an inline node, set
5047 		// start offset to one plus the index of start node, then set start
5048 		// node to its parent and continue this loop from the beginning."
5049 		if (startOffset == getNodeLength(startNode)
5050 		&& inSameEditingHost(startNode, startNode.parentNode)
5051 		&& isInlineNode(startNode)) {
5052 			startOffset = 1 + getNodeIndex(startNode);
5053 			startNode = startNode.parentNode;
5054 			continue;
5055 		}
5056 
5057 		// "If start offset is start node's length, break from this loop."
5058 		if (startOffset == getNodeLength(startNode)) {
5059 			break;
5060 		}
5061 
5062 		// "Let reference node be the child of start node with index equal to
5063 		// start offset."
5064 		var referenceNode = startNode.childNodes[startOffset];
5065 
5066 		// "If reference node is a block node or an Element with no children,
5067 		// or is neither an Element nor a Text node, break from this loop."
5068 		if (isBlockNode(referenceNode)
5069 		|| (referenceNode.nodeType == $_.Node.ELEMENT_NODE
5070 		&& !referenceNode.hasChildNodes())
5071 		|| (referenceNode.nodeType != $_.Node.ELEMENT_NODE
5072 		&& referenceNode.nodeType != $_.Node.TEXT_NODE)) {
5073 			break;
5074 		}
5075 
5076 		// "Set start node to reference node and start offset to 0."
5077 		startNode = referenceNode;
5078 		startOffset = 0;
5079 	}
5080 
5081 	// "While end node has at least one child:"
5082 	while (endNode.hasChildNodes()) {
5083 		// "If end offset is 0, and end node's parent is in the same editing
5084 		// host, and end node is an inline node, set end offset to the index of
5085 		// end node, then set end node to its parent and continue this loop
5086 		// from the beginning."
5087 		if (endOffset == 0
5088 		&& inSameEditingHost(endNode, endNode.parentNode)
5089 		&& isInlineNode(endNode)) {
5090 			endOffset = getNodeIndex(endNode);
5091 			endNode = endNode.parentNode;
5092 			continue;
5093 		}
5094 
5095 		// "If end offset is 0, break from this loop."
5096 		if (endOffset == 0) {
5097 			break;
5098 		}
5099 
5100 		// "Let reference node be the child of end node with index equal to end
5101 		// offset minus one."
5102 		var referenceNode = endNode.childNodes[endOffset - 1];
5103 
5104 		// "If reference node is a block node or an Element with no children,
5105 		// or is neither an Element nor a Text node, break from this loop."
5106 		if (isBlockNode(referenceNode)
5107 		|| (referenceNode.nodeType == $_.Node.ELEMENT_NODE
5108 		&& !referenceNode.hasChildNodes())
5109 		|| (referenceNode.nodeType != $_.Node.ELEMENT_NODE
5110 		&& referenceNode.nodeType != $_.Node.TEXT_NODE)) {
5111 			break;
5112 		}
5113 
5114 		// "Set end node to reference node and end offset to the length of
5115 		// reference node."
5116 		endNode = referenceNode;
5117 		endOffset = getNodeLength(referenceNode);
5118 	}
5119 
5120 	// "If (end node, end offset) is not after (start node, start offset), set
5121 	// range's end to its start and abort these steps."
5122 	if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") {
5123 		range.setEnd(range.startContainer, range.startOffset);
5124 		return;
5125 	}
5126 
5127 	// "If start node is a Text node and start offset is 0, set start offset to
5128 	// the index of start node, then set start node to its parent."
5129 	if (startNode.nodeType == $_.Node.TEXT_NODE
5130 	&& startOffset == 0
5131 	&& startNode != endNode) {
5132 //		startOffset = getNodeIndex(startNode);
5133 //		startNode = startNode.parentNode;
5134 	}
5135 
5136 	// "If end node is a Text node and end offset is its length, set end offset
5137 	// to one plus the index of end node, then set end node to its parent."
5138 	if (endNode.nodeType == $_.Node.TEXT_NODE
5139 	&& endOffset == getNodeLength(endNode)
5140 	&& startNode != endNode) {
5141 		endOffset = 1 + getNodeIndex(endNode);
5142 		endNode = endNode.parentNode;
5143 	}
5144 
5145 	// "Set range's start to (start node, start offset) and its end to (end
5146 	// node, end offset)."
5147 	range.setStart(startNode, startOffset);
5148 	range.setEnd(endNode, endOffset);
5149 
5150 	// "Let start block be the start node of range."
5151 	var startBlock = range.startContainer;
5152 
5153 	// "While start block's parent is in the same editing host and start block
5154 	// is an inline node, set start block to its parent."
5155 	while (inSameEditingHost(startBlock, startBlock.parentNode)
5156 	&& isInlineNode(startBlock)) {
5157 		startBlock = startBlock.parentNode;
5158 	}
5159 
5160 	// "If start block is neither a block node nor an editing host, or "span"
5161 	// is not an allowed child of start block, or start block is a td or th,
5162 	// set start block to null."
5163 	if ((!isBlockNode(startBlock) && !isEditingHost(startBlock))
5164 	|| !isAllowedChild("span", startBlock)
5165 	|| isHtmlElementInArray(startBlock, ["td", "th"])) {
5166 		startBlock = null;
5167 	}
5168 
5169 	// "Let end block be the end node of range."
5170 	var endBlock = range.endContainer;
5171 	
5172 	// "While end block's parent is in the same editing host and end block is
5173 	// an inline node, set end block to its parent."
5174 	while (inSameEditingHost(endBlock, endBlock.parentNode)
5175 	&& isInlineNode(endBlock)) {
5176 		endBlock = endBlock.parentNode;
5177 	}
5178 	
5179 	// "If end block is neither a block node nor an editing host, or "span" is
5180 	// not an allowed child of end block, or end block is a td or th, set end
5181 	// block to null."
5182 	if ((!isBlockNode(endBlock) && !isEditingHost(endBlock))
5183 	|| !isAllowedChild("span", endBlock)
5184 	|| isHtmlElementInArray(endBlock, ["td", "th"])) {
5185 		endBlock = null;
5186 	}
5187 
5188 	// "Record current states and values, and let overrides be the result."
5189 	var overrides = recordCurrentStatesAndValues(range);
5190 	// "If start node and end node are the same, and start node is an editable
5191 	// Text node:"
5192 	if (startNode == endNode
5193 	&& isEditable(startNode)
5194 	&& startNode.nodeType == $_.Node.TEXT_NODE) {
5195 		// "Let parent be the parent of node."
5196 		var parent_ = startNode.parentNode;
5197 
5198 		// "Call deleteData(start offset, end offset − start offset) on start
5199 		// node."
5200 		startNode.deleteData(startOffset, endOffset - startOffset);
5201 
5202 		// if deleting the text moved two spaces together, we replace the left one by a  , which makes the two spaces a visible
5203 		// two space sequence
5204 		if (startOffset > 0 && startNode.data.substr(startOffset - 1, 1) === ' '
5205 		&& startOffset < startNode.data.length && startNode.data.substr(startOffset, 1) === ' ') {
5206 			startNode.replaceData(startOffset - 1, 1, '\xa0');
5207 		}
5208 
5209 		// "Canonicalize whitespace at (start node, start offset)."
5210 		canonicalizeWhitespace(startNode, startOffset);
5211 
5212 		// "Set range's end to its start."
5213 		// Ok, also set the range's start to its start, because modifying the text 
5214 		// might have somehow corrupted the range
5215 		range.setStart(range.startContainer, range.startOffset);
5216 		range.setEnd(range.startContainer, range.startOffset);
5217 
5218 		// "Restore states and values from overrides."
5219 		restoreStatesAndValues(overrides, range);
5220 
5221 		// "If parent is editable or an editing host, is not an inline node,
5222 		// and has no children, call createElement("br") on the context object
5223 		// and append the result as the last child of parent."
5224 		// only do this, if the offsetHeight is 0
5225 		if ((isEditable(parent_) || isEditingHost(parent_))
5226 		&& !isInlineNode(parent_)) {
5227 			// TODO is this always correct?
5228 			ensureContainerEditable(parent_);
5229 		}
5230 
5231 		// "Abort these steps."
5232 		return;
5233 	}
5234 
5235 	// "If start node is an editable Text node, call deleteData() on it, with
5236 	// start offset as the first argument and (length of start node − start
5237 	// offset) as the second argument."
5238 	if (isEditable(startNode)
5239 	&& startNode.nodeType == $_.Node.TEXT_NODE) {
5240 		startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
5241 	}
5242 
5243 	// "Let node list be a list of nodes, initially empty."
5244 	//
5245 	// "For each node contained in range, append node to node list if the last
5246 	// member of node list (if any) is not an ancestor of node; node is
5247 	// editable; and node is not a thead, tbody, tfoot, tr, th, or td."
5248 	var nodeList = getContainedNodes(range,
5249 		function(node) {
5250 			return isEditable(node)
5251 				&& !isHtmlElementInArray(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
5252 		}
5253 	);
5254 
5255 	// "For each node in node list:"
5256 	for (var i = 0; i < nodeList.length; i++) {
5257 		var node = nodeList[i];
5258 
5259 		// "Let parent be the parent of node."
5260 		var parent_ = node.parentNode;
5261 
5262 		// "Remove node from parent."
5263 		parent_.removeChild(node);
5264 
5265 		// "If strip wrappers is true or parent is not an ancestor container of
5266 		// start node, while parent is an editable inline node with length 0,
5267 		// let grandparent be the parent of parent, then remove parent from
5268 		// grandparent, then set parent to grandparent."
5269 		if (stripWrappers
5270 		|| (!isAncestor(parent_, startNode) && parent_ != startNode)) {
5271 5272 			while (isEditable(parent_)
5273 			&& isInlineNode(parent_)
5274 			&& getNodeLength(parent_) == 0) {
5275 				var grandparent = parent_.parentNode;
5276 				grandparent.removeChild(parent_);
5277 				parent_ = grandparent;
5278 			}
5279 		}
5280 
5281 		// "If parent is editable or an editing host, is not an inline node,
5282 		// and has no children, call createElement("br") on the context object
5283 		// and append the result as the last child of parent."
5284 		// only do this, if the offsetHeight is 0
5285 		if ((isEditable(parent_) || isEditingHost(parent_))
5286 		&& !isInlineNode(parent_)) {
5287 			ensureContainerEditable(parent_);
5288 		}
5289 	}
5290 
5291 	// "If end node is an editable Text node, call deleteData(0, end offset) on
5292 	// it."
5293 	if (isEditable(endNode)
5294 	&& endNode.nodeType == $_.Node.TEXT_NODE) {
5295 		endNode.deleteData(0, endOffset);
5296 	}
5297 
5298 	// "Canonicalize whitespace at range's start."
5299 	canonicalizeWhitespace(range.startContainer, range.startOffset);
5300 
5301 	// "Canonicalize whitespace at range's end."
5302 	canonicalizeWhitespace(range.endContainer, range.endOffset);
5303 
5304 	// "If block merging is false, or start block or end block is null, or
5305 	// start block is not in the same editing host as end block, or start block
5306 	// and end block are the same:"
5307 	if (!blockMerging
5308 	|| !startBlock
5309 	|| !endBlock
5310 	|| !inSameEditingHost(startBlock, endBlock)
5311 	|| startBlock == endBlock) {
5312 		// "Set range's end to its start."
5313 		range.setEnd(range.startContainer, range.startOffset);
5314 
5315 		// "Restore states and values from overrides."
5316 		restoreStatesAndValues(overrides, range);
5317 
5318 		// "Abort these steps."
5319 		return;
5320 	}
5321 
5322 	// "If start block has one child, which is a collapsed block prop, remove
5323 	// its child from it."
5324 	if (startBlock.children.length == 1
5325 	&& isCollapsedBlockProp(startBlock.firstChild)) {
5326 		startBlock.removeChild(startBlock.firstChild);
5327 	}
5328 
5329 	// "If end block has one child, which is a collapsed block prop, remove its
5330 	// child from it."
5331 	if (endBlock.children.length == 1
5332 	&& isCollapsedBlockProp(endBlock.firstChild)) {
5333 		endBlock.removeChild(endBlock.firstChild);
5334 	}
5335 
5336 5337 	// "If start block is an ancestor of end block:"
5338 	if (isAncestor(startBlock, endBlock)) {
5339 		// "Let reference node be end block."
5340 		var referenceNode = endBlock;
5341 
5342 		// "While reference node is not a child of start block, set reference
5343 		// node to its parent."
5344 		while (referenceNode.parentNode != startBlock) {
5345 			referenceNode = referenceNode.parentNode;
5346 		}
5347 
5348 		// "Set the start and end of range to (start block, index of reference
5349 		// node)."
5350 		range.setStart(startBlock, getNodeIndex(referenceNode));
5351 		range.setEnd(startBlock, getNodeIndex(referenceNode));
5352 
5353 		// "If end block has no children:"
5354 		if (!endBlock.hasChildNodes()) {
5355 			// "While end block is editable and is the only child of its parent
5356 			// and is not a child of start block, let parent equal end block,
5357 			// then remove end block from parent, then set end block to
5358 			// parent."
5359 			while (isEditable(endBlock)
5360 			&& endBlock.parentNode.childNodes.length == 1
5361 			&& endBlock.parentNode != startBlock) {
5362 				var parent_ = endBlock;
5363 				parent_.removeChild(endBlock);
5364 				endBlock = parent_;
5365 			}
5366 
5367 			// "If end block is editable and is not an inline node, and its
5368 			// previousSibling and nextSibling are both inline nodes, call
5369 			// createElement("br") on the context object and insert it into end
5370 			// block's parent immediately after end block."
5371 
5372 			if (isEditable(endBlock)
5373 			&& !isInlineNode(endBlock)
5374 			&& isInlineNode(endBlock.previousSibling)
5375 			&& isInlineNode(endBlock.nextSibling)) {
5376 				endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling);
5377 			}
5378 
5379 			// "If end block is editable, remove it from its parent."
5380 			if (isEditable(endBlock)) {
5381 				endBlock.parentNode.removeChild(endBlock);
5382 			}
5383 
5384 			// "Restore states and values from overrides."
5385 			restoreStatesAndValues(overrides, range);
5386 
5387 			// "Abort these steps."
5388 			return;
5389 		}
5390 
5391 		// "If end block's firstChild is not an inline node, restore states and
5392 		// values from overrides, then abort these steps."
5393 		if (!isInlineNode(endBlock.firstChild)) {
5394 			restoreStatesAndValues(overrides, range);
5395 			return;
5396 		}
5397 
5398 		// "Let children be a list of nodes, initially empty."
5399 		var children = [];
5400 
5401 		// "Append the first child of end block to children."
5402 		children.push(endBlock.firstChild);
5403 
5404 		// "While children's last member is not a br, and children's last
5405 		// member's nextSibling is an inline node, append children's last
5406 		// member's nextSibling to children."
5407 		while (!isNamedHtmlElement(children[children.length - 1], "br")
5408 		&& isInlineNode(children[children.length - 1].nextSibling)) {
5409 			children.push(children[children.length - 1].nextSibling);
5410 		}
5411 
5412 		// "Record the values of children, and let values be the result."
5413 		var values = recordValues(children);
5414 
5415 		// "While children's first member's parent is not start block, split
5416 		// the parent of children."
5417 		while (children[0].parentNode != startBlock) {
5418 			splitParent(children, range);
5419 		}
5420 
5421 		// "If children's first member's previousSibling is an editable br,
5422 		// remove that br from its parent."
5423 		if (isEditable(children[0].previousSibling)
5424 		&& isNamedHtmlElement(children[0].previousSibling, "br")) {
5425 			children[0].parentNode.removeChild(children[0].previousSibling);
5426 		}
5427 
5428 	// "Otherwise, if start block is a descendant of end block:"
5429 	} else if (isDescendant(startBlock, endBlock)) {
5430 		// "Set the start and end of range to (start block, length of start
5431 		// block)."
5432 		range.setStart(startBlock, getNodeLength(startBlock));
5433 		range.setEnd(startBlock, getNodeLength(startBlock));
5434 
5435 		// "Let reference node be start block."
5436 		var referenceNode = startBlock;
5437 
5438 		// "While reference node is not a child of end block, set reference
5439 		// node to its parent."
5440 		while (referenceNode.parentNode != endBlock) {
5441 			referenceNode = referenceNode.parentNode;
5442 		}
5443 
5444 		// "If reference node's nextSibling is an inline node and start block's
5445 		// lastChild is a br, remove start block's lastChild from it."
5446 		if (isInlineNode(referenceNode.nextSibling)
5447 		&& isNamedHtmlElement(startBlock.lastChild, "br")) {
5448 			startBlock.removeChild(startBlock.lastChild);
5449 		}
5450 
5451 		// "Let nodes to move be a list of nodes, initially empty."
5452 		var nodesToMove = [];
5453 
5454 		// "If reference node's nextSibling is neither null nor a br nor a
5455 		// block node, append it to nodes to move."
5456 		if (referenceNode.nextSibling
5457 		&& !isNamedHtmlElement(referenceNode.nextSibling, "br")
5458 		&& !isBlockNode(referenceNode.nextSibling)) {
5459 			nodesToMove.push(referenceNode.nextSibling);
5460 		}
5461 
5462 		// "While nodes to move is nonempty and its last member's nextSibling
5463 		// is neither null nor a br nor a block node, append it to nodes to
5464 		// move."
5465 		if (nodesToMove.length
5466 		&& nodesToMove[nodesToMove.length - 1].nextSibling
5467 		&& !isNamedHtmlElement(nodesToMove[nodesToMove.length - 1].nextSibling, "br")
5468 		&& !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) {
5469 			nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling);
5470 		}
5471 
5472 		// "Record the values of nodes to move, and let values be the result."
5473 		var values = recordValues(nodesToMove);
5474 
5475 		// "For each node in nodes to move, append node as the last child of
5476 		// start block, preserving ranges."
5477 		$_( nodesToMove ).forEach(function(node) {
5478 			movePreservingRanges(node, startBlock, -1, range);
5479 		});
5480 
5481 		// "If the nextSibling of reference node is a br, remove it from its
5482 		// parent."
5483 		if (isNamedHtmlElement(referenceNode.nextSibling, "br")) {
5484 			referenceNode.parentNode.removeChild(referenceNode.nextSibling);
5485 		}
5486 
5487 	// "Otherwise:"
5488 	} else {
5489 		// "Set the start and end of range to (start block, length of start
5490 		// block)."
5491 		range.setStart(startBlock, getNodeLength(startBlock));
5492 		range.setEnd(startBlock, getNodeLength(startBlock));
5493 
5494 		// "If end block's firstChild is an inline node and start block's
5495 		// lastChild is a br, remove start block's lastChild from it."
5496 		if (isInlineNode(endBlock.firstChild)
5497 		&& isNamedHtmlElement(startBlock.lastChild, "br")) {
5498 			startBlock.removeChild(startBlock.lastChild);
5499 		}
5500 
5501 		// "Record the values of end block's children, and let values be the
5502 		// result."
5503 		var values = recordValues([].slice.call(toArray(endBlock.childNodes)));
5504 
5505 		// "While end block has children, append the first child of end block
5506 		// to start block, preserving ranges."
5507 		while (endBlock.hasChildNodes()) {
5508 			movePreservingRanges(endBlock.firstChild, startBlock, -1, range);
5509 		}
5510 
5511 		// "While end block has no children, let parent be the parent of end
5512 		// block, then remove end block from parent, then set end block to
5513 		// parent."
5514 		while (!endBlock.hasChildNodes()) {
5515 			var parent_ = endBlock.parentNode;
5516 			parent_.removeChild(endBlock);
5517 			endBlock = parent_;
5518 		}
5519 	}
5520 
5521 	// "Restore the values from values."
5522 	restoreValues(values, range);
5523 
5524 	// "If start block has no children, call createElement("br") on the context
5525 	// object and append the result as the last child of start block."
5526 	ensureContainerEditable(startBlock);
5527 
5528 	// "Restore states and values from overrides."
5529 	restoreStatesAndValues(overrides, range);
5530 }
5531 
5532 
5533 //@}
5534 ///// Splitting a node list's parent /////
5535 //@{
5536 
5537 function splitParent(nodeList, range) {
5538 	// "Let original parent be the parent of the first member of node list."
5539 	var originalParent = nodeList[0].parentNode;
5540 
5541 	// "If original parent is not editable or its parent is null, do nothing
5542 	// and abort these steps."
5543 	if (!isEditable(originalParent)
5544 	|| !originalParent.parentNode) {
5545 		return;
5546 	}
5547 
5548 	// "If the first child of original parent is in node list, remove
5549 	// extraneous line breaks before original parent."
5550 	if (jQuery.inArray(originalParent.firstChild, nodeList) != -1) {
5551 		removeExtraneousLineBreaksBefore(originalParent);
5552 	}
5553 
5554 	var firstChildInNodeList = jQuery.inArray(originalParent.firstChild, nodeList) != -1;
5555 	var lastChildInNodeList = jQuery.inArray(originalParent.lastChild, nodeList) != -1;
5556 
5557 	// "If the first child of original parent is in node list, and original
5558 	// parent follows a line break, set follows line break to true. Otherwise,
5559 	// set follows line break to false."
5560 	var followsLineBreak_ = firstChildInNodeList && followsLineBreak(originalParent);
5561 
5562 	// "If the last child of original parent is in node list, and original
5563 	// parent precedes a line break, set precedes line break to true.
5564 	// Otherwise, set precedes line break to false."
5565 	var precedesLineBreak_ = lastChildInNodeList && precedesLineBreak(originalParent);
5566 
5567 	// "If the first child of original parent is not in node list, but its last
5568 	// child is:"
5569 	if (!firstChildInNodeList && lastChildInNodeList) {
5570 		// "For each node in node list, in reverse order, insert node into the
5571 		// parent of original parent immediately after original parent,
5572 		// preserving ranges."
5573 		for (var i = nodeList.length - 1; i >= 0; i--) {
5574 			movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getNodeIndex(originalParent), range);
5575 		}
5576 
5577 		// "If precedes line break is true, and the last member of node list
5578 		// does not precede a line break, call createElement("br") on the
5579 		// context object and insert the result immediately after the last
5580 		// member of node list."
5581 		if (precedesLineBreak_
5582 		&& !precedesLineBreak(nodeList[nodeList.length - 1])) {
5583 			nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
5584 		}
5585 
5586 		// "Remove extraneous line breaks at the end of original parent."
5587 		removeExtraneousLineBreaksAtTheEndOf(originalParent);
5588 
5589 		// "Abort these steps."
5590 		return;
5591 	}
5592 
5593 	// "If the first child of original parent is not in node list:"
5594 	if (!firstChildInNodeList) {
5595 		// "Let cloned parent be the result of calling cloneNode(false) on
5596 		// original parent."
5597 		var clonedParent = originalParent.cloneNode(false);
5598 
5599 		// "If original parent has an id attribute, unset it."
5600 		originalParent.removeAttribute("id");
5601 
5602 		// "Insert cloned parent into the parent of original parent immediately
5603 		// before original parent."
5604 		originalParent.parentNode.insertBefore(clonedParent, originalParent);
5605 
5606 		// "While the previousSibling of the first member of node list is not
5607 		// null, append the first child of original parent as the last child of
5608 		// cloned parent, preserving ranges."
5609 		while (nodeList[0].previousSibling) {
5610 			movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length, range);
5611 		}
5612 	}
5613 
5614 	// "For each node in node list, insert node into the parent of original
5615 	// parent immediately before original parent, preserving ranges."
5616 	for (var i = 0; i < nodeList.length; i++) {
5617 		movePreservingRanges(nodeList[i], originalParent.parentNode, getNodeIndex(originalParent), range);
5618 	}
5619 
5620 	// "If follows line break is true, and the first member of node list does
5621 	// not follow a line break, call createElement("br") on the context object
5622 	// and insert the result immediately before the first member of node list."
5623 	if (followsLineBreak_
5624 	&& !followsLineBreak(nodeList[0])) {
5625 		nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]);
5626 	}
5627 
5628 	// "If the last member of node list is an inline node other than a br, and
5629 	// the first child of original parent is a br, and original parent is not
5630 	// an inline node, remove the first child of original parent from original
5631 	// parent."
5632 	if (isInlineNode(nodeList[nodeList.length - 1])
5633 	&& !isNamedHtmlElement(nodeList[nodeList.length - 1], "br")
5634 	&& isNamedHtmlElement(originalParent.firstChild, "br")
5635 	&& !isInlineNode(originalParent)) {
5636 		originalParent.removeChild(originalParent.firstChild);
5637 	}
5638 
5639 	// "If original parent has no children:"
5640 	if (!originalParent.hasChildNodes()) {
5641 		// if the current range is collapsed and at the end of the originalParent.parentNode
5642 		// the offset will not be available anymore after the next step (remove child)
5643 		// that's why we need to fix the range to prevent a bogus offset
5644 		if (originalParent.parentNode === range.startContainer
5645 		&& originalParent.parentNode === range.endContainer
5646 		&& range.startContainer === range.endContainer
5647 		&& range.startOffset === range.endOffset
5648 		&& originalParent.parentNode.childNodes.length === range.startOffset) {
5649 			range.startOffset = originalParent.parentNode.childNodes.length - 1;
5650 			range.endOffset = range.startOffset;
5651 		}
5652 
5653 		// "Remove original parent from its parent."
5654 		originalParent.parentNode.removeChild(originalParent);
5655 
5656 		// "If precedes line break is true, and the last member of node list
5657 		// does not precede a line break, call createElement("br") on the
5658 		// context object and insert the result immediately after the last
5659 		// member of node list."
5660 		if (precedesLineBreak_
5661 		&& !precedesLineBreak(nodeList[nodeList.length - 1])) {
5662 			nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling);
5663 		}
5664 
5665 	// "Otherwise, remove extraneous line breaks before original parent."
5666 	} else {
5667 		removeExtraneousLineBreaksBefore(originalParent);
5668 	}
5669 
5670 	// "If node list's last member's nextSibling is null, but its parent is not
5671 	// null, remove extraneous line breaks at the end of node list's last
5672 	// member's parent."
5673 	if (!nodeList[nodeList.length - 1].nextSibling
5674 	&& nodeList[nodeList.length - 1].parentNode) {
5675 		removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode);
5676 	}
5677 }
5678 
5679 // "To remove a node node while preserving its descendants, split the parent of
5680 // node's children if it has any. If it has no children, instead remove it from
5681 // its parent."
5682 function removePreservingDescendants(node, range) {
5683 	if (node.hasChildNodes()) {
5684 		splitParent([].slice.call(toArray(node.childNodes)), range);
5685 	} else {
5686 		node.parentNode.removeChild(node);
5687 	}
5688 }
5689 
5690 
5691 //@}
5692 ///// Canonical space sequences /////
5693 //@{
5694 
5695 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) {
5696 	// "If n is zero, return the empty string."
5697 	if (n == 0) {
5698 		return "";
5699 	}
5700 
5701 	// "If n is one and both non-breaking start and non-breaking end are false,
5702 	// return a single space (U+0020)."
5703 	if (n == 1 && !nonBreakingStart && !nonBreakingEnd) {
5704 		return " ";
5705 	}
5706 
5707 	// "If n is one, return a single non-breaking space (U+00A0)."
5708 	if (n == 1) {
5709 		return "\xa0";
5710 	}
5711 
5712 	// "Let buffer be the empty string."
5713 	var buffer = "";
5714 
5715 	// "If non-breaking start is true, let repeated pair be U+00A0 U+0020.
5716 	// Otherwise, let it be U+0020 U+00A0."
5717 	var repeatedPair;
5718 	if (nonBreakingStart) {
5719 		repeatedPair = "\xa0 ";
5720 	} else {
5721 		repeatedPair = " \xa0";
5722 	}
5723 
5724 	// "While n is greater than three, append repeated pair to buffer and
5725 	// subtract two from n."
5726 	while (n > 3) {
5727 		buffer += repeatedPair;
5728 		n -= 2;
5729 	}
5730 
5731 	// "If n is three, append a three-element string to buffer depending on
5732 	// non-breaking start and non-breaking end:"
5733 	if (n == 3) {
5734 		buffer +=
5735 			!nonBreakingStart && !nonBreakingEnd ? " \xa0 "
5736 			: nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 "
5737 			: !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0"
5738 			: nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0"
5739 			: "impossible";
5740 
5741 	// "Otherwise, append a two-element string to buffer depending on
5742 	// non-breaking start and non-breaking end:"
5743 	} else {
5744 		buffer +=
5745 			!nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5746 			: nonBreakingStart && !nonBreakingEnd ? "\xa0 "
5747 			: !nonBreakingStart && nonBreakingEnd ? " \xa0"
5748 			: nonBreakingStart && nonBreakingEnd ? "\xa0\xa0"
5749 			: "impossible";
5750 	}
5751 
5752 	// "Return buffer."
5753 	return buffer;
5754 }
5755 
5756 function canonicalizeWhitespace(node, offset) {
5757 	// "If node is neither editable nor an editing host, abort these steps."
5758 	if (!isEditable(node) && !isEditingHost(node)) {
5759 		return;
5760 	}
5761 
5762 	// "Let start node equal node and let start offset equal offset."
5763 	var startNode = node;
5764 	var startOffset = offset;
5765 
5766 	// "Repeat the following steps:"
5767 	while (true) {
5768 		// "If start node has a child in the same editing host with index start
5769 		// offset minus one, set start node to that child, then set start
5770 		// offset to start node's length."
5771 		if (0 <= startOffset - 1
5772 		&& inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
5773 			startNode = startNode.childNodes[startOffset - 1];
5774 			startOffset = getNodeLength(startNode);
5775 
5776 		// "Otherwise, if start offset is zero and start node does not follow a
5777 		// line break and start node's parent is in the same editing host, set
5778 		// start offset to start node's index, then set start node to its
5779 		// parent."
5780 		} else if (startOffset == 0
5781 		&& !followsLineBreak(startNode)
5782 		&& inSameEditingHost(startNode, startNode.parentNode)) {
5783 			startOffset = getNodeIndex(startNode);
5784 			startNode = startNode.parentNode;
5785 
5786 		// "Otherwise, if start node is a Text node and its parent's resolved
5787 		// value for "white-space" is neither "pre" nor "pre-wrap" and start
5788 		// offset is not zero and the (start offset − 1)st element of start
5789 		// node's data is a space (0x0020) or non-breaking space (0x00A0),
5790 5791 		// subtract one from start offset."
5792 		} else if (startNode.nodeType == $_.Node.TEXT_NODE
5793 		&& jQuery.inArray($_.getComputedStyle(startNode.parentNode).whiteSpace, ["pre", "pre-wrap"]) == -1
5794 		&& startOffset != 0
5795 		&& /[ \xa0]/.test(startNode.data[startOffset - 1])) {
5796 			startOffset--;
5797 
5798 		// "Otherwise, break from this loop."
5799 		} else {
5800 			break;
5801 		}
5802 	}
5803 
5804 	// "Let end node equal start node and end offset equal start offset."
5805 	var endNode = startNode;
5806 	var endOffset = startOffset;
5807 
5808 	// "Let length equal zero."
5809 	var length = 0;
5810 
5811 	// "Let follows space be false."
5812 	var followsSpace = false;
5813 
5814 	// "Repeat the following steps:"
5815 	while (true) {
5816 		// "If end node has a child in the same editing host with index end
5817 		// offset, set end node to that child, then set end offset to zero."
5818 		if (endOffset < endNode.childNodes.length
5819 		&& inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
5820 			endNode = endNode.childNodes[endOffset];
5821 			endOffset = 0;
5822 
5823 		// "Otherwise, if end offset is end node's length and end node does not
5824 		// precede a line break and end node's parent is in the same editing
5825 		// host, set end offset to one plus end node's index, then set end node
5826 		// to its parent."
5827 		} else if (endOffset == getNodeLength(endNode)
5828 		&& !precedesLineBreak(endNode)
5829 		&& inSameEditingHost(endNode, endNode.parentNode)) {
5830 			endOffset = 1 + getNodeIndex(endNode);
5831 			endNode = endNode.parentNode;
5832 
5833 		// "Otherwise, if end node is a Text node and its parent's resolved
5834 		// value for "white-space" is neither "pre" nor "pre-wrap" and end
5835 		// offset is not end node's length and the end offsetth element of
5836 		// end node's data is a space (0x0020) or non-breaking space (0x00A0):"
5837 		} else if (endNode.nodeType == $_.Node.TEXT_NODE
5838 		&& jQuery.inArray($_.getComputedStyle(endNode.parentNode).whiteSpace, ["pre", "pre-wrap"]) == -1
5839 		&& endOffset != getNodeLength(endNode)
5840 		&& /[ \xa0]/.test(endNode.data[endOffset])) {
5841 			// "If follows space is true and the end offsetth element of end
5842 			// node's data is a space (0x0020), call deleteData(end offset, 1)
5843 			// on end node, then continue this loop from the beginning."
5844 			if (followsSpace
5845 			&& " " == endNode.data[endOffset]) {
5846 				endNode.deleteData(endOffset, 1);
5847 				continue;
5848 			}
5849 
5850 			// "Set follows space to true if the end offsetth element of end
5851 			// node's data is a space (0x0020), false otherwise."
5852 			followsSpace = " " == endNode.data[endOffset];
5853 
5854 			// "Add one to end offset."
5855 			endOffset++;
5856 
5857 			// "Add one to length."
5858 			length++;
5859 
5860 		// "Otherwise, break from this loop."
5861 		} else {
5862 			break;
5863 		}
5864 	}
5865 
5866 	// "Let replacement whitespace be the canonical space sequence of length
5867 	// length. non-breaking start is true if start offset is zero and start
5868 	// node follows a line break, and false otherwise. non-breaking end is true
5869 	// if end offset is end node's length and end node precedes a line break,
5870 	// and false otherwise."
5871 	var replacementWhitespace = canonicalSpaceSequence(length,
5872 		startOffset == 0 && followsLineBreak(startNode),
5873 		endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
5874 
5875 	// "While (start node, start offset) is before (end node, end offset):"
5876 	while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
5877 		// "If start node has a child with index start offset, set start node
5878 		// to that child, then set start offset to zero."
5879 		if (startOffset < startNode.childNodes.length) {
5880 			startNode = startNode.childNodes[startOffset];
5881 			startOffset = 0;
5882 
5883 		// "Otherwise, if start node is not a Text node or if start offset is
5884 		// start node's length, set start offset to one plus start node's
5885 		// index, then set start node to its parent."
5886 		} else if (startNode.nodeType != $_.Node.TEXT_NODE
5887 		|| startOffset == getNodeLength(startNode)) {
5888 			startOffset = 1 + getNodeIndex(startNode);
5889 			startNode = startNode.parentNode;
5890 
5891 		// "Otherwise:"
5892 		} else {
5893 			// "Remove the first element from replacement whitespace, and let
5894 			// element be that element."
5895 			var element = replacementWhitespace[0];
5896 			replacementWhitespace = replacementWhitespace.slice(1);
5897 
5898 			// "If element is not the same as the start offsetth element of
5899 			// start node's data:"
5900 			if (element != startNode.data[startOffset]) {
5901 				// "Call insertData(start offset, element) on start node."
5902 				startNode.insertData(startOffset, element);
5903 
5904 				// "Call deleteData(start offset + 1, 1) on start node."
5905 				startNode.deleteData(startOffset + 1, 1);
5906 			}
5907 
5908 			// "Add one to start offset."
5909 			startOffset++;
5910 		}
5911 	}
5912 }
5913 
5914 
5915 //@}
5916 ///// Indenting and outdenting /////
5917 //@{
5918 
5919 function cleanLists(node, range) {
5920 	// remove any whitespace nodes around list nodes
5921 	if (node) {
5922 		jQuery(node).find('ul,ol,li').each(function () {
5923 			jQuery(this).contents().each(function () {
5924 				if (isWhitespaceNode(this)) {
5925 					var index = getNodeIndex(this);
5926 
5927 					// if the range points to somewhere behind the removed text node, we reduce the offset
5928 					if (range.startContainer === this.parentNode && range.startOffset > index) {
5929 						range.startOffset--;
5930 					} else if (range.startContainer === this) {
5931 						// the range starts in the removed text node, let it start right before
5932 						range.startContainer = this.parentNode;
5933 						range.startOffset = index;
5934 					}
5935 					// same thing for end of the range
5936 					if (range.endContainer === this.parentNode && range.endOffset > index) {
5937 						range.endOffset--;
5938 					} else if (range.endContainer === this) {
5939 						range.endContainer = this.parentNode;
5940 						range.endOffset = index;
5941 					}
5942 					// finally remove the whitespace node
5943 					jQuery(this).remove();
5944 				}
5945 			});
5946 		});
5947 	}
5948 }
5949 
5950 
5951 //@}
5952 ///// Indenting and outdenting /////
5953 //@{
5954 
5955 function indentNodes(nodeList, range) {
5956 	// "If node list is empty, do nothing and abort these steps."
5957 	if (!nodeList.length) {
5958 		return;
5959 	}
5960 
5961 	// "Let first node be the first member of node list."
5962 	var firstNode = nodeList[0];
5963 
5964 	// "If first node's parent is an ol or ul:"
5965 	if (isHtmlElementInArray(firstNode.parentNode, ["OL", "UL"])) {
5966 		// "Let tag be the local name of the parent of first node."
5967 		var tag = firstNode.parentNode.tagName;
5968 
5969 		// "Wrap node list, with sibling criteria returning true for an HTML
5970 		// element with local name tag and false otherwise, and new parent
5971 		// instructions returning the result of calling createElement(tag) on
5972 		// the ownerDocument of first node."
5973 		wrap(nodeList,
5974 			function(node) { return isHtmlElement_obsolete(node, tag) },
5975 			function() { return firstNode.ownerDocument.createElement(tag) },
5976 			range
5977 		);
5978 
5979 		// "Abort these steps."
5980 		return;
5981 	}
5982 
5983 	// "Wrap node list, with sibling criteria returning true for a simple
5984 	// indentation element and false otherwise, and new parent instructions
5985 	// returning the result of calling createElement("blockquote") on the
5986 	// ownerDocument of first node. Let new parent be the result."
5987 	var newParent = wrap(nodeList,
5988 		function(node) { return isSimpleIndentationElement(node) },
5989 		function() { return firstNode.ownerDocument.createElement("blockquote") },
5990 		range
5991 	);
5992 
5993 	// "Fix disallowed ancestors of new parent."
5994 	fixDisallowedAncestors(newParent, range);
5995 }
5996 
5997 function outdentNode(node, range) {
5998 	// "If node is not editable, abort these steps."
5999 	if (!isEditable(node)) {
6000 		return;
6001 	}
6002 
6003 	// "If node is a simple indentation element, remove node, preserving its
6004 	// descendants.  Then abort these steps."
6005 	if (isSimpleIndentationElement(node)) {
6006 		removePreservingDescendants(node, range);
6007 		return;
6008 	}
6009 
6010 	// "If node is an indentation element:"
6011 	if (isIndentationElement(node)) {
6012 		// "Unset the class and dir attributes of node, if any."
6013 		node.removeAttribute("class");
6014 		node.removeAttribute("dir");
6015 
6016 		// "Unset the margin, padding, and border CSS properties of node."
6017 6018 		node.style.margin = "";
6019 		node.style.padding = "";
6020 		node.style.border = "";
6021 		if (node.getAttribute("style") == "") {
6022 			node.removeAttribute("style");
6023 		}
6024 
6025 		// "Set the tag name of node to "div"."
6026 		setTagName(node, "div", range);
6027 
6028 		// "Abort these steps."
6029 		return;
6030 	}
6031 
6032 	// "Let current ancestor be node's parent."
6033 	var currentAncestor = node.parentNode;
6034 
6035 	// "Let ancestor list be a list of nodes, initially empty."
6036 6037 	var ancestorList = [];
6038 
6039 	// "While current ancestor is an editable Element that is neither a simple
6040 	// indentation element nor an ol nor a ul, append current ancestor to
6041 	// ancestor list and then set current ancestor to its parent."
6042 	while (isEditable(currentAncestor)
6043 	&& currentAncestor.nodeType == $_.Node.ELEMENT_NODE
6044 	&& !isSimpleIndentationElement(currentAncestor)
6045 	&& !isHtmlElementInArray(currentAncestor, ["ol", "ul"])) {
6046 		ancestorList.push(currentAncestor);
6047 		currentAncestor = currentAncestor.parentNode;
6048 	}
6049 
6050 	// "If current ancestor is not an editable simple indentation element:"
6051 	if (!isEditable(currentAncestor)
6052 	|| !isSimpleIndentationElement(currentAncestor)) {
6053 		// "Let current ancestor be node's parent."
6054 		currentAncestor = node.parentNode;
6055 
6056 		// "Let ancestor list be the empty list."
6057 		ancestorList = [];
6058 
6059 		// "While current ancestor is an editable Element that is neither an
6060 		// indentation element nor an ol nor a ul, append current ancestor to
6061 		// ancestor list and then set current ancestor to its parent."
6062 		while (isEditable(currentAncestor)
6063 		&& currentAncestor.nodeType == $_.Node.ELEMENT_NODE
6064 		&& !isIndentationElement(currentAncestor)
6065 		&& !isHtmlElementInArray(currentAncestor, ["ol", "ul"])) {
6066 			ancestorList.push(currentAncestor);
6067 			currentAncestor = currentAncestor.parentNode;
6068 		}
6069 	}
6070 
6071 	// "If node is an ol or ul and current ancestor is not an editable
6072 	// indentation element:"
6073 	if (isHtmlElementInArray(node, ["OL", "UL"])
6074 	&& (!isEditable(currentAncestor)
6075 	|| !isIndentationElement(currentAncestor))) {
6076 		// "Unset the reversed, start, and type attributes of node, if any are
6077 		// set."
6078 		node.removeAttribute("reversed");
6079 		node.removeAttribute("start");
6080 		node.removeAttribute("type");
6081 
6082 		// "Let children be the children of node."
6083 		var children = [].slice.call(toArray(node.childNodes));
6084 
6085 		// "If node has attributes, and its parent is not an ol or ul, set the
6086 		// tag name of node to "div"."
6087 		if (node.attributes.length
6088 		&& !isHtmlElementInArray(node.parentNode, ["OL", "UL"])) {
6089 			setTagName(node, "div", range);
6090 
6091 		// "Otherwise:"
6092 		} else {
6093 			// "Record the values of node's children, and let values be the
6094 			// result."
6095 			var values = recordValues([].slice.call(toArray(node.childNodes)));
6096 
6097 			// "Remove node, preserving its descendants."
6098 			removePreservingDescendants(node, range);
6099 
6100 			// "Restore the values from values."
6101 			restoreValues(values, range);
6102 		}
6103 
6104 		// "Fix disallowed ancestors of each member of children."
6105 		for (var i = 0; i < children.length; i++) {
6106 			fixDisallowedAncestors(children[i], range);
6107 		}
6108 
6109 		// "Abort these steps."
6110 		return;
6111 	}
6112 
6113 	// "If current ancestor is not an editable indentation element, abort these
6114 	// steps."
6115 	if (!isEditable(currentAncestor)
6116 	|| !isIndentationElement(currentAncestor)) {
6117 		return;
6118 	}
6119 
6120 	// "Append current ancestor to ancestor list."
6121 	ancestorList.push(currentAncestor);
6122 
6123 	// "Let original ancestor be current ancestor."
6124 	var originalAncestor = currentAncestor;
6125 
6126 	// "While ancestor list is not empty:"
6127 	while (ancestorList.length) {
6128 		// "Let current ancestor be the last member of ancestor list."
6129 		//
6130 		// "Remove the last member of ancestor list."
6131 		currentAncestor = ancestorList.pop();
6132 
6133 		// "Let target be the child of current ancestor that is equal to either
6134 		// node or the last member of ancestor list."
6135 		var target = node.parentNode == currentAncestor
6136 			? node
6137 			: ancestorList[ancestorList.length - 1];
6138 
6139 		// "If target is an inline node that is not a br, and its nextSibling
6140 		// is a br, remove target's nextSibling from its parent."
6141 		if (isInlineNode(target)
6142 		&& !isNamedHtmlElement(target, 'BR')
6143 		&& isNamedHtmlElement(target.nextSibling, "BR")) {
6144 			target.parentNode.removeChild(target.nextSibling);
6145 		}
6146 
6147 		// "Let preceding siblings be the preceding siblings of target, and let
6148 		// following siblings be the following siblings of target."
6149 		var precedingSiblings = [].slice.call(toArray(currentAncestor.childNodes), 0, getNodeIndex(target));
6150 		var followingSiblings = [].slice.call(toArray(currentAncestor.childNodes), 1 + getNodeIndex(target));
6151 
6152 		// "Indent preceding siblings."
6153 		indentNodes(precedingSiblings, range);
6154 
6155 		// "Indent following siblings."
6156 		indentNodes(followingSiblings, range);
6157 	}
6158 
6159 	// "Outdent original ancestor."
6160 	outdentNode(originalAncestor, range);
6161 }
6162 
6163 
6164 //@}
6165 ///// Toggling lists /////
6166 //@{
6167 
6168 function toggleLists(tagName, range) {
6169 	// "Let mode be "disable" if the selection's list state is tag name, and
6170 	// "enable" otherwise."
6171 	var mode = getSelectionListState() == tagName ? "disable" : "enable";
6172 
6173 	tagName = tagName.toUpperCase();
6174 
6175 	// "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
6176 	// "ol"."
6177 	var otherTagName = tagName == "OL" ? "UL" : "OL";
6178 
6179 	// "Let items be a list of all lis that are ancestor containers of the
6180 	// range's start and/or end node."
6181 	//
6182 	// It's annoying to get this in tree order using functional stuff without
6183 	// doing getDescendants(document), which is slow, so I do it imperatively.
6184 	var items = [];
6185 	(function(){
6186 		for (
6187 			var ancestorContainer = range.endContainer;
6188 			ancestorContainer != range.commonAncestorContainer;
6189 			ancestorContainer = ancestorContainer.parentNode
6190 		) {
6191 			if (isNamedHtmlElement(ancestorContainer, "li")) {
6192 				items.unshift(ancestorContainer);
6193 			}
6194 		}
6195 		for (
6196 			var ancestorContainer = range.startContainer;
6197 			ancestorContainer;
6198 			ancestorContainer = ancestorContainer.parentNode
6199 		) {
6200 			if (isNamedHtmlElement(ancestorContainer, "li")) {
6201 				items.unshift(ancestorContainer);
6202 			}
6203 		}
6204 	})();
6205 
6206 	// "For each item in items, normalize sublists of item."
6207 	$_( items ).forEach( function( thisArg ) {
6208 			normalizeSublists( thisArg, range);
6209 	});
6210 
6211 	// "Block-extend the range, and let new range be the result."
6212 	var newRange = blockExtend(range);
6213 
6214 	// "If mode is "enable", then let lists to convert consist of every
6215 	// editable HTML element with local name other tag name that is contained
6216 	// in new range, and for every list in lists to convert:"
6217 	if (mode == "enable") {
6218 		$_( getAllContainedNodes(newRange, function(node) {
6219 			return isEditable(node)
6220 				&& isHtmlElement_obsolete(node, otherTagName);
6221 		}) ).forEach(function(list) {
6222 			// "If list's previousSibling or nextSibling is an editable HTML
6223 			// element with local name tag name:"
6224 			if ((isEditable(list.previousSibling) && isHtmlElement_obsolete(list.previousSibling, tagName))
6225 			|| (isEditable(list.nextSibling) && isHtmlElement_obsolete(list.nextSibling, tagName))) {
6226 				// "Let children be list's children."
6227 				var children = [].slice.call(toArray(list.childNodes));
6228 
6229 				// "Record the values of children, and let values be the
6230 				// result."
6231 				var values = recordValues(children);
6232 
6233 				// "Split the parent of children."
6234 				splitParent(children, range);
6235 
6236 				// "Wrap children, with sibling criteria returning true for an
6237 				// HTML element with local name tag name and false otherwise."
6238 				wrap(children, 
6239 					function(node) { return isHtmlElement_obsolete(node, tagName) },
6240 					function() {return null },
6241 					range
6242 				);
6243 
6244 				// "Restore the values from values."
6245 				restoreValues(values, range);
6246 
6247 			// "Otherwise, set the tag name of list to tag name."
6248 			} else {
6249 				setTagName(list, tagName, range);
6250 			}
6251 		});
6252 	}
6253 
6254 	// "Let node list be a list of nodes, initially empty."
6255 	//
6256 	// "For each node node contained in new range, if node is editable; the
6257 	// last member of node list (if any) is not an ancestor of node; node
6258 	// is not an indentation element; and either node is an ol or ul, or its
6259 	// parent is an ol or ul, or it is an allowed child of "li"; then append
6260 	// node to node list."
6261 	var nodeList = getContainedNodes(newRange, function(node) {
6262 		return isEditable(node)
6263 		&& !isIndentationElement(node)
6264 		&& (isHtmlElementInArray(node, ["OL", "UL"])
6265 		|| isHtmlElementInArray(node.parentNode, ["OL", "UL"])
6266 		|| isAllowedChild(node, "li"));
6267 	});
6268 
6269 	// "If mode is "enable", remove from node list any ol or ul whose parent is
6270 	// not also an ol or ul."
6271 	if (mode == "enable") {
6272 		nodeList = $_( nodeList ).filter(function(node) {
6273 			return !isHtmlElementInArray(node, ["ol", "ul"])
6274 				|| isHtmlElementInArray(node.parentNode, ["ol", "ul"]);
6275 		});
6276 	}
6277 
6278 	// "If mode is "disable", then while node list is not empty:"
6279 	if (mode == "disable") {
6280 		while (nodeList.length) {
6281 			// "Let sublist be an empty list of nodes."
6282 			var sublist = [];
6283 
6284 			// "Remove the first member from node list and append it to
6285 			// sublist."
6286 			sublist.push(nodeList.shift());
6287 
6288 			// "If the first member of sublist is an HTML element with local
6289 			// name tag name, outdent it and continue this loop from the
6290 			// beginning."
6291 			if (isHtmlElement_obsolete(sublist[0], tagName)) {
6292 				outdentNode(sublist[0], range);
6293 				continue;
6294 			}
6295 
6296 			// "While node list is not empty, and the first member of node list
6297 			// is the nextSibling of the last member of sublist and is not an
6298 			// HTML element with local name tag name, remove the first member
6299 			// from node list and append it to sublist."
6300 			while (nodeList.length
6301 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling
6302 			&& !isHtmlElement_obsolete(nodeList[0], tagName)) {
6303 				sublist.push(nodeList.shift());
6304 			}
6305 
6306 			// "Record the values of sublist, and let values be the result."
6307 			var values = recordValues(sublist);
6308 
6309 			// "Split the parent of sublist."
6310 			splitParent(sublist, range);
6311 
6312 			// "Fix disallowed ancestors of each member of sublist."
6313 			for (var i = 0; i < sublist.length; i++) {
6314 				fixDisallowedAncestors(sublist[i], range);
6315 			}
6316 
6317 			// "Restore the values from values."
6318 			restoreValues(values, range);
6319 		}
6320 
6321 	// "Otherwise, while node list is not empty:"
6322 	} else {
6323 		while (nodeList.length) {
6324 			// "Let sublist be an empty list of nodes."
6325 			var sublist = [];
6326 
6327 			// "While either sublist is empty, or node list is not empty and
6328 			// its first member is the nextSibling of sublist's last member:"
6329 			while (!sublist.length
6330 			|| (nodeList.length
6331 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling)) {
6332 				// "If node list's first member is a p or div, set the tag name
6333 				// of node list's first member to "li", and append the result
6334 				// to sublist. Remove the first member from node list."
6335 				if (isHtmlElementInArray(nodeList[0], ["p", "div"])) {
6336 					sublist.push(setTagName(nodeList[0], "li", range));
6337 					nodeList.shift();
6338 
6339 				// "Otherwise, if the first member of node list is an li or ol
6340 				// or ul, remove it from node list and append it to sublist."
6341 				} else if (isHtmlElementInArray(nodeList[0], ["li", "ol", "ul"])) {
6342 					sublist.push(nodeList.shift());
6343 
6344 				// "Otherwise:"
6345 				} else {
6346 					// "Let nodes to wrap be a list of nodes, initially empty."
6347 					var nodesToWrap = [];
6348 
6349 					// "While nodes to wrap is empty, or node list is not empty
6350 					// and its first member is the nextSibling of nodes to
6351 					// wrap's last member and the first member of node list is
6352 					// an inline node and the last member of nodes to wrap is
6353 					// an inline node other than a br, remove the first member
6354 					// from node list and append it to nodes to wrap."
6355 					while (!nodesToWrap.length
6356 					|| (nodeList.length
6357 					&& nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling
6358 					&& isInlineNode(nodeList[0])
6359 					&& isInlineNode(nodesToWrap[nodesToWrap.length - 1])
6360 					&& !isNamedHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) {
6361 						nodesToWrap.push(nodeList.shift());
6362 					}
6363 
6364 					// "Wrap nodes to wrap, with new parent instructions
6365 					// returning the result of calling createElement("li") on
6366 					// the context object. Append the result to sublist."
6367 					sublist.push(
6368 						wrap(nodesToWrap,
6369 							undefined,
6370 							function() { return document.createElement("li") },
6371 							range
6372 						)
6373 					);
6374 				}
6375 			}
6376 
6377 			// "If sublist's first member's parent is an HTML element with
6378 			// local name tag name, or if every member of sublist is an ol or
6379 			// ul, continue this loop from the beginning."
6380 			if (isHtmlElement_obsolete(sublist[0].parentNode, tagName)
6381 			|| $_(sublist).every(function(node) { return isHtmlElementInArray(node, ["ol", "ul"]) })) {
6382 				continue;
6383 			}
6384 
6385 			// "If sublist's first member's parent is an HTML element with
6386 			// local name other tag name:"
6387 			if (isHtmlElement_obsolete(sublist[0].parentNode, otherTagName)) {
6388 				// "Record the values of sublist, and let values be the
6389 				// result."
6390 				var values = recordValues(sublist);
6391 
6392 				// "Split the parent of sublist."
6393 				splitParent(sublist, range);
6394 
6395 				// "Wrap sublist, with sibling criteria returning true for an
6396 				// HTML element with local name tag name and false otherwise,
6397 				// and new parent instructions returning the result of calling
6398 				// createElement(tag name) on the context object."
6399 				wrap(sublist,
6400 					function(node) { return isHtmlElement_obsolete(node, tagName) },
6401 					function() { return document.createElement(tagName) },
6402 					range
6403 				);
6404 
6405 				// "Restore the values from values."
6406 				restoreValues(values, range);
6407 
6408 				// "Continue this loop from the beginning."
6409 				continue;
6410 			}
6411 
6412 			// "Wrap sublist, with sibling criteria returning true for an HTML
6413 			// element with local name tag name and false otherwise, and new
6414 			// parent instructions being the following:"
6415 			// . . .
6416 			// "Fix disallowed ancestors of the previous step's result."
6417 			fixDisallowedAncestors(
6418 				wrap(sublist,
6419 					function(node) { return isHtmlElement_obsolete(node, tagName) },
6420 					function() {
6421 						// "If sublist's first member's parent is not an editable
6422 						// simple indentation element, or sublist's first member's
6423 						// parent's previousSibling is not an editable HTML element
6424 						// with local name tag name, call createElement(tag name)
6425 						// on the context object and return the result."
6426 						if (!isEditable(sublist[0].parentNode)
6427 						|| !isSimpleIndentationElement(sublist[0].parentNode)
6428 						|| !isEditable(sublist[0].parentNode.previousSibling)
6429 						|| !isHtmlElement_obsolete(sublist[0].parentNode.previousSibling, tagName)) {
6430 							return document.createElement(tagName);
6431 						}
6432 	
6433 						// "Let list be sublist's first member's parent's
6434 						// previousSibling."
6435 						var list = sublist[0].parentNode.previousSibling;
6436 	
6437 						// "Normalize sublists of list's lastChild."
6438 						normalizeSublists(list.lastChild, range);
6439 	
6440 						// "If list's lastChild is not an editable HTML element
6441 						// with local name tag name, call createElement(tag name)
6442 						// on the context object, and append the result as the last
6443 						// child of list."
6444 6445 						if (!isEditable(list.lastChild)
6446 						|| !isHtmlElement_obsolete(list.lastChild, tagName)) {
6447 							list.appendChild(document.createElement(tagName));
6448 						}
6449 	
6450 						// "Return the last child of list."
6451 						return list.lastChild;
6452 					},
6453 					range
6454 				)
6455 				, range
6456 			);
6457 		}
6458 	}
6459 }
6460 
6461 
6462 //@}
6463 ///// Justifying the selection /////
6464 //@{
6465 
6466 function justifySelection(alignment, range) {
6467 	
6468 	// "Block-extend the active range, and let new range be the result."
6469 	var newRange = blockExtend(range);
6470 
6471 	// "Let element list be a list of all editable Elements contained in new
6472 	// range that either has an attribute in the HTML namespace whose local
6473 	// name is "align", or has a style attribute that sets "text-align", or is
6474 	// a center."
6475 	var elementList = getAllContainedNodes(newRange, function(node) {
6476 		return node.nodeType == $_.Node.ELEMENT_NODE
6477 			&& isEditable(node)
6478 			// Ignoring namespaces here
6479 			&& (
6480 				hasAttribute(node, "align")
6481 				|| node.style.textAlign != ""
6482 				|| isNamedHtmlElement(node, 'center')
6483 			);
6484 	});
6485 
6486 	// "For each element in element list:"
6487 	for (var i = 0; i < elementList.length; i++) {
6488 		var element = elementList[i];
6489 
6490 		// "If element has an attribute in the HTML namespace whose local name
6491 		// is "align", remove that attribute."
6492 		element.removeAttribute("align");
6493 
6494 		// "Unset the CSS property "text-align" on element, if it's set by a
6495 		// style attribute."
6496 		element.style.textAlign = "";
6497 		if (element.getAttribute("style") == "") {
6498 			element.removeAttribute("style");
6499 		}
6500 
6501 		// "If element is a div or span or center with no attributes, remove
6502 		// it, preserving its descendants."
6503 		if (isHtmlElementInArray(element, ["div", "span", "center"])
6504 		&& !element.attributes.length) {
6505 			removePreservingDescendants(element, range);
6506 		}
6507 
6508 		// "If element is a center with one or more attributes, set the tag
6509 		// name of element to "div"."
6510 		if (isNamedHtmlElement(element, 'center')
6511 		&& element.attributes.length) {
6512 			setTagName(element, "div", range);
6513 		}
6514 	}
6515 
6516 	// "Block-extend the active range, and let new range be the result."
6517 	newRange = blockExtend(globalRange);
6518 
6519 	// "Let node list be a list of nodes, initially empty."
6520 	var nodeList = [];
6521 
6522 	// "For each node node contained in new range, append node to node list if
6523 	// the last member of node list (if any) is not an ancestor of node; node
6524 	// is editable; node is an allowed child of "div"; and node's alignment
6525 	// value is not alignment."
6526 	nodeList = getContainedNodes(newRange, function(node) {
6527 		return isEditable(node)
6528 			&& isAllowedChild(node, "div")
6529 			&& getAlignmentValue(node) != alignment;
6530 	});
6531 
6532 	// "While node list is not empty:"
6533 	while (nodeList.length) {
6534 		// "Let sublist be a list of nodes, initially empty."
6535 		var sublist = [];
6536 
6537 		// "Remove the first member of node list and append it to sublist."
6538 		sublist.push(nodeList.shift());
6539 
6540 		// "While node list is not empty, and the first member of node list is
6541 		// the nextSibling of the last member of sublist, remove the first
6542 		// member of node list and append it to sublist."
6543 		while (nodeList.length
6544 		&& nodeList[0] == sublist[sublist.length - 1].nextSibling) {
6545 			sublist.push(nodeList.shift());
6546 		}
6547 
6548 		// "Wrap sublist. Sibling criteria returns true for any div that has
6549 		// one or both of the following two attributes and no other attributes,
6550 		// and false otherwise:"
6551 		//
6552 		//   * "An align attribute whose value is an ASCII case-insensitive
6553 		//     match for alignment.
6554 		//   * "A style attribute which sets exactly one CSS property
6555 		//     (including unrecognized or invalid attributes), which is
6556 		//     "text-align", which is set to alignment.
6557 		//
6558 		// "New parent instructions are to call createElement("div") on the
6559 		// context object, then set its CSS property "text-align" to alignment
6560 		// and return the result."
6561 		wrap(sublist,
6562 			function(node) {
6563 				return isNamedHtmlElement(node, 'div')
6564 					&& $_(node.attributes).every(function(attr) {
6565 						return (attr.name == "align" && attr.value.toLowerCase() == alignment)
6566 							|| (attr.name == "style" && getStyleLength(node) == 1 && node.style.textAlign == alignment);
6567 					});
6568 			},
6569 			function() {
6570 				var newParent = document.createElement("div");
6571 				newParent.setAttribute("style", "text-align: " + alignment);
6572 				return newParent;
6573 			},
6574 			range
6575 		);
6576 	}
6577 }
6578 
6579 //@}
6580 ///// Check whether the given element is an end break /////
6581 //@{
6582 function isEndBreak(element) {
6583 	return (isNamedHtmlElement(element, 'br')
6584 		&& element.parentNode.lastChild === element);
6585 }
6586 
6587 //@}
6588 ///// Create an end break /////
6589 //@{
6590 function createEndBreak() {
6591 	return document.createElement("br");
6592 }
6593 
6594 /**
6595  * Ensure the container is editable
6596  * E.g. when called for an empty paragraph or header, and the browser is not IE,
6597  * we need to append a br (marked with class aloha-end-br)
6598  * For IE7, there is a special behaviour that will append zero-width whitespace
6599  * @param {DOMNode} container
6600  */
6601 function ensureContainerEditable(container) {
6602 	if (!container) {
6603 		return;
6604 	}
6605 
6606 	if (isNamedHtmlElement(container.lastChild, "br")) {
6607 		return;
6608 	}
6609 
6610 	if ($_(container.childNodes).some(isVisible)) {
6611 		return;
6612 	}
6613 
6614 	if (!jQuery.browser.msie) {
6615 		// for normal browsers, the end-br will do
6616 		container.appendChild(createEndBreak());
6617 	} else if (jQuery.browser.msie && jQuery.browser.version <= 7 &&
6618 			isHtmlElementInArray(container, ["p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "blockquote"])) {
6619 		// for IE7, we need to insert a text node containing a single zero-width whitespace character
6620 		if (!container.firstChild) {
6621 			container.appendChild(document.createTextNode('\u200b'));
6622 		}
6623 	}
6624 }
6625 
6626 //@}
6627 ///// Move the given collapsed range over adjacent zero-width whitespace characters.
6628 ///// The range is 
6629 //@{
6630 /**
6631  * Move the given collapsed range over adjacent zero-width whitespace characters.
6632  * If the range is not collapsed or is not contained in a text node, it is not modified
6633  * @param range range to modify
6634  * @param forward {Boolean} true to move forward, false to move backward
6635  */
6636 function moveOverZWSP(range, forward) {
6637 	var offset;
6638 	if (!range.collapsed) {
6639 		return;
6640 	}
6641 
6642 	offset = range.startOffset;
6643 
6644 	if (forward) {
6645 		// check whether the range starts in a text node
6646 		if (range.startContainer && range.startContainer.nodeType === $_.Node.TEXT_NODE) {
6647 			// move forward (i.e. increase offset) as long as we stay in the text node and have zwsp characters to the right
6648 			while (offset < range.startContainer.data.length && range.startContainer.data.charAt(offset) === '\u200b') {
6649 				offset++;
6650 			}
6651 		}
6652 	} else {
6653 		// check whether the range starts in a text node
6654 		if (range.startContainer && range.startContainer.nodeType === $_.Node.TEXT_NODE) {
6655 			// move backward (i.e. decrease offset) as long as we stay in the text node and have zwsp characters to the left
6656 			while (offset > 0 && range.startContainer.data.charAt(offset - 1) === '\u200b') {
6657 				offset--;
6658 			}
6659 		}
6660 	}
6661 
6662 	// if the offset was changed, set it back to the collapsed range
6663 	if (offset !== range.startOffset) {
6664 		range.setStart(range.startContainer, offset);
6665 		range.setEnd(range.startContainer, offset);
6666 	}
6667 }
6668 
6669 /**
6670  * implementation of the delete command
6671  * will attempt to delete contents within range if non-collapsed
6672  * or delete the character left of the cursor position if range
6673  * is collapsed. Is used to define the behaviour of the backspace
6674  * button.
6675  *
6676  * @param      value   is just there for compatibility with the commands api. parameter is ignored.
6677  * @param      range   the range to execute the delete command for
6678  * @return     void
6679  */
6680 commands["delete"] = {
6681 	action: function(value, range) {
6682 		// special behaviour for skipping zero-width whitespaces in IE7
6683 		if (jQuery.browser.msie && jQuery.browser.version <= 7) {
6684 			moveOverZWSP(range, false);
6685 		}
6686 
6687 		// "If the active range is not collapsed, delete the contents of the
6688 		// active range and abort these steps."
6689 		if (!range.collapsed) {
6690 			deleteContents(range);
6691 			return;
6692 		}
6693 
6694 		// "Canonicalize whitespace at (active range's start node, active
6695 		// range's start offset)."
6696 		canonicalizeWhitespace(range.startContainer, range.startOffset);
6697 
6698 		// "Let node and offset be the active range's start node and offset."
6699 		var node = range.startContainer;
6700 		var offset = range.startOffset;
6701 		var isBr = false;
6702 		var isHr = false;
6703 
6704 		// "Repeat the following steps:"
6705 		while ( true ) {
6706 			// we need to reset isBr and isHr on every interation of the loop
6707 			if ( offset > 0 ) {
6708 				isBr = isNamedHtmlElement(node.childNodes[offset - 1], "br") || false;
6709 				isHr = isNamedHtmlElement(node.childNodes[offset - 1], "hr") || false;
6710 			}
6711 
6712 			// "If offset is zero and node's previousSibling is an editable
6713 			// invisible node, remove node's previousSibling from its parent."
6714 			if (offset == 0
6715 			&& isEditable(node.previousSibling)
6716 			&& isInvisible(node.previousSibling)) {
6717 				node.parentNode.removeChild(node.previousSibling);
6718 
6719 			// "Otherwise, if node has a child with index offset − 1 and that
6720 			// child is an editable invisible node, remove that child from
6721 			// node, then subtract one from offset."
6722 			} else if (0 <= offset - 1
6723 			&& offset - 1 < node.childNodes.length
6724 			&& isEditable(node.childNodes[offset - 1])
6725 			&& (isInvisible(node.childNodes[offset - 1]) || isBr || isHr )) {
6726 				node.removeChild(node.childNodes[offset - 1]);
6727 				offset--;
6728 				if (isBr || isHr) {
6729 					range.setStart(node, offset);
6730 					range.setEnd(node, offset);
6731 					return;
6732 				}
6733 
6734 			// "Otherwise, if offset is zero and node is an inline node, or if
6735 			// node is an invisible node, set offset to the index of node, then
6736 			// set node to its parent."
6737 			} else if ((offset == 0
6738 			&& isInlineNode(node))
6739 			|| isInvisible(node)) {
6740 				offset = getNodeIndex(node);
6741 				node = node.parentNode;
6742 
6743 			// "Otherwise, if node has a child with index offset − 1 and that
6744 			// child is an editable a, remove that child from node, preserving
6745 			// its descendants. Then abort these steps."
6746 			} else if (0 <= offset - 1
6747 			&& offset - 1 < node.childNodes.length
6748 			&& isEditable(node.childNodes[offset - 1])
6749 			&& isNamedHtmlElement(node.childNodes[offset - 1], "a")) {
6750 				removePreservingDescendants(node.childNodes[offset - 1], range);
6751 				return;
6752 
6753 			// "Otherwise, if node has a child with index offset − 1 and that
6754 			// child is not a block node or a br or an img, set node to that
6755 			// child, then set offset to the length of node."
6756 			} else if (0 <= offset - 1
6757 			&& offset - 1 < node.childNodes.length
6758 			&& !isBlockNode(node.childNodes[offset - 1])
6759 			&& !isHtmlElementInArray(node.childNodes[offset - 1], ["br", "img"])) {
6760 				node = node.childNodes[offset - 1];
6761 				offset = getNodeLength(node);
6762 
6763 			// "Otherwise, break from this loop."
6764 			} else {
6765 				break;
6766 			}
6767 		}
6768 
6769 		// if the previous node is an aloha-table we want to delete it
6770 		var delBlock;
6771 		if (delBlock = getBlockAtPreviousPosition(node, offset)) {
6772 			delBlock.parentNode.removeChild(delBlock);
6773 			return;
6774 		}
6775 
6776 		// "If node is a Text node and offset is not zero, call collapse(node,
6777 		// offset) on the Selection. Then delete the contents of the range with
6778 		// start (node, offset − 1) and end (node, offset) and abort these
6779 		// steps."
6780 		if (node.nodeType == $_.Node.TEXT_NODE
6781 		&& offset != 0) {
6782 			range.setStart(node, offset - 1);
6783 			range.setEnd(node, offset - 1);
6784 			deleteContents(node, offset - 1, node, offset);
6785 			return;
6786 		}
6787 
6788 		// @iebug
6789 		// when inserting a special char via the plugin 
6790 		// there where problems deleting them again with backspace after insertation
6791 		// see https://github.com/alohaeditor/Aloha-Editor/issues/517
6792 		if (node.nodeType == $_.Node.TEXT_NODE
6793 		&& offset == 0 && jQuery.browser.msie) {
6794 			offset = 1;
6795 			range.setStart(node, offset);
6796 			range.setEnd(node, offset);
6797 			range.startOffset = 0;
6798 			deleteContents(range);
6799 			return;
6800 		}
6801 
6802 		// "If node is an inline node, abort these steps."
6803 		if (isInlineNode(node)) {
6804 			return;
6805 		}
6806 
6807 		// "If node has a child with index offset − 1 and that child is a br or
6808 		// hr or img, call collapse(node, offset) on the Selection. Then delete
6809 		// the contents of the range with start (node, offset − 1) and end
6810 		// (node, offset) and abort these steps."
6811 		if (0 <= offset - 1
6812 		&& offset - 1 < node.childNodes.length
6813 		&& isHtmlElementInArray(node.childNodes[offset - 1], ["br", "hr", "img"])) {
6814 			range.setStart(node, offset);
6815 			range.setEnd(node, offset);
6816 			deleteContents(range);
6817 			return;
6818 		}
6819 
6820 		// "If node is an li or dt or dd and is the first child of its parent,
6821 		// and offset is zero:"
6822 		if (isHtmlElementInArray(node, ["li", "dt", "dd"])
6823 		&& node == node.parentNode.firstChild
6824 		&& offset == 0) {
6825 			// "Let items be a list of all lis that are ancestors of node."
6826 			//
6827 			// Remember, must be in tree order.
6828 			var items = [];
6829 			for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
6830 				if (isNamedHtmlElement(ancestor, 'li')) {
6831 					items.unshift(ancestor);
6832 				}
6833 			}
6834 
6835 			// "Normalize sublists of each item in items."
6836 			for (var i = 0; i < items.length; i++) {
6837 				normalizeSublists(items[i], range);
6838 			}
6839 
6840 			// "Record the values of the one-node list consisting of node, and
6841 			// let values be the result."
6842 			var values = recordValues([node]);
6843 
6844 			// "Split the parent of the one-node list consisting of node."
6845 			splitParent([node], range);
6846 
6847 			// "Restore the values from values."
6848 			restoreValues(values, range);
6849 
6850 			// "If node is a dd or dt, and it is not an allowed child of any of
6851 			// its ancestors in the same editing host, set the tag name of node
6852 			// to the default single-line container name and let node be the
6853 			// result."
6854 			if (isHtmlElementInArray(node, ["dd", "dt"])
6855 			&& $_(getAncestors(node)).every(function(ancestor) {
6856 				return !inSameEditingHost(node, ancestor)
6857 					|| !isAllowedChild(node, ancestor)
6858 			})) {
6859 				node = setTagName(node, defaultSingleLineContainerName, range);
6860 			}
6861 
6862 			// "Fix disallowed ancestors of node."
6863 			fixDisallowedAncestors(node, range);
6864 
6865 			// fix the lists to be html5 conformant
6866 			for (var i = 0; i < items.length; i++) {
6867 				unNormalizeSublists(items[i].parentNode, range);
6868 			}
6869 
6870 			// "Abort these steps."
6871 			return;
6872 		}
6873 
6874 		// "Let start node equal node and let start offset equal offset."
6875 		var startNode = node;
6876 		var startOffset = offset;
6877 
6878 		// "Repeat the following steps:"
6879 		while (true) {
6880 			// "If start offset is zero, set start offset to the index of start
6881 			// node and then set start node to its parent."
6882 			if (startOffset == 0) {
6883 				startOffset = getNodeIndex(startNode);
6884 				startNode = startNode.parentNode;
6885 
6886 			// "Otherwise, if start node has an editable invisible child with
6887 			// index start offset minus one, remove it from start node and
6888 			// subtract one from start offset."
6889 			} else if (0 <= startOffset - 1
6890 			&& startOffset - 1 < startNode.childNodes.length
6891 			&& isEditable(startNode.childNodes[startOffset - 1])
6892 			&& isInvisible(startNode.childNodes[startOffset - 1])) {
6893 				startNode.removeChild(startNode.childNodes[startOffset - 1]);
6894 				startOffset--;
6895 
6896 			// "Otherwise, break from this loop."
6897 			} else {
6898 				break;
6899 			}
6900 		}
6901 
6902 		// "If offset is zero, and node has an editable ancestor container in
6903 		// the same editing host that's an indentation element:"
6904 		if (offset == 0
6905 		&& $_( getAncestors(node).concat(node) ).filter(function(ancestor) {
6906 			return isEditable(ancestor)
6907 				&& inSameEditingHost(ancestor, node)
6908 				&& isIndentationElement(ancestor);
6909 		}).length) {
6910 			// "Block-extend the range whose start and end are both (node, 0),
6911 			// and let new range be the result."
6912 			var newRange = Aloha.createRange();
6913 			newRange.setStart(node, 0);
6914 			newRange.setEnd(node, 0);
6915 			newRange = blockExtend(newRange);
6916 
6917 			// "Let node list be a list of nodes, initially empty."
6918 			//
6919 			// "For each node current node contained in new range, append
6920 			// current node to node list if the last member of node list (if
6921 			// any) is not an ancestor of current node, and current node is
6922 			// editable but has no editable descendants."
6923 			var nodeList = getContainedNodes(newRange, function(currentNode) {
6924 				return isEditable(currentNode)
6925 					&& !hasEditableDescendants(currentNode);
6926 			});
6927 
6928 			// "Outdent each node in node list."
6929 			for (var i = 0; i < nodeList.length; i++) {
6930 				outdentNode(nodeList[i], range);
6931 			}
6932 
6933 			// "Abort these steps."
6934 			return;
6935 		}
6936 
6937 		// "If the child of start node with index start offset is a table,
6938 		// abort these steps."
6939 		if (isNamedHtmlElement(startNode.childNodes[startOffset], "table")) {
6940 			return;
6941 		}
6942 
6943 		// "If start node has a child with index start offset − 1, and that
6944 		// child is a table:"
6945 		if (0 <= startOffset - 1
6946 		&& startOffset - 1 < startNode.childNodes.length
6947 		&& isNamedHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
6948 			// "Call collapse(start node, start offset − 1) on the context
6949 			// object's Selection."
6950 			range.setStart(startNode, startOffset - 1);
6951 
6952 			// "Call extend(start node, start offset) on the context object's
6953 			// Selection."
6954 			range.setEnd(startNode, startOffset);
6955 
6956 			// "Abort these steps."
6957 			return;
6958 		}
6959 
6960 		// "If offset is zero; and either the child of start node with index
6961 		// start offset minus one is an hr, or the child is a br whose
6962 		// previousSibling is either a br or not an inline node:"
6963 		if (offset == 0
6964 		&& (isNamedHtmlElement(startNode.childNodes[startOffset - 1], "hr")
6965 			|| (
6966 				isNamedHtmlElement(startNode.childNodes[startOffset - 1], "br")
6967 				&& (
6968 					isNamedHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
6969 					|| !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
6970 				)
6971 			)
6972 		)) {
6973 			// "Call collapse(node, offset) on the Selection."
6974 			range.setStart(node, offset);
6975 			range.setEnd(node, offset);
6976 
6977 			// "Delete the contents of the range with start (start node, start
6978 			// offset − 1) and end (start node, start offset)."
6979 			deleteContents(startNode, startOffset - 1, startNode, startOffset);
6980 
6981 			// "Abort these steps."
6982 			return;
6983 		}
6984 
6985 		// "If the child of start node with index start offset is an li or dt
6986 		// or dd, and that child's firstChild is an inline node, and start
6987 		// offset is not zero:"
6988 		if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6989 		&& isInlineNode(startNode.childNodes[startOffset].firstChild)
6990 		&& startOffset != 0) {
6991 			// "Let previous item be the child of start node with index start
6992 			// offset minus one."
6993 			var previousItem = startNode.childNodes[startOffset - 1];
6994 
6995 			// "If previous item's lastChild is an inline node other than a br,
6996 			// call createElement("br") on the context object and append the
6997 			// result as the last child of previous item."
6998 			if (isInlineNode(previousItem.lastChild)
6999 			&& !isNamedHtmlElement(previousItem.lastChild, "br")) {
7000 				previousItem.appendChild(document.createElement("br"));
7001 			}
7002 
7003 			// "If previous item's lastChild is an inline node, call
7004 			// createElement("br") on the context object and append the result
7005 			// as the last child of previous item."
7006 			if (isInlineNode(previousItem.lastChild)) {
7007 				previousItem.appendChild(document.createElement("br"));
7008 			}
7009 		}
7010 
7011 		// "If the child of start node with index start offset is an li or dt
7012 		// or dd, and its previousSibling is also an li or dt or dd, set start
7013 		// node to its child with index start offset − 1, then set start offset
7014 		// to start node's length, then set node to start node's nextSibling,
7015 		// then set offset to 0."
7016 		if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"])
7017 		&& isHtmlElementInArray(startNode.childNodes[startOffset - 1], ["li", "dt", "dd"])) {
7018 			startNode = startNode.childNodes[startOffset - 1];
7019 			startOffset = getNodeLength(startNode);
7020 			node = startNode.nextSibling;
7021 			offset = 0;
7022 
7023 		// "Otherwise, while start node has a child with index start offset
7024 		// minus one:"
7025 		} else {
7026 			while (0 <= startOffset - 1
7027 			&& startOffset - 1 < startNode.childNodes.length) {
7028 				// "If start node's child with index start offset minus one is
7029 				// editable and invisible, remove it from start node, then
7030 				// subtract one from start offset."
7031 				if (isEditable(startNode.childNodes[startOffset - 1])
7032 				&& isInvisible(startNode.childNodes[startOffset - 1])) {
7033 					startNode.removeChild(startNode.childNodes[startOffset - 1]);
7034 					startOffset--;
7035 
7036 				// "Otherwise, set start node to its child with index start
7037 				// offset minus one, then set start offset to the length of
7038 				// start node."
7039 				} else {
7040 					startNode = startNode.childNodes[startOffset - 1];
7041 					startOffset = getNodeLength(startNode);
7042 				}
7043 			}
7044 		}
7045 
7046 		// "Delete the contents of the range with start (start node, start
7047 		// offset) and end (node, offset)."
7048 		var delRange = Aloha.createRange();
7049 		delRange.setStart(startNode, startOffset);
7050 		delRange.setEnd(node, offset);
7051 		deleteContents(delRange);
7052 
7053 		if (!isAncestorContainer(document.body, range.startContainer)) {
7054 			if (delRange.startContainer.hasChildNodes() || delRange.startContainer.nodeType == $_.Node.TEXT_NODE) {
7055 				range.setStart(delRange.startContainer, delRange.startOffset);
7056 				range.setEnd(delRange.startContainer, delRange.startOffset);
7057 			} else {
7058 				range.setStart(delRange.startContainer.parentNode, getNodeIndex(delRange.startContainer));
7059 				range.setEnd(delRange.startContainer.parentNode, getNodeIndex(delRange.startContainer));
7060 			}
7061 		}
7062 	}
7063 };
7064 
7065 //@}
7066 ///// The formatBlock command /////
7067 //@{
7068 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
7069 // "h4", "h5", "h6", "p", or "pre"."
7070 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
7071 	"h4", "h5", "h6", "p", "pre"];
7072 
7073 commands.formatblock = {
7074 	action: function(value) {
7075 		// "If value begins with a "<" character and ends with a ">" character,
7076 		// remove the first and last characters from it."
7077 		if (/^<.*>$/.test(value)) {
7078 			value = value.slice(1, -1);
7079 		}
7080 
7081 		// "Let value be converted to ASCII lowercase."
7082 		value = value.toLowerCase();
7083 
7084 		// "If value is not a formattable block name, abort these steps and do
7085 		// nothing."
7086 		if ($_(formattableBlockNames).indexOf(value) == -1) {
7087 			return;
7088 		}
7089 
7090 		// "Block-extend the active range, and let new range be the result."
7091 		var newRange = blockExtend(getActiveRange());
7092 
7093 		// "Let node list be an empty list of nodes."
7094 		//
7095 		// "For each node node contained in new range, append node to node list
7096 		// if it is editable, the last member of original node list (if any) is
7097 		// not an ancestor of node, node is either a non-list single-line
7098 		// container or an allowed child of "p" or a dd or dt, and node is not
7099 		// the ancestor of a prohibited paragraph child."
7100 		var nodeList = getContainedNodes(newRange, function(node) {
7101 			return isEditable(node)
7102 				&& (isNonListSingleLineContainer(node)
7103 				|| isAllowedChild(node, "p")
7104 				|| isHtmlElementInArray(node, ["dd", "dt"]))
7105 				&& !$_( getDescendants(node) ).some(isProhibitedParagraphChild);
7106 		});
7107 
7108 		// "Record the values of node list, and let values be the result."
7109 		var values = recordValues(nodeList);
7110 
7111 		// "For each node in node list, while node is the descendant of an
7112 		// editable HTML element in the same editing host, whose local name is
7113 		// a formattable block name, and which is not the ancestor of a
7114 		// prohibited paragraph child, split the parent of the one-node list
7115 		// consisting of node."
7116 		for (var i = 0; i < nodeList.length; i++) {
7117 			var node = nodeList[i];
7118 			while ($_( getAncestors(node) ).some(function(ancestor) {
7119 				return isEditable(ancestor)
7120 					&& inSameEditingHost(ancestor, node)
7121 					&& isHtmlElement_obsolete(ancestor, formattableBlockNames)
7122 					&& !$_( getDescendants(ancestor) ).some(isProhibitedParagraphChild);
7123 			})) {
7124 				splitParent([node], range);
7125 			}
7126 		}
7127 
7128 		// "Restore the values from values."
7129 		restoreValues(values, range);
7130 
7131 		// "While node list is not empty:"
7132 		while (nodeList.length) {
7133 			var sublist;
7134 
7135 			// "If the first member of node list is a single-line
7136 			// container:"
7137 			if (isSingleLineContainer(nodeList[0])) {
7138 				// "Let sublist be the children of the first member of node
7139 				// list."
7140 				sublist = [].slice.call(toArray(nodeList[0].childNodes));
7141 
7142 				// "Record the values of sublist, and let values be the
7143 				// result."
7144 				var values = recordValues(sublist);
7145 
7146 				// "Remove the first member of node list from its parent,
7147 				// preserving its descendants."
7148 				removePreservingDescendants(nodeList[0], range);
7149 
7150 				// "Restore the values from values."
7151 				restoreValues(values, range);
7152 
7153 				// "Remove the first member from node list."
7154 				nodeList.shift();
7155 
7156 			// "Otherwise:"
7157 			} else {
7158 				// "Let sublist be an empty list of nodes."
7159 				sublist = [];
7160 
7161 				// "Remove the first member of node list and append it to
7162 				// sublist."
7163 				sublist.push(nodeList.shift());
7164 
7165 				// "While node list is not empty, and the first member of
7166 				// node list is the nextSibling of the last member of
7167 				// sublist, and the first member of node list is not a
7168 				// single-line container, and the last member of sublist is
7169 				// not a br, remove the first member of node list and
7170 				// append it to sublist."
7171 				while (nodeList.length
7172 				&& nodeList[0] == sublist[sublist.length - 1].nextSibling
7173 				&& !isSingleLineContainer(nodeList[0])
7174 				&& !isNamedHtmlElement(sublist[sublist.length - 1], "BR")) {
7175 					sublist.push(nodeList.shift());
7176 				}
7177 			}
7178 
7179 			// "Wrap sublist. If value is "div" or "p", sibling criteria
7180 			// returns false; otherwise it returns true for an HTML element
7181 			// with local name value and no attributes, and false otherwise.
7182 			// New parent instructions return the result of running
7183 			// createElement(value) on the context object. Then fix disallowed
7184 			// ancestors of the result."
7185 			fixDisallowedAncestors(
7186 				wrap(sublist,
7187 					jQuery.inArray(value, ["div", "p"]) == - 1
7188 						? function(node) { return isHtmlElement_obsolete(node, value) && !node.attributes.length }
7189 						: function() { return false },
7190 					function() { return document.createElement(value) },
7191 					range
7192 				),
7193 				range
7194 			);
7195 		}
7196 	}, indeterm: function() {
7197 		// "Block-extend the active range, and let new range be the result."
7198 		var newRange = blockExtend(getActiveRange());
7199 
7200 		// "Let node list be all visible editable nodes that are contained in
7201 		// new range and have no children."
7202 		var nodeList = getAllContainedNodes(newRange, function(node) {
7203 			return isVisible(node)
7204 				&& isEditable(node)
7205 				&& !node.hasChildNodes();
7206 		});
7207 
7208 		// "If node list is empty, return false."
7209 		if (!nodeList.length) {
7210 			return false;
7211 		}
7212 
7213 		// "Let type be null."
7214 		var type = null;
7215 
7216 		// "For each node in node list:"
7217 		for (var i = 0; i < nodeList.length; i++) {
7218 			var node = nodeList[i];
7219 
7220 			// "While node's parent is editable and in the same editing host as
7221 			// node, and node is not an HTML element whose local name is a
7222 			// formattable block name, set node to its parent."
7223 			while (isEditable(node.parentNode)
7224 			&& inSameEditingHost(node, node.parentNode)
7225 			&& !isHtmlElement_obsolete(node, formattableBlockNames)) {
7226 				node = node.parentNode;
7227 			}
7228 
7229 			// "Let current type be the empty string."
7230 			var currentType = "";
7231 
7232 			// "If node is an editable HTML element whose local name is a
7233 			// formattable block name, and node is not the ancestor of a
7234 			// prohibited paragraph child, set current type to node's local
7235 			// name."
7236 			if (isEditable(node)
7237 			&& isHtmlElement_obsolete(node, formattableBlockNames)
7238 			&& !$_( getDescendants(node) ).some(isProhibitedParagraphChild)) {
7239 				currentType = node.tagName;
7240 			}
7241 
7242 			// "If type is null, set type to current type."
7243 			if (type === null) {
7244 				type = currentType;
7245 
7246 			// "Otherwise, if type does not equal current type, return true."
7247 			} else if (type != currentType) {
7248 				return true;
7249 			}
7250 		}
7251 
7252 		// "Return false."
7253 		return false;
7254 	}, value: function() {
7255 		// "Block-extend the active range, and let new range be the result."
7256 		var newRange = blockExtend(getActiveRange());
7257 
7258 		// "Let node be the first visible editable node that is contained in
7259 		// new range and has no children. If there is no such node, return the
7260 		// empty string."
7261 		var nodes = getAllContainedNodes(newRange, function(node) {
7262 			return isVisible(node)
7263 				&& isEditable(node)
7264 				&& !node.hasChildNodes();
7265 		});
7266 		if (!nodes.length) {
7267 			return "";
7268 		}
7269 		var node = nodes[0];
7270 
7271 		// "While node's parent is editable and in the same editing host as
7272 		// node, and node is not an HTML element whose local name is a
7273 		// formattable block name, set node to its parent."
7274 		while (isEditable(node.parentNode)
7275 		&& inSameEditingHost(node, node.parentNode)
7276 		&& !isHtmlElement_obsolete(node, formattableBlockNames)) {
7277 			node = node.parentNode;
7278 		}
7279 
7280 		// "If node is an editable HTML element whose local name is a
7281 		// formattable block name, and node is not the ancestor of a prohibited
7282 		// paragraph child, return node's local name, converted to ASCII
7283 		// lowercase."
7284 		if (isEditable(node)
7285 		&& isHtmlElement_obsolete(node, formattableBlockNames)
7286 		&& !$_( getDescendants(node) ).some(isProhibitedParagraphChild)) {
7287 			return node.tagName.toLowerCase();
7288 		}
7289 
7290 		// "Return the empty string."
7291 		return "";
7292 	}
7293 };
7294 
7295 //@}
7296 ///// The forwardDelete command /////
7297 //@{
7298 commands.forwarddelete = {
7299 	action: function(value, range) {
7300 		// special behaviour for skipping zero-width whitespaces in IE7
7301 		if (jQuery.browser.msie && jQuery.browser.version <= 7) {
7302 			moveOverZWSP(range, true);
7303 		}
7304 
7305 		// "If the active range is not collapsed, delete the contents of the
7306 		// active range and abort these steps."
7307 		if (!range.collapsed) {
7308 			deleteContents(range);
7309 			return;
7310 		}
7311 
7312 		// "Canonicalize whitespace at (active range's start node, active
7313 		// range's start offset)."
7314 		canonicalizeWhitespace(range.startContainer, range.startOffset);
7315 
7316 		// "Let node and offset be the active range's start node and offset."
7317 		var node = range.startContainer;
7318 		var offset = range.startOffset;
7319 		var isBr = false;
7320 		var isHr = false;
7321 
7322 		// "Repeat the following steps:"
7323 		while (true) {
7324 			// check whether the next element is a br or hr
7325 			if ( offset < node.childNodes.length ) {
7326 //				isBr = isHtmlElement_obsolete(node.childNodes[offset], "br") || false;
7327 //				isHr = isHtmlElement_obsolete(node.childNodes[offset], "hr") || false;
7328 			}
7329 
7330 			// "If offset is the length of node and node's nextSibling is an
7331 			// editable invisible node, remove node's nextSibling from its
7332 			// parent."
7333 			if (offset == getNodeLength(node)
7334 			&& isEditable(node.nextSibling)
7335 			&& isInvisible(node.nextSibling)) {
7336 				node.parentNode.removeChild(node.nextSibling);
7337 
7338 			// "Otherwise, if node has a child with index offset and that child
7339 			// is an editable invisible node, remove that child from node."
7340 			} else if (offset < node.childNodes.length
7341 			&& isEditable(node.childNodes[offset])
7342 			&& (isInvisible(node.childNodes[offset]) || isBr || isHr )) {
7343 				node.removeChild(node.childNodes[offset]);
7344 				if (isBr || isHr) {
7345 					ensureContainerEditable(node);
7346 					range.setStart(node, offset);
7347 					range.setEnd(node, offset);
7348 					return;
7349 				}
7350 
7351 			// "Otherwise, if node has a child with index offset and that child
7352 			// is a collapsed block prop, add one to offset."
7353 			} else if (offset < node.childNodes.length
7354 			&& isCollapsedBlockProp(node.childNodes[offset])) {
7355 				offset++;
7356 
7357 			// "Otherwise, if offset is the length of node and node is an
7358 			// inline node, or if node is invisible, set offset to one plus the
7359 			// index of node, then set node to its parent."
7360 			} else if ((offset == getNodeLength(node)
7361 			&& isInlineNode(node))
7362 			|| isInvisible(node)) {
7363 				offset = 1 + getNodeIndex(node);
7364 				node = node.parentNode;
7365 
7366 			// "Otherwise, if node has a child with index offset and that child
7367 			// is not a block node or a br or an img, set node to that child,
7368 			// then set offset to zero."
7369 			} else if (offset < node.childNodes.length
7370 			&& !isBlockNode(node.childNodes[offset])
7371 			&& !isHtmlElementInArray(node.childNodes[offset], ["br", "img"])) {
7372 				node = node.childNodes[offset];
7373 				offset = 0;
7374 
7375 			// "Otherwise, break from this loop."
7376 			} else {
7377 				break;
7378 			}
7379 		}
7380 
7381 		// collapse whitespace in the node, if it is a text node
7382 		canonicalizeWhitespace(range.startContainer, range.startOffset);
7383 
7384 		// if the next node is an aloha-table we want to delete it
7385 		var delBlock;
7386 		if (delBlock = getBlockAtNextPosition(node, offset)) {
7387 			delBlock.parentNode.removeChild(delBlock);
7388 			return;
7389 		}
7390 		
7391 		// "If node is a Text node and offset is not node's length:"
7392 		if (node.nodeType == $_.Node.TEXT_NODE
7393 		&& offset != getNodeLength(node)) {
7394 			// "Call collapse(node, offset) on the Selection."
7395 			range.setStart(node, offset);
7396 			range.setEnd(node, offset);
7397 
7398 			// "Let end offset be offset plus one."
7399 			var endOffset = offset + 1;
7400 
7401 			// "While end offset is not node's length and the end offsetth
7402 			// element of node's data has general category M when interpreted
7403 			// as a Unicode code point, add one to end offset."
7404 			//
7405 			// TODO: Not even going to try handling anything beyond the most
7406 			// basic combining marks, since I couldn't find a good list.  I
7407 			// special-case a few Hebrew diacritics too to test basic coverage
7408 			// of non-Latin stuff.
7409 			while (endOffset != node.length
7410 			&& /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
7411 				endOffset++;
7412 			}
7413 
7414 			// "Delete the contents of the range with start (node, offset) and
7415 			// end (node, end offset)."
7416 			deleteContents(node, offset, node, endOffset);
7417 
7418 			// "Abort these steps."
7419 			return;
7420 		}
7421 
7422 		// "If node is an inline node, abort these steps."
7423 		if (isInlineNode(node)) {
7424 			return;
7425 		}
7426 
7427 		// "If node has a child with index offset and that child is a br or hr
7428 		// or img, call collapse(node, offset) on the Selection. Then delete
7429 		// the contents of the range with start (node, offset) and end (node,
7430 		// offset + 1) and abort these steps."
7431 		if (offset < node.childNodes.length
7432 		&& isHtmlElementInArray(node.childNodes[offset], ["br", "hr", "img"])) {
7433 			range.setStart(node, offset);
7434 			range.setEnd(node, offset);
7435 			deleteContents(node, offset, node, offset + 1);
7436 			return;
7437 		}
7438 
7439 		// "Let end node equal node and let end offset equal offset."
7440 		var endNode = node;
7441 		var endOffset = offset;
7442 
7443 		// "Repeat the following steps:"
7444 		while (true) {
7445 			// "If end offset is the length of end node, set end offset to one
7446 			// plus the index of end node and then set end node to its parent."
7447 			if (endOffset == getNodeLength(endNode)) {
7448 				endOffset = 1 + getNodeIndex(endNode);
7449 				endNode = endNode.parentNode;
7450 
7451 			// "Otherwise, if end node has a an editable invisible child with
7452 			// index end offset, remove it from end node."
7453 			} else if (endOffset < endNode.childNodes.length
7454 			&& isEditable(endNode.childNodes[endOffset])
7455 			&& isInvisible(endNode.childNodes[endOffset])) {
7456 				endNode.removeChild(endNode.childNodes[endOffset]);
7457 
7458 			// "Otherwise, break from this loop."
7459 			} else {
7460 				break;
7461 			}
7462 		}
7463 
7464 		// "If the child of end node with index end offset minus one is a
7465 		// table, abort these steps."
7466 		if (isNamedHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
7467 			return;
7468 		}
7469 
7470 		// "If the child of end node with index end offset is a table:"
7471 		if (isNamedHtmlElement(endNode.childNodes[endOffset], "table")) {
7472 			// "Call collapse(end node, end offset) on the context object's
7473 			// Selection."
7474 			range.setStart(endNode, endOffset);
7475 
7476 			// "Call extend(end node, end offset + 1) on the context object's
7477 			// Selection."
7478 			range.setEnd(endNode, endOffset + 1);
7479 
7480 			// "Abort these steps."
7481 			return;
7482 		}
7483 
7484 		// "If offset is the length of node, and the child of end node with
7485 		// index end offset is an hr or br:"
7486 		if (offset == getNodeLength(node)
7487 		&& isHtmlElementInArray(endNode.childNodes[endOffset], ["br", "hr"])) {
7488 			// "Call collapse(node, offset) on the Selection."
7489 			range.setStart(node, offset);
7490 			range.setEnd(node, offset);
7491 
7492 			// "Delete the contents of the range with end (end node, end
7493 			// offset) and end (end node, end offset + 1)."
7494 			deleteContents(endNode, endOffset, endNode, endOffset + 1);
7495 
7496 			// "Abort these steps."
7497 			return;
7498 		}
7499 
7500 		// "While end node has a child with index end offset:"
7501 		while (endOffset < endNode.childNodes.length) {
7502 			// "If end node's child with index end offset is editable and
7503 			// invisible, remove it from end node."
7504 			if (isEditable(endNode.childNodes[endOffset])
7505 			&& isInvisible(endNode.childNodes[endOffset])) {
7506 				endNode.removeChild(endNode.childNodes[endOffset]);
7507 
7508 			// "Otherwise, set end node to its child with index end offset and
7509 			// set end offset to zero."
7510 			} else {
7511 				endNode = endNode.childNodes[endOffset];
7512 				endOffset = 0;
7513 			}
7514 		}
7515 
7516 		// "Delete the contents of the range with start (node, offset) and end
7517 		// (end node, end offset)."
7518 		deleteContents(node, offset, endNode, endOffset);
7519 	}
7520 };
7521 
7522 //@}
7523 ///// The indent command /////
7524 //@{
7525 commands.indent = {
7526 	action: function() {
7527 		// "Let items be a list of all lis that are ancestor containers of the
7528 		// active range's start and/or end node."
7529 		//
7530 		// Has to be in tree order, remember!
7531 		var items = [];
7532 		for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7533 			if (isNamedHtmlElement(node, "LI")) {
7534 				items.unshift(node);
7535 			}
7536 		}
7537 		for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7538 			if (isNamedHtmlElement(node, "LI")) {
7539 				items.unshift(node);
7540 			}
7541 		}
7542 		for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
7543 			if (isNamedHtmlElement(node, "LI")) {
7544 				items.unshift(node);
7545 			}
7546 		}
7547 
7548 		// "For each item in items, normalize sublists of item."
7549 		for (var i = 0; i < items.length; i++) {
7550 			normalizeSublists(items[i, range]);
7551 		}
7552 
7553 		// "Block-extend the active range, and let new range be the result."
7554 		var newRange = blockExtend(getActiveRange());
7555 
7556 		// "Let node list be a list of nodes, initially empty."
7557 		var nodeList = [];
7558 
7559 		// "For each node node contained in new range, if node is editable and
7560 		// is an allowed child of "div" or "ol" and if the last member of node
7561 		// list (if any) is not an ancestor of node, append node to node list."
7562 		nodeList = getContainedNodes(newRange, function(node) {
7563 			return isEditable(node)
7564 				&& (isAllowedChild(node, "div")
7565 				|| isAllowedChild(node, "ol"));
7566 		});
7567 
7568 		// "If the first member of node list is an li whose parent is an ol or
7569 		// ul, and its previousSibling is an li as well, normalize sublists of
7570 		// its previousSibling."
7571 		if (nodeList.length
7572 		&& isNamedHtmlElement(nodeList[0], "LI")
7573 		&& isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"])
7574 		&& isNamedHtmlElement(nodeList[0].previousSibling, "LI")) {
7575 			normalizeSublists(nodeList[0].previousSibling, range);
7576 		}
7577 
7578 		// "While node list is not empty:"
7579 		while (nodeList.length) {
7580 			// "Let sublist be a list of nodes, initially empty."
7581 			var sublist = [];
7582 
7583 			// "Remove the first member of node list and append it to sublist."
7584 			sublist.push(nodeList.shift());
7585 
7586 			// "While the first member of node list is the nextSibling of the
7587 			// last member of sublist, remove the first member of node list and
7588 			// append it to sublist."
7589 			while (nodeList.length
7590 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling) {
7591 				sublist.push(nodeList.shift());
7592 			}
7593 
7594 			// "Indent sublist."
7595 			indentNodes(sublist, range);
7596 		}
7597 	}
7598 };
7599 
7600 //@}
7601 ///// The insertHorizontalRule command /////
7602 //@{
7603 commands.inserthorizontalrule = {
7604 	action: function(value, range) {
7605 		
7606 		// "While range's start offset is 0 and its start node's parent is not
7607 		// null, set range's start to (parent of start node, index of start
7608 		// node)."
7609 		while (range.startOffset == 0
7610 		&& range.startContainer.parentNode) {
7611 			range.setStart(range.startContainer.parentNode, getNodeIndex(range.startContainer));
7612 		}
7613 
7614 		// "While range's end offset is the length of its end node, and its end
7615 		// node's parent is not null, set range's end to (parent of end node, 1
7616 		// + index of start node)."
7617 		while (range.endOffset == getNodeLength(range.endContainer)
7618 		&& range.endContainer.parentNode) {
7619 			range.setEnd(range.endContainer.parentNode, 1 + getNodeIndex(range.endContainer));
7620 		}
7621 
7622 		// "Delete the contents of range, with block merging false."
7623 		deleteContents(range, {blockMerging: false});
7624 
7625 		// "If the active range's start node is neither editable nor an editing
7626 		// host, abort these steps."
7627 		if (!isEditable(getActiveRange().startContainer)
7628 		&& !isEditingHost(getActiveRange().startContainer)) {
7629 			return;
7630 		}
7631 
7632 		// "If the active range's start node is a Text node and its start
7633 		// offset is zero, set the active range's start and end to (parent of
7634 		// start node, index of start node)."
7635 		if (getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE
7636 		&& getActiveRange().startOffset == 0) {
7637 			getActiveRange().setStart(getActiveRange().startContainer.parentNode, getNodeIndex(getActiveRange().startContainer));
7638 			getActiveRange().collapse(true);
7639 		}
7640 
7641 		// "If the active range's start node is a Text node and its start
7642 		// offset is the length of its start node, set the active range's start
7643 		// and end to (parent of start node, 1 + index of start node)."
7644 		if (getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE
7645 		&& getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
7646 			getActiveRange().setStart(getActiveRange().startContainer.parentNode, 1 + getNodeIndex(getActiveRange().startContainer));
7647 			getActiveRange().collapse(true);
7648 		}
7649 
7650 		// "Let hr be the result of calling createElement("hr") on the
7651 		// context object."
7652 		var hr = document.createElement("hr");
7653 
7654 		// "Run insertNode(hr) on the range."
7655 		range.insertNode(hr);
7656 
7657 		// "Fix disallowed ancestors of hr."
7658 		fixDisallowedAncestors(hr, range);
7659 
7660 		// "Run collapse() on the Selection, with first argument equal to the
7661 		// parent of hr and the second argument equal to one plus the index of
7662 		// hr."
7663 		//
7664 		// Not everyone actually supports collapse(), so we do it manually
7665 		// instead.  Also, we need to modify the actual range we're given as
7666 		// well, for the sake of autoimplementation.html's range-filling-in.
7667 		range.setStart(hr.parentNode, 1 + getNodeIndex(hr));
7668 		range.setEnd(hr.parentNode, 1 + getNodeIndex(hr));
7669 		Aloha.getSelection().removeAllRanges();
7670 		Aloha.getSelection().addRange(range);
7671 	}
7672 };
7673 
7674 //@}
7675 ///// The insertHTML command /////
7676 //@{
7677 commands.inserthtml = {
7678 	action: function(value, range) {
7679 		
7680 		
7681 		// "Delete the contents of the active range."
7682 		deleteContents(range);
7683 
7684 		// "If the active range's start node is neither editable nor an editing
7685 		// host, abort these steps."
7686 		if (!isEditable(range.startContainer)
7687 		&& !isEditingHost(range.startContainer)) {
7688 			return;
7689 		}
7690 
7691 		// "Let frag be the result of calling createContextualFragment(value)
7692 		// on the active range."
7693 		var frag = range.createContextualFragment(value);
7694 
7695 		// "Let last child be the lastChild of frag."
7696 		var lastChild = frag.lastChild;
7697 
7698 		// "If last child is null, abort these steps."
7699 		if (!lastChild) {
7700 			return;
7701 		}
7702 
7703 		// "Let descendants be all descendants of frag."
7704 		var descendants = getDescendants(frag);
7705 
7706 		// "If the active range's start node is a block node:"
7707 		if (isBlockNode(range.startContainer)) {
7708 			// "Let collapsed block props be all editable collapsed block prop
7709 			// children of the active range's start node that have index
7710 			// greater than or equal to the active range's start offset."
7711 			//
7712 			// "For each node in collapsed block props, remove node from its
7713 			// parent."
7714 			$_(range.startContainer.childNodes).filter(function(node, range) {
7715 				return isEditable(node)
7716 					&& isCollapsedBlockProp(node)
7717 					&& getNodeIndex(node) >= range.startOffset;
7718 			}, true).forEach(function(node) {
7719 				node.parentNode.removeChild(node);
7720 			});
7721 		}
7722 
7723 		// "Call insertNode(frag) on the active range."
7724 		range.insertNode(frag);
7725 
7726 		// "If the active range's start node is a block node with no visible
7727 		// children, call createElement("br") on the context object and append
7728 		// the result as the last child of the active range's start node."
7729 		if (isBlockNode(range.startContainer)) {
7730 			ensureContainerEditable(range.startContainer);
7731 		}
7732 
7733 		// "Call collapse() on the context object's Selection, with last
7734 		// child's parent as the first argument and one plus its index as the
7735 		// second."
7736 		range.setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7737 		range.setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7738 
7739 		// "Fix disallowed ancestors of each member of descendants."
7740 		for (var i = 0; i < descendants.length; i++) {
7741 			fixDisallowedAncestors(descendants[i], range);
7742 		}
7743 		
7744 		setActiveRange( range );
7745 	}
7746 };
7747 
7748 //@}
7749 ///// The insertImage command /////
7750 //@{
7751 commands.insertimage = {
7752 	action: function(value) {
7753 		// "If value is the empty string, abort these steps and do nothing."
7754 		if (value === "") {
7755 			return;
7756 		}
7757 
7758 		// "Let range be the active range."
7759 		var range = getActiveRange();
7760 
7761 		// "Delete the contents of range, with strip wrappers false."
7762 		deleteContents(range, {stripWrappers: false});
7763 
7764 		// "If the active range's start node is neither editable nor an editing
7765 		// host, abort these steps."
7766 		if (!isEditable(getActiveRange().startContainer)
7767 		&& !isEditingHost(getActiveRange().startContainer)) {
7768 			return;
7769 		}
7770 
7771 		// "If range's start node is a block node whose sole child is a br, and
7772 		// its start offset is 0, remove its start node's child from it."
7773 		if (isBlockNode(range.startContainer)
7774 		&& range.startContainer.childNodes.length == 1
7775 		&& isNamedHtmlElement(range.startContainer.firstChild, "br")
7776 		&& range.startOffset == 0) {
7777 			range.startContainer.removeChild(range.startContainer.firstChild);
7778 		}
7779 
7780 		// "Let img be the result of calling createElement("img") on the
7781 		// context object."
7782 		var img = document.createElement("img");
7783 
7784 		// "Run setAttribute("src", value) on img."
7785 		img.setAttribute("src", value);
7786 
7787 		// "Run insertNode(img) on the range."
7788 		range.insertNode(img);
7789 
7790 		// "Run collapse() on the Selection, with first argument equal to the
7791 		// parent of img and the second argument equal to one plus the index of
7792 		// img."
7793 		//
7794 		// Not everyone actually supports collapse(), so we do it manually
7795 		// instead.  Also, we need to modify the actual range we're given as
7796 		// well, for the sake of autoimplementation.html's range-filling-in.
7797 		range.setStart(img.parentNode, 1 + getNodeIndex(img));
7798 		range.setEnd(img.parentNode, 1 + getNodeIndex(img));
7799 		Aloha.getSelection().removeAllRanges();
7800 		Aloha.getSelection().addRange(range);
7801 
7802 		// IE adds width and height attributes for some reason, so remove those
7803 		// to actually do what the spec says.
7804 		img.removeAttribute("width");
7805 		img.removeAttribute("height");
7806 	}
7807 };
7808 
7809 //@}
7810 ///// The insertLineBreak command /////
7811 //@{
7812 commands.insertlinebreak = {
7813 	action: function(value, range) {
7814 		// "Delete the contents of the active range, with strip wrappers false."
7815 		deleteContents(range, {stripWrappers: false});
7816 
7817 		// "If the active range's start node is neither editable nor an editing
7818 		// host, abort these steps."
7819 		if (!isEditable(range.startContainer)
7820 		&& !isEditingHost(range.startContainer)) {
7821 			return;
7822 		}
7823 
7824 		// "If the active range's start node is an Element, and "br" is not an
7825 		// allowed child of it, abort these steps."
7826 		if (range.startContainer.nodeType == $_.Node.ELEMENT_NODE
7827 		&& !isAllowedChild("br", range.startContainer)) {
7828 			return;
7829 7830 		}
7831 
7832 		// "If the active range's start node is not an Element, and "br" is not
7833 		// an allowed child of the active range's start node's parent, abort
7834 		// these steps."
7835 		if (range.startContainer.nodeType != $_.Node.ELEMENT_NODE
7836 		&& !isAllowedChild("br", range.startContainer.parentNode)) {
7837 			return;
7838 		}
7839 
7840 		// "If the active range's start node is a Text node and its start
7841 		// offset is zero, call collapse() on the context object's Selection,
7842 		// with first argument equal to the active range's start node's parent
7843 		// and second argument equal to the active range's start node's index."
7844 		if (range.startContainer.nodeType == $_.Node.TEXT_NODE
7845 		&& range.startOffset == 0) {
7846 			var newNode = range.startContainer.parentNode;
7847 			var newOffset = getNodeIndex(range.startContainer);
7848 			Aloha.getSelection().collapse(newNode, newOffset);
7849 			range.setStart(newNode, newOffset);
7850 			range.setEnd(newNode, newOffset);
7851 		}
7852 
7853 		// "If the active range's start node is a Text node and its start
7854 		// offset is the length of its start node, call collapse() on the
7855 		// context object's Selection, with first argument equal to the active
7856 		// range's start node's parent and second argument equal to one plus
7857 		// the active range's start node's index."
7858 		if (range.startContainer.nodeType == $_.Node.TEXT_NODE
7859 		&& range.startOffset == getNodeLength(range.startContainer)) {
7860 			var newNode = range.startContainer.parentNode;
7861 			var newOffset = 1 + getNodeIndex(range.startContainer);
7862 			Aloha.getSelection().collapse(newNode, newOffset);
7863 			range.setStart(newNode, newOffset);
7864 			range.setEnd(newNode, newOffset);
7865 		}
7866 
7867 		// "Let br be the result of calling createElement("br") on the context
7868 		// object."
7869 		var br = document.createElement("br");
7870 
7871 		// "Call insertNode(br) on the active range."
7872 		range.insertNode(br);
7873 
7874 		// "Call collapse() on the context object's Selection, with br's parent
7875 		// as the first argument and one plus br's index as the second
7876 		// argument."
7877 		Aloha.getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7878 		range.setStart(br.parentNode, 1 + getNodeIndex(br));
7879 		range.setEnd(br.parentNode, 1 + getNodeIndex(br));
7880 
7881 		// "If br is a collapsed line break, call createElement("br") on the
7882 		// context object and let extra br be the result, then call
7883 		// insertNode(extra br) on the active range."
7884 		if (isCollapsedLineBreak(br)) {
7885 			// TODO
7886 			range.insertNode(createEndBreak());
7887 
7888 			// Compensate for nonstandard implementations of insertNode
7889 			Aloha.getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7890 			range.setStart(br.parentNode, 1 + getNodeIndex(br));
7891 			range.setEnd(br.parentNode, 1 + getNodeIndex(br));
7892 		}
7893 		
7894 		// IE7 is adding this styles: height: auto; min-height: 0px; max-height: none;
7895 		// with that there is the ugly "IE-editable-outline"
7896 		if (jQuery.browser.msie && jQuery.browser.version < 8) {
7897 			br.parentNode.removeAttribute("style");
7898 		}
7899 	}
7900 };
7901 
7902 //@}
7903 ///// The insertOrderedList command /////
7904 //@{
7905 commands.insertorderedlist = {
7906 	// "Toggle lists with tag name "ol"."
7907 	action: function() { toggleLists("ol") },
7908 	// "True if the selection's list state is "mixed" or "mixed ol", false
7909 	// otherwise."
7910 	indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
7911 	// "True if the selection's list state is "ol", false otherwise."
7912 	state: function() { return getSelectionListState() == "ol" }
7913 };
7914 
7915 var listRelatedElements = {"LI": true, "DT": true, "DD": true};
7916 
7917 //@}
7918 ///// The insertParagraph command /////
7919 //@{
7920 commands.insertparagraph = {
7921 	action: function(value, range) {
7922 
7923 		// "Delete the contents of the active range."
7924 		deleteContents(range);
7925 
7926 		// clean lists in the editing host, this will remove any whitespace nodes around lists
7927 		// because the following algorithm is not prepared to deal with them
7928 		cleanLists(getEditingHostOf(range.startContainer), range);
7929 
7930 		// "If the active range's start node is neither editable nor an editing
7931 		// host, abort these steps."
7932 		if (!isEditable(range.startContainer)
7933 		&& !isEditingHost(range.startContainer)) {
7934 			return;
7935 		}
7936 
7937 		// "Let node and offset be the active range's start node and offset."
7938 		var node = range.startContainer;
7939 		var offset = range.startOffset;
7940 
7941 		// "If node is a Text node, and offset is neither 0 nor the length of
7942 		// node, call splitText(offset) on node."
7943 		if (node.nodeType == $_.Node.TEXT_NODE
7944 		&& offset != 0
7945 		&& offset != getNodeLength(node)) {
7946 			node.splitText(offset);
7947 		}
7948 
7949 		// "If node is a Text node and offset is its length, set offset to one
7950 		// plus the index of node, then set node to its parent."
7951 		if (node.nodeType == $_.Node.TEXT_NODE
7952 		&& offset == getNodeLength(node)) {
7953 			offset = 1 + getNodeIndex(node);
7954 			node = node.parentNode;
7955 		}
7956 
7957 		// "If node is a Text or Comment node, set offset to the index of node,
7958 		// then set node to its parent."
7959 		if (node.nodeType == $_.Node.TEXT_NODE
7960 		|| node.nodeType == $_.Node.COMMENT_NODE) {
7961 			offset = getNodeIndex(node);
7962 			node = node.parentNode;
7963 		}
7964 
7965 		// "Call collapse(node, offset) on the context object's Selection."
7966 		Aloha.getSelection().collapse(node, offset);
7967 		range.setStart(node, offset);
7968 		range.setEnd(node, offset);
7969 
7970 		// "Let container equal node."
7971 		var container = node;
7972 
7973 		// "While container is not a single-line container, and container's
7974 		// parent is editable and in the same editing host as node, set
7975 		// container to its parent."
7976 		while (!isSingleLineContainer(container)
7977 		&& isEditable(container.parentNode)
7978 		&& inSameEditingHost(node, container.parentNode)) {
7979 			container = container.parentNode;
7980 		}
7981 
7982 		// "If container is not editable or not in the same editing host as
7983 		// node or is not a single-line container:"
7984 		if (!isEditable(container)
7985 		|| !inSameEditingHost(container, node)
7986 		|| !isSingleLineContainer(container)) {
7987 			// "Let tag be the default single-line container name."
7988 			var tag = defaultSingleLineContainerName;
7989 
7990 			// "Block-extend the active range, and let new range be the
7991 			// result."
7992 			var newRange = blockExtend(range);
7993 
7994 			// "Let node list be a list of nodes, initially empty."
7995 			//
7996 			// "Append to node list the first node in tree order that is
7997 			// contained in new range and is an allowed child of "p", if any."
7998 			var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
7999 				.slice(0, 1);
8000 
8001 			// "If node list is empty:"
8002 			if (!nodeList.length) {
8003 				// "If tag is not an allowed child of the active range's start
8004 				// node, abort these steps."
8005 				if (!isAllowedChild(tag, range.startContainer)) {
8006 					return;
8007 				}
8008 
8009 				// "Set container to the result of calling createElement(tag)
8010 				// on the context object."
8011 				container = document.createElement(tag);
8012 
8013 				// "Call insertNode(container) on the active range."
8014 				range.insertNode(container);
8015 
8016 				// "Call createElement("br") on the context object, and append
8017 				// the result as the last child of container."
8018 				// TODO not always
8019 				container.appendChild(createEndBreak());
8020 
8021 				// "Call collapse(container, 0) on the context object's
8022 				// Selection."
8023 				// TODO: remove selection from command
8024 				Aloha.getSelection().collapse(container, 0); 
8025 				range.setStart(container, 0);
8026 				range.setEnd(container, 0);
8027 
8028 				// "Abort these steps."
8029 				return;
8030 			}
8031 
8032 			// "While the nextSibling of the last member of node list is not
8033 			// null and is an allowed child of "p", append it to node list."
8034 			while (nodeList[nodeList.length - 1].nextSibling
8035 			&& isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
8036 				nodeList.push(nodeList[nodeList.length - 1].nextSibling);
8037 			}
8038 
8039 			// "Wrap node list, with sibling criteria returning false and new
8040 			// parent instructions returning the result of calling
8041 			// createElement(tag) on the context object. Set container to the
8042 8043 			// result."
8044 			container = wrap(nodeList,
8045 				function() { return false },
8046 				function() { return document.createElement(tag) },
8047 				range
8048 			);
8049 		}
8050 
8051 		// "If container's local name is "address", "listing", or "pre":"
8052 		if (container.tagName == "ADDRESS"
8053 		|| container.tagName == "LISTING"
8054 		|| container.tagName == "PRE") {
8055 			// "Let br be the result of calling createElement("br") on the
8056 			// context object."
8057 			var br = document.createElement("br");
8058 
8059 			// remember the old height
8060 			var oldHeight = container.offsetHeight;
8061 
8062 			// "Call insertNode(br) on the active range."
8063 			range.insertNode(br);
8064 
8065 			// determine the new height
8066 			var newHeight = container.offsetHeight;
8067 
8068 			// "Call collapse(node, offset + 1) on the context object's
8069 			// Selection."
8070 			Aloha.getSelection().collapse(node, offset + 1);
8071 			range.setStart(node, offset + 1);
8072 			range.setEnd(node, offset + 1);
8073 
8074 			// "If br is the last descendant of container, let br be the result
8075 			// of calling createElement("br") on the context object, then call
8076 			// insertNode(br) on the active range." (Fix: only do this, if the container height did not change by inserting a single <br/>)
8077 			//
8078 			// Work around browser bugs: some browsers select the
8079 			// newly-inserted node, not per spec.
8080 			if (oldHeight == newHeight && !isDescendant(nextNode(br), container)) {
8081 				// TODO check
8082 				range.insertNode(createEndBreak());
8083 				Aloha.getSelection().collapse(node, offset + 1);
8084 				range.setEnd(node, offset + 1);
8085 			}
8086 8087 
			// "Abort these steps."
8088 			return;
8089 		}
8090 
8091 		// "If container's local name is "li", "dt", or "dd"; and either it has
8092 		// no children or it has a single child and that child is a br:"
8093 		if (listRelatedElements[container.tagName]
8094 		&& (!container.hasChildNodes()
8095 		|| (container.childNodes.length == 1
8096 		&& isNamedHtmlElement(container.firstChild, "br")))) {
8097 			// "Split the parent of the one-node list consisting of container."
8098 			splitParent([container], range);
8099 
8100 			// "If container has no children, call createElement("br") on the
8101 			// context object and append the result as the last child of
8102 			// container."
8103 			// only do this, if inserting the br does NOT modify the offset height of the container
8104 //			if (!container.hasChildNodes()) {
8105 //				var oldHeight = container.offsetHeight, endBr = createEndBreak();
8106 //				container.appendChild(endBr);
8107 //				if (container.offsetHeight !== oldHeight) {
8108 //					container.removeChild(endBr);
8109 //				}
8110 //			}
8111 8112 
			// "If container is a dd or dt, and it is not an allowed child of
8113 			// any of its ancestors in the same editing host, set the tag name
8114 			// of container to the default single-line container name and let
8115 			// container be the result."
8116 			if (isHtmlElementInArray(container, ["dd", "dt"])
8117 			&& $_( getAncestors(container) ).every(function(ancestor) {
8118 				return !inSameEditingHost(container, ancestor)
8119 					|| !isAllowedChild(container, ancestor)
8120 			})) {
8121 				container = setTagName(container, defaultSingleLineContainerName, range);
8122 			}
8123 
8124 			// "Fix disallowed ancestors of container."
8125 			fixDisallowedAncestors(container, range);
8126 
8127 			// fix invalid nested lists
8128 			if (isNamedHtmlElement(container, 'li')
8129 			&& isNamedHtmlElement(container.nextSibling, "li")
8130 			&& isHtmlElementInArray(container.nextSibling.firstChild, ["ol", "ul"])) {
8131 				// we found a li containing only a br followed by a li containing a list as first element: merge the two li's
8132 				var listParent = container.nextSibling, length = container.nextSibling.childNodes.length;
8133 				for (var i = 0; i < length; i++) {
8134 					// we always move the first child into the container
8135 					container.appendChild(listParent.childNodes[0]);
8136 				}
8137 				listParent.parentNode.removeChild(listParent);
8138 			}
8139 
8140 			// "Abort these steps."
8141 			return;
8142 		}
8143 
8144 		// "Let new line range be a new range whose start is the same as
8145 		// the active range's, and whose end is (container, length of
8146 		// container)."
8147 		var newLineRange = Aloha.createRange();
8148 		newLineRange.setStart(range.startContainer, range.startOffset);
8149 		newLineRange.setEnd(container, getNodeLength(container));
8150 
8151 		// "While new line range's start offset is zero and its start node is
8152 		// not container, set its start to (parent of start node, index of
8153 		// start node)."
8154 		while (newLineRange.startOffset == 0
8155 		&& newLineRange.startContainer != container) {
8156 			newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
8157 		}
8158 
8159 		// "While new line range's start offset is the length of its start node
8160 		// and its start node is not container, set its start to (parent of
8161 		// start node, 1 + index of start node)."
8162 		while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
8163 		&& newLineRange.startContainer != container) {
8164 			newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
8165 		}
8166 
8167 		// "Let end of line be true if new line range contains either nothing
8168 		// or a single br, and false otherwise."
8169 		var containedInNewLineRange = getContainedNodes(newLineRange);
8170 		var endOfLine = !containedInNewLineRange.length
8171 			|| (containedInNewLineRange.length == 1
8172 			&& isNamedHtmlElement(containedInNewLineRange[0], "br"));
8173 
8174 		// "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
8175 		// "h6", and end of line is true, let new container name be the default
8176 		// single-line container name."
8177 		var newContainerName;
8178 		if (/^H[1-6]$/.test(container.tagName)
8179 		&& endOfLine) {
8180 			newContainerName = defaultSingleLineContainerName;
8181 
8182 		// "Otherwise, if the local name of container is "dt" and end of line
8183 		// is true, let new container name be "dd"."
8184 		} else if (container.tagName == "DT"
8185 		&& endOfLine) {
8186 			newContainerName = "dd";
8187 
8188 		// "Otherwise, if the local name of container is "dd" and end of line
8189 		// is true, let new container name be "dt"."
8190 		} else if (container.tagName == "DD"
8191 		&& endOfLine) {
8192 			newContainerName = "dt";
8193 
8194 		// "Otherwise, let new container name be the local name of container."
8195 		} else {
8196 			newContainerName = container.tagName.toLowerCase();
8197 		}
8198 
8199 		// "Let new container be the result of calling createElement(new
8200 		// container name) on the context object."
8201 		var newContainer = document.createElement(newContainerName);
8202 
8203 		// "Copy all non empty attributes of the container to new container."
8204 		copyAttributes( container,  newContainer );
8205 
8206 		// "If new container has an id attribute, unset it."
8207 		newContainer.removeAttribute("id");
8208 
8209 		// "Insert new container into the parent of container immediately after
8210 		// container."
8211 		container.parentNode.insertBefore(newContainer, container.nextSibling);
8212 
8213 		// "Let contained nodes be all nodes contained in new line range."
8214 		var containedNodes = getAllContainedNodes(newLineRange);
8215 
8216 		// "Let frag be the result of calling extractContents() on new line
8217 		// range."
8218 		var frag = newLineRange.extractContents();
8219 
8220 		// "Unset the id attribute (if any) of each Element descendant of frag
8221 		// that is not in contained nodes."
8222 		var descendants = getDescendants(frag);
8223 		for (var i = 0; i < descendants.length; i++) {
8224 			if (descendants[i].nodeType == $_.Node.ELEMENT_NODE
8225 			&& $_(containedNodes).indexOf(descendants[i]) == -1) {
8226 				descendants[i].removeAttribute("id");
8227 			}
8228 		}
8229 
8230 		var fragChildren = [], fragChild = frag.firstChild;
8231 		if (fragChild) {
8232 			do {
8233 				if (!isWhitespaceNode(fragChild)) {
8234 					fragChildren.push(fragChild);
8235 				}
8236 			} while(fragChild = fragChild.nextSibling);
8237 		}
8238 
8239 		// if newContainer is a li and frag contains only a list, we add a br in the li (but only if the height would not change)
8240 		if (isNamedHtmlElement(newContainer, 'li') && fragChildren.length && isHtmlElementInArray(fragChildren[0], ["ul", "ol"])) {
8241 			var oldHeight = newContainer.offsetHeight;
8242 			var endBr = createEndBreak();
8243 			newContainer.appendChild(endBr);
8244 			var newHeight = newContainer.offsetHeight;
8245 			if (oldHeight !== newHeight) {
8246 				newContainer.removeChild(endBr);
8247 			}
8248 		}
8249 
8250 		// "Call appendChild(frag) on new container."
8251 		newContainer.appendChild(frag);
8252 
8253 		// "If container has no visible children, call createElement("br") on
8254 		// the context object, and append the result as the last child of
8255 		// container."
8256 		ensureContainerEditable(container);
8257 
8258 		// "If new container has no visible children, call createElement("br")
8259 		// on the context object, and append the result as the last child of
8260 		// new container."
8261 		ensureContainerEditable(newContainer);
8262 
8263 		// "Call collapse(new container, 0) on the context object's Selection."
8264 		Aloha.getSelection().collapse(newContainer, 0);
8265 		range.setStart(newContainer, 0);
8266 		range.setEnd(newContainer, 0);
8267 	}
8268 };
8269 
8270 //@}
8271 ///// The insertText command /////
8272 //@{
8273 commands.inserttext = {
8274 	action: function(value, range) {
8275 		// "Delete the contents of the active range, with strip wrappers
8276 		// false."
8277 		deleteContents(range, {stripWrappers: false});
8278 
8279 		// "If the active range's start node is neither editable nor an editing
8280 		// host, abort these steps."
8281 		if (!isEditable(range.startContainer)
8282 		&& !isEditingHost(range.startContainer)) {
8283 			return;
8284 		}
8285 
8286 		// "If value's length is greater than one:"
8287 		if (value.length > 1) {
8288 			// "For each element el in value, take the action for the
8289 			// insertText command, with value equal to el."
8290 			for (var i = 0; i < value.length; i++) {
8291 				commands.inserttext.action( value[i], range );
8292 			}
8293 
8294 			// "Abort these steps."
8295 			return;
8296 		}
8297 
8298 		// "If value is the empty string, abort these steps."
8299 		if (value == "") {
8300 			return;
8301 		}
8302 
8303 		// "If value is a newline (U+00A0), take the action for the
8304 		// insertParagraph command and abort these steps."
8305 		if (value == "\n") {
8306 			commands.insertparagraph.action( '', range );
8307 			return;
8308 		}
8309 
8310 		// "Let node and offset be the active range's start node and offset."
8311 		var node = range.startContainer;
8312 		var offset = range.startOffset;
8313 
8314 		// "If node has a child whose index is offset − 1, and that child is a
8315 		// Text node, set node to that child, then set offset to node's
8316 		// length."
8317 		if (0 <= offset - 1
8318 		&& offset - 1 < node.childNodes.length
8319 		&& node.childNodes[offset - 1].nodeType == $_.Node.TEXT_NODE) {
8320 			node = node.childNodes[offset - 1];
8321 			offset = getNodeLength(node);
8322 		}
8323 
8324 		// "If node has a child whose index is offset, and that child is a Text
8325 		// node, set node to that child, then set offset to zero."
8326 		if (0 <= offset
8327 		&& offset < node.childNodes.length
8328 		&& node.childNodes[offset].nodeType == $_.Node.TEXT_NODE) {
8329 			node = node.childNodes[offset];
8330 			offset = 0;
8331 		}
8332 
8333 		// "If value is a space (U+0020), and either node is an Element whose
8334 		// resolved value for "white-space" is neither "pre" nor "pre-wrap" or
8335 		// node is not an Element but its parent is an Element whose resolved
8336 		// value for "white-space" is neither "pre" nor "pre-wrap", set value
8337 		// to a non-breaking space (U+00A0)."
8338 		var refElement = node.nodeType == $_.Node.ELEMENT_NODE ? node : node.parentNode;
8339 		if (value == " "
8340 		&& refElement.nodeType == $_.Node.ELEMENT_NODE
8341 		&& jQuery.inArray($_.getComputedStyle(refElement).whiteSpace, ["pre", "pre-wrap"]) == -1) {
8342 			value = "\xa0";
8343 		}
8344 
8345 		// "Record current overrides, and let overrides be the result."
8346 		var overrides = recordCurrentOverrides( range );
8347 
8348 		// "If node is a Text node:"
8349 		if (node.nodeType == $_.Node.TEXT_NODE) {
8350 			// "Call insertData(offset, value) on node."
8351 			node.insertData(offset, value);
8352 
8353 			// "Call collapse(node, offset) on the context object's Selection."
8354 			Aloha.getSelection().collapse(node, offset);
8355 			range.setStart(node, offset);
8356 
8357 			// "Call extend(node, offset + 1) on the context object's
8358 			// Selection."
8359 			Aloha.getSelection().extend(node, offset + 1);
8360 			range.setEnd(node, offset + 1);
8361 
8362 		// "Otherwise:"
8363 		} else {
8364 			// "If node has only one child, which is a collapsed line break,
8365 			// remove its child from it."
8366 			//
8367 			// FIXME: IE incorrectly returns false here instead of true
8368 			// sometimes?
8369 			if (node.childNodes.length == 1
8370 			&& isCollapsedLineBreak(node.firstChild)) {
8371 				node.removeChild(node.firstChild);
8372 			}
8373 
8374 			// "Let text be the result of calling createTextNode(value) on the
8375 			// context object."
8376 			var text = document.createTextNode(value);
8377 
8378 			// "Call insertNode(text) on the active range."
8379 			range.insertNode(text);
8380 
8381 			// "Call collapse(text, 0) on the context object's Selection."
8382 			Aloha.getSelection().collapse(text, 0);
8383 			range.setStart(text, 0);
8384 
8385 			// "Call extend(text, 1) on the context object's Selection."
8386 			Aloha.getSelection().extend(text, 1);
8387 			range.setEnd(text, 1);
8388 		}
8389 
8390 		// "Restore states and values from overrides."
8391 		restoreStatesAndValues(overrides);
8392 
8393 		// "Canonicalize whitespace at the active range's start."
8394 		canonicalizeWhitespace(range.startContainer, range.startOffset);
8395 
8396 		// "Canonicalize whitespace at the active range's end."
8397 		canonicalizeWhitespace(range.endContainer, range.endOffset);
8398 
8399 		// "Call collapseToEnd() on the context object's Selection."
8400 		Aloha.getSelection().collapseToEnd();
8401 		range.collapse(false);
8402 	}
8403 };
8404 
8405 //@}
8406 ///// The insertUnorderedList command /////
8407 //@{
8408 commands.insertunorderedlist = {
8409 	// "Toggle lists with tag name "ul"."
8410 	action: function() { toggleLists("ul") },
8411 	// "True if the selection's list state is "mixed" or "mixed ul", false
8412 	// otherwise."
8413 	indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
8414 	// "True if the selection's list state is "ul", false otherwise."
8415 	state: function() { return getSelectionListState() == "ul" }
8416 };
8417 
8418 //@}
8419 ///// The justifyCenter command /////
8420 //@{
8421 commands.justifycenter = {
8422 	// "Justify the selection with alignment "center"."
8423 	action: function(value, range) { justifySelection("center", range) },
8424 	indeterm: function() {
8425 		// "Block-extend the active range. Return true if among visible
8426 		// editable nodes that are contained in the result and have no
8427 		// children, at least one has alignment value "center" and at least one
8428 		// does not. Otherwise return false."
8429 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8430 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8431 		});
8432 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "center" })
8433 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "center" });
8434 	}, state: function() {
8435 		// "Block-extend the active range. Return true if there is at least one
8436 		// visible editable node that is contained in the result and has no
8437 		// children, and all such nodes have alignment value "center".
8438 		// Otherwise return false."
8439 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8440 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8441 		});
8442 		return nodes.length
8443 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "center" });
8444 	}, value: function() {
8445 		// "Block-extend the active range, and return the alignment value of
8446 		// the first visible editable node that is contained in the result and
8447 		// has no children. If there is no such node, return "left"."
8448 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8449 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8450 		});
8451 		if (nodes.length) {
8452 			return getAlignmentValue(nodes[0]);
8453 		} else {
8454 			return "left";
8455 		}
8456 	}
8457 };
8458 
8459 //@}
8460 ///// The justifyFull command /////
8461 //@{
8462 commands.justifyfull = {
8463 	// "Justify the selection with alignment "justify"."
8464 	action: function(value, range) { justifySelection("justify", range) },
8465 	indeterm: function() {
8466 		// "Block-extend the active range. Return true if among visible
8467 		// editable nodes that are contained in the result and have no
8468 		// children, at least one has alignment value "justify" and at least
8469 		// one does not. Otherwise return false."
8470 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8471 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8472 		});
8473 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "justify" })
8474 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "justify" });
8475 	}, state: function() {
8476 		// "Block-extend the active range. Return true if there is at least one
8477 		// visible editable node that is contained in the result and has no
8478 		// children, and all such nodes have alignment value "justify".
8479 		// Otherwise return false."
8480 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8481 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8482 		});
8483 		return nodes.length
8484 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "justify" });
8485 	}, value: function() {
8486 		// "Block-extend the active range, and return the alignment value of
8487 		// the first visible editable node that is contained in the result and
8488 		// has no children. If there is no such node, return "left"."
8489 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8490 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8491 		});
8492 		if (nodes.length) {
8493 			return getAlignmentValue(nodes[0]);
8494 		} else {
8495 			return "left";
8496 		}
8497 	}
8498 };
8499 
8500 //@}
8501 ///// The justifyLeft command /////
8502 //@{
8503 commands.justifyleft = {
8504 	// "Justify the selection with alignment "left"."
8505 	action: function(value, range) { justifySelection("left", range) },
8506 	indeterm: function() {
8507 		// "Block-extend the active range. Return true if among visible
8508 		// editable nodes that are contained in the result and have no
8509 		// children, at least one has alignment value "left" and at least one
8510 		// does not. Otherwise return false."
8511 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8512 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8513 		});
8514 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "left" })
8515 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "left" });
8516 	}, state: function() {
8517 		// "Block-extend the active range. Return true if there is at least one
8518 		// visible editable node that is contained in the result and has no
8519 		// children, and all such nodes have alignment value "left".  Otherwise
8520 		// return false."
8521 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8522 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8523 		});
8524 		return nodes.length
8525 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "left" });
8526 	}, value: function() {
8527 		// "Block-extend the active range, and return the alignment value of
8528 		// the first visible editable node that is contained in the result and
8529 		// has no children. If there is no such node, return "left"."
8530 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8531 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8532 		});
8533 		if (nodes.length) {
8534 			return getAlignmentValue(nodes[0]);
8535 		} else {
8536 			return "left";
8537 		}
8538 	}
8539 };
8540 
8541 //@}
8542 ///// The justifyRight command /////
8543 //@{
8544 commands.justifyright = {
8545 	// "Justify the selection with alignment "right"."
8546 	action: function(value, range) { justifySelection("right", range) },
8547 	indeterm: function() {
8548 		// "Block-extend the active range. Return true if among visible
8549 		// editable nodes that are contained in the result and have no
8550 		// children, at least one has alignment value "right" and at least one
8551 		// does not. Otherwise return false."
8552 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8553 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8554 		});
8555 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "right" })
8556 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "right" });
8557 	}, state: function() {
8558 		// "Block-extend the active range. Return true if there is at least one
8559 		// visible editable node that is contained in the result and has no
8560 8561 		// children, and all such nodes have alignment value "right".
8562 		// Otherwise return false."
8563 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8564 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8565 		});
8566 		return nodes.length
8567 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "right" });
8568 	}, value: function() {
8569 		// "Block-extend the active range, and return the alignment value of
8570 		// the first visible editable node that is contained in the result and
8571 		// has no children. If there is no such node, return "left"."
8572 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8573 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8574 		});
8575 		if (nodes.length) {
8576 			return getAlignmentValue(nodes[0]);
8577 		} else {
8578 			return "left";
8579 		}
8580 	}
8581 };
8582 
8583 //@}
8584 ///// The outdent command /////
8585 //@{
8586 commands.outdent = {
8587 	action: function() {
8588 		// "Let items be a list of all lis that are ancestor containers of the
8589 		// range's start and/or end node."
8590 		//
8591 		// It's annoying to get this in tree order using functional stuff
8592 		// without doing getDescendants(document), which is slow, so I do it
8593 		// imperatively.
8594 		var items = [];
8595 		(function(){
8596 			for (
8597 				var ancestorContainer = getActiveRange().endContainer;
8598 				ancestorContainer != getActiveRange().commonAncestorContainer;
8599 				ancestorContainer = ancestorContainer.parentNode
8600 			) {
8601 				if (isNamedHtmlElement(ancestorContainer, "li")) {
8602 					items.unshift(ancestorContainer);
8603 				}
8604 			}
8605 			for (
8606 				var ancestorContainer = getActiveRange().startContainer;
8607 				ancestorContainer;
8608 				ancestorContainer = ancestorContainer.parentNode
8609 			) {
8610 				if (isNamedHtmlElement(ancestorContainer, "li")) {
8611 					items.unshift(ancestorContainer);
8612 				}
8613 			}
8614 		})();
8615 
8616 		// "For each item in items, normalize sublists of item."
8617 		$_( items ).forEach( function( thisArg) {
8618 			normalizeSublists( thisArg, range);
8619 		});
8620 
8621 		// "Block-extend the active range, and let new range be the result."
8622 		var newRange = blockExtend(getActiveRange());
8623 
8624 		// "Let node list be a list of nodes, initially empty."
8625 		//
8626 		// "For each node node contained in new range, append node to node list
8627 		// if the last member of node list (if any) is not an ancestor of node;
8628 		// node is editable; and either node has no editable descendants, or is
8629 		// an ol or ul, or is an li whose parent is an ol or ul."
8630 		var nodeList = getContainedNodes(newRange, function(node) {
8631 			return isEditable(node)
8632 				&& (!$_( getDescendants(node) ).some(isEditable)
8633 				|| isHtmlElementInArray(node, ["ol", "ul"])
8634 				|| (isNamedHtmlElement(node, 'li') && isHtmlElementInArray(node.parentNode, ["ol", "ul"])));
8635 		});
8636 
8637 		// "While node list is not empty:"
8638 		while (nodeList.length) {
8639 			// "While the first member of node list is an ol or ul or is not
8640 			// the child of an ol or ul, outdent it and remove it from node
8641 			// list."
8642 			while (nodeList.length
8643 			&& (isHtmlElementInArray(nodeList[0], ["OL", "UL"])
8644 			|| !isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"]))) {
8645 				outdentNode(nodeList.shift(), range);
8646 			}
8647 
8648 			// "If node list is empty, break from these substeps."
8649 			if (!nodeList.length) {
8650 				break;
8651 			}
8652 
8653 			// "Let sublist be a list of nodes, initially empty."
8654 			var sublist = [];
8655 
8656 			// "Remove the first member of node list and append it to sublist."
8657 			sublist.push(nodeList.shift());
8658 
8659 			// "While the first member of node list is the nextSibling of the
8660 			// last member of sublist, and the first member of node list is not
8661 			// an ol or ul, remove the first member of node list and append it
8662 			// to sublist."
8663 			while (nodeList.length
8664 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling
8665 			&& !isHtmlElementInArray(nodeList[0], ["OL", "UL"])) {
8666 				sublist.push(nodeList.shift());
8667 			}
8668 
8669 			// "Record the values of sublist, and let values be the result."
8670 			var values = recordValues(sublist);
8671 
8672 			// "Split the parent of sublist, with new parent null."
8673 			splitParent(sublist, range);
8674 
8675 			// "Fix disallowed ancestors of each member of sublist."
8676 			$_( sublist ).forEach(fixDisallowedAncestors);
8677 
8678 			// "Restore the values from values."
8679 			restoreValues(values, range);
8680 		}
8681 	}
8682 };
8683 
8684 //@}
8685 
8686 //////////////////////////////////
8687 ///// Miscellaneous commands /////
8688 //////////////////////////////////
8689 
8690 ///// The selectAll command /////
8691 //@{
8692 commands.selectall = {
8693 	// Note, this ignores the whole globalRange/getActiveRange() thing and
8694 	// works with actual selections.  Not suitable for autoimplementation.html.
8695 	action: function() {
8696 		// "Let target be the body element of the context object."
8697 		var target = document.body;
8698 
8699 		// "If target is null, let target be the context object's
8700 		// documentElement."
8701 		if (!target) {
8702 			target = document.documentElement;
8703 		}
8704 
8705 		// "If target is null, call getSelection() on the context object, and
8706 		// call removeAllRanges() on the result."
8707 		if (!target) {
8708 			Aloha.getSelection().removeAllRanges();
8709 
8710 		// "Otherwise, call getSelection() on the context object, and call
8711 		// selectAllChildren(target) on the result."
8712 		} else {
8713 			Aloha.getSelection().selectAllChildren(target);
8714 		}
8715 	}
8716 };
8717 
8718 //@}
8719 ///// The styleWithCSS command /////
8720 //@{
8721 commands.stylewithcss = {
8722 	action: function(value) {
8723 		// "If value is an ASCII case-insensitive match for the string
8724 		// "false", set the CSS styling flag to false. Otherwise, set the
8725 		// CSS styling flag to true."
8726 		cssStylingFlag = String(value).toLowerCase() != "false";
8727 	}, state: function() { return cssStylingFlag }
8728 };
8729 
8730 //@}
8731 ///// The useCSS command /////
8732 //@{
8733 commands.usecss = {
8734 	action: function(value) {
8735 		// "If value is an ASCII case-insensitive match for the string "false",
8736 		// set the CSS styling flag to true. Otherwise, set the CSS styling
8737 		// flag to false."
8738 		cssStylingFlag = String(value).toLowerCase() == "false";
8739 	}
8740 };
8741 //@}
8742 
8743 // Some final setup
8744 //@{
8745 (function() {
8746 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
8747 // temporary, which means I need an extra closure to not leak the temporaries
8748 // into the global namespace.  >:(
8749 var commandNames = [];
8750 for (var command in commands) {
8751 	commandNames.push(command);
8752 }
8753 $_( commandNames ).forEach(function(command) {
8754 	// "If a command does not have a relevant CSS property specified, it
8755 	// defaults to null."
8756 	if (!("relevantCssProperty" in commands[command])) {
8757 		commands[command].relevantCssProperty = null;
8758 	}
8759 
8760 	// "If a command has inline command activated values defined but
8761 	// nothing else defines when it is indeterminate, it is indeterminate
8762 	// if among editable Text nodes effectively contained in the active
8763 	// range, there is at least one whose effective command value is one of
8764 	// the given values and at least one whose effective command value is
8765 	// not one of the given values."
8766 	if ("inlineCommandActivatedValues" in commands[command]
8767 	&& !("indeterm" in commands[command])) {
8768 		commands[command].indeterm = function( range ) {
8769 			var values = $_( getAllEffectivelyContainedNodes(range, function(node) {
8770 				return isEditable(node)
8771 					&& node.nodeType == $_.Node.TEXT_NODE;
8772 			}) ).map(function(node) { return getEffectiveCommandValue(node, command) });
8773 
8774 			var matchingValues = $_( values ).filter(function(value) {
8775 				return $_( commands[command].inlineCommandActivatedValues ).indexOf(value) != -1;
8776 			});
8777 
8778 			return matchingValues.length >= 1
8779 				&& values.length - matchingValues.length >= 1;
8780 		};
8781 	}
8782 
8783 	// "If a command has inline command activated values defined, its state
8784 	// is true if either no editable Text node is effectively contained in
8785 	// the active range, and the active range's start node's effective
8786 	// command value is one of the given values; or if there is at least
8787 	// one editable Text node effectively contained in the active range,
8788 	// and all of them have an effective command value equal to one of the
8789 	// given values."
8790 	if ("inlineCommandActivatedValues" in commands[command]) {
8791 		commands[command].state = function(range) {
8792 			var nodes = getAllEffectivelyContainedNodes(range, function(node) {
8793 				return isEditable(node)
8794 					&& node.nodeType == $_.Node.TEXT_NODE;
8795 			});
8796 
8797 			if (nodes.length == 0) {
8798 				return $_( commands[command].inlineCommandActivatedValues )
8799 					.indexOf(getEffectiveCommandValue(range.startContainer, command)) != -1;
8800 				return ret;
8801 			} else {
8802 				return $_( nodes ).every(function(node) {
8803 					return $_( commands[command].inlineCommandActivatedValues )
8804 						.indexOf(getEffectiveCommandValue(node, command)) != -1;
8805 				});
8806 			}
8807 		};
8808 	}
8809 
8810 	// "If a command is a standard inline value command, it is
8811 	// indeterminate if among editable Text nodes that are effectively
8812 	// contained in the active range, there are two that have distinct
8813 	// effective command values. Its value is the effective command value
8814 	// of the first editable Text node that is effectively contained in the
8815 	// active range, or if there is no such node, the effective command
8816 	// value of the active range's start node."
8817 	if ("standardInlineValueCommand" in commands[command]) {
8818 		commands[command].indeterm = function() {
8819 			var values = $_(getAllEffectivelyContainedNodes(getActiveRange()))
8820 				.filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE }, true)
8821 				.map(function(node) { return getEffectiveCommandValue(node, command) });
8822 			for (var i = 1; i < values.length; i++) {
8823 				if (values[i] != values[i - 1]) {
8824 					return true;
8825 				}
8826 			}
8827 			return false;
8828 		};
8829 
8830 		commands[command].value = function(range) {
8831 			var refNode = getAllEffectivelyContainedNodes(range, function(node) {
8832 				return isEditable(node)
8833 					&& node.nodeType == $_.Node.TEXT_NODE;
8834 			})[0];
8835 
8836 			if (typeof refNode == "undefined") {
8837 				refNode = range.startContainer;
8838 			}
8839 
8840 			return getEffectiveCommandValue(refNode, command);
8841 		};
8842 	}
8843 });
8844 })();
8845 //@}
8846 return {
8847 	commands: commands,
8848 	execCommand: myExecCommand,
8849 	queryCommandIndeterm: myQueryCommandIndeterm,
8850 	queryCommandState: myQueryCommandState,
8851 	queryCommandValue: myQueryCommandValue,
8852 	queryCommandEnabled: myQueryCommandEnabled,
8853 	queryCommandSupported: myQueryCommandSupported,
8854 	copyAttributes: copyAttributes,
8855 	createEndBreak: createEndBreak,
8856 	isEndBreak: isEndBreak,
8857 	ensureContainerEditable: ensureContainerEditable,
8858 	isEditingHost: isEditingHost,
8859 	isEditable: isEditable
8860 }
8861 }); // end define
8862 // vim: foldmarker=@{,@} foldmethod=marker
8863