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 	if (!isNamedHtmlElement(element, 'br')) {
6584 		return false;
6585 	}
6586 	return jQuery(element).hasClass('aloha-end-br');
6587 }
6588 
6589 //@}
6590 ///// Create an end break /////
6591 //@{
6592 function createEndBreak() {
6593 	var endBr = document.createElement("br");
6594 	endBr.setAttribute("class", "aloha-end-br");
6595 
6596 	return endBr;
6597 }
6598 
6599 /**
6600  * Ensure the container is editable
6601  * E.g. when called for an empty paragraph or header, and the browser is not IE,
6602  * we need to append a br (marked with class aloha-end-br)
6603  * For IE7, there is a special behaviour that will append zero-width whitespace
6604  * @param {DOMNode} container
6605  */
6606 function ensureContainerEditable(container) {
6607 	if (!container) {
6608 		return;
6609 	}
6610 
6611 	if (isNamedHtmlElement(container.lastChild, "br")) {
6612 		return;
6613 	}
6614 
6615 	if ($_(container.childNodes).some(isVisible)) {
6616 		return;
6617 	}
6618 
6619 	if (!jQuery.browser.msie) {
6620 		// for normal browsers, the end-br will do
6621 		container.appendChild(createEndBreak());
6622 	} else if (jQuery.browser.msie && jQuery.browser.version <= 7 &&
6623 			isHtmlElementInArray(container, ["p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "blockquote"])) {
6624 		// for IE7, we need to insert a text node containing a single zero-width whitespace character
6625 		if (!container.firstChild) {
6626 			container.appendChild(document.createTextNode('\u200b'));
6627 		}
6628 	}
6629 }
6630 
6631 //@}
6632 ///// Move the given collapsed range over adjacent zero-width whitespace characters.
6633 ///// The range is 
6634 //@{
6635 /**
6636  * Move the given collapsed range over adjacent zero-width whitespace characters.
6637  * If the range is not collapsed or is not contained in a text node, it is not modified
6638  * @param range range to modify
6639  * @param forward {Boolean} true to move forward, false to move backward
6640  */
6641 function moveOverZWSP(range, forward) {
6642 	var offset;
6643 	if (!range.collapsed) {
6644 		return;
6645 	}
6646 
6647 	offset = range.startOffset;
6648 
6649 	if (forward) {
6650 		// check whether the range starts in a text node
6651 		if (range.startContainer && range.startContainer.nodeType === $_.Node.TEXT_NODE) {
6652 			// move forward (i.e. increase offset) as long as we stay in the text node and have zwsp characters to the right
6653 			while (offset < range.startContainer.data.length && range.startContainer.data.charAt(offset) === '\u200b') {
6654 				offset++;
6655 			}
6656 		}
6657 	} else {
6658 		// check whether the range starts in a text node
6659 		if (range.startContainer && range.startContainer.nodeType === $_.Node.TEXT_NODE) {
6660 			// move backward (i.e. decrease offset) as long as we stay in the text node and have zwsp characters to the left
6661 			while (offset > 0 && range.startContainer.data.charAt(offset - 1) === '\u200b') {
6662 				offset--;
6663 			}
6664 		}
6665 	}
6666 
6667 	// if the offset was changed, set it back to the collapsed range
6668 	if (offset !== range.startOffset) {
6669 		range.setStart(range.startContainer, offset);
6670 		range.setEnd(range.startContainer, offset);
6671 	}
6672 }
6673 
6674 /**
6675  * implementation of the delete command
6676  * will attempt to delete contents within range if non-collapsed
6677  * or delete the character left of the cursor position if range
6678  * is collapsed. Is used to define the behaviour of the backspace
6679  * button.
6680  *
6681  * @param      value   is just there for compatibility with the commands api. parameter is ignored.
6682  * @param      range   the range to execute the delete command for
6683  * @return     void
6684  */
6685 commands["delete"] = {
6686 	action: function(value, range) {
6687 		// special behaviour for skipping zero-width whitespaces in IE7
6688 		if (jQuery.browser.msie && jQuery.browser.version <= 7) {
6689 			moveOverZWSP(range, false);
6690 		}
6691 
6692 		// "If the active range is not collapsed, delete the contents of the
6693 		// active range and abort these steps."
6694 		if (!range.collapsed) {
6695 			deleteContents(range);
6696 			return;
6697 		}
6698 
6699 		// "Canonicalize whitespace at (active range's start node, active
6700 		// range's start offset)."
6701 		canonicalizeWhitespace(range.startContainer, range.startOffset);
6702 
6703 		// "Let node and offset be the active range's start node and offset."
6704 		var node = range.startContainer;
6705 		var offset = range.startOffset;
6706 		var isBr = false;
6707 		var isHr = false;
6708 
6709 		// "Repeat the following steps:"
6710 		while ( true ) {
6711 			// we need to reset isBr and isHr on every interation of the loop
6712 			if ( offset > 0 ) {
6713 				isBr = isNamedHtmlElement(node.childNodes[offset - 1], "br") || false;
6714 				isHr = isNamedHtmlElement(node.childNodes[offset - 1], "hr") || false;
6715 			}
6716 
6717 			// "If offset is zero and node's previousSibling is an editable
6718 			// invisible node, remove node's previousSibling from its parent."
6719 			if (offset == 0
6720 			&& isEditable(node.previousSibling)
6721 			&& isInvisible(node.previousSibling)) {
6722 				node.parentNode.removeChild(node.previousSibling);
6723 
6724 			// "Otherwise, if node has a child with index offset − 1 and that
6725 			// child is an editable invisible node, remove that child from
6726 			// node, then subtract one from offset."
6727 			} else if (0 <= offset - 1
6728 			&& offset - 1 < node.childNodes.length
6729 			&& isEditable(node.childNodes[offset - 1])
6730 			&& (isInvisible(node.childNodes[offset - 1]) || isBr || isHr )) {
6731 				node.removeChild(node.childNodes[offset - 1]);
6732 				offset--;
6733 				if (isBr || isHr) {
6734 					range.setStart(node, offset);
6735 					range.setEnd(node, offset);
6736 					return;
6737 				}
6738 
6739 			// "Otherwise, if offset is zero and node is an inline node, or if
6740 			// node is an invisible node, set offset to the index of node, then
6741 			// set node to its parent."
6742 			} else if ((offset == 0
6743 			&& isInlineNode(node))
6744 			|| isInvisible(node)) {
6745 				offset = getNodeIndex(node);
6746 				node = node.parentNode;
6747 
6748 			// "Otherwise, if node has a child with index offset − 1 and that
6749 			// child is an editable a, remove that child from node, preserving
6750 			// its descendants. Then abort these steps."
6751 			} else if (0 <= offset - 1
6752 			&& offset - 1 < node.childNodes.length
6753 			&& isEditable(node.childNodes[offset - 1])
6754 			&& isNamedHtmlElement(node.childNodes[offset - 1], "a")) {
6755 				removePreservingDescendants(node.childNodes[offset - 1], range);
6756 				return;
6757 
6758 			// "Otherwise, if node has a child with index offset − 1 and that
6759 			// child is not a block node or a br or an img, set node to that
6760 			// child, then set offset to the length of node."
6761 			} else if (0 <= offset - 1
6762 			&& offset - 1 < node.childNodes.length
6763 			&& !isBlockNode(node.childNodes[offset - 1])
6764 			&& !isHtmlElementInArray(node.childNodes[offset - 1], ["br", "img"])) {
6765 				node = node.childNodes[offset - 1];
6766 				offset = getNodeLength(node);
6767 
6768 			// "Otherwise, break from this loop."
6769 			} else {
6770 				break;
6771 			}
6772 		}
6773 
6774 		// if the previous node is an aloha-table we want to delete it
6775 		var delBlock;
6776 		if (delBlock = getBlockAtPreviousPosition(node, offset)) {
6777 			delBlock.parentNode.removeChild(delBlock);
6778 			return;
6779 		}
6780 
6781 		// "If node is a Text node and offset is not zero, call collapse(node,
6782 		// offset) on the Selection. Then delete the contents of the range with
6783 		// start (node, offset − 1) and end (node, offset) and abort these
6784 		// steps."
6785 		if (node.nodeType == $_.Node.TEXT_NODE
6786 		&& offset != 0) {
6787 			range.setStart(node, offset - 1);
6788 			range.setEnd(node, offset - 1);
6789 			deleteContents(node, offset - 1, node, offset);
6790 			return;
6791 		}
6792 
6793 		// @iebug
6794 		// when inserting a special char via the plugin 
6795 		// there where problems deleting them again with backspace after insertation
6796 		// see https://github.com/alohaeditor/Aloha-Editor/issues/517
6797 		if (node.nodeType == $_.Node.TEXT_NODE
6798 		&& offset == 0 && jQuery.browser.msie) {
6799 			offset = 1;
6800 			range.setStart(node, offset);
6801 			range.setEnd(node, offset);
6802 			range.startOffset = 0;
6803 			deleteContents(range);
6804 			return;
6805 		}
6806 
6807 		// "If node is an inline node, abort these steps."
6808 		if (isInlineNode(node)) {
6809 			return;
6810 		}
6811 
6812 		// "If node has a child with index offset − 1 and that child is a br or
6813 		// hr or img, call collapse(node, offset) on the Selection. Then delete
6814 		// the contents of the range with start (node, offset − 1) and end
6815 		// (node, offset) and abort these steps."
6816 		if (0 <= offset - 1
6817 		&& offset - 1 < node.childNodes.length
6818 		&& isHtmlElementInArray(node.childNodes[offset - 1], ["br", "hr", "img"])) {
6819 			range.setStart(node, offset);
6820 			range.setEnd(node, offset);
6821 			deleteContents(range);
6822 			return;
6823 		}
6824 
6825 		// "If node is an li or dt or dd and is the first child of its parent,
6826 		// and offset is zero:"
6827 		if (isHtmlElementInArray(node, ["li", "dt", "dd"])
6828 		&& node == node.parentNode.firstChild
6829 		&& offset == 0) {
6830 			// "Let items be a list of all lis that are ancestors of node."
6831 			//
6832 			// Remember, must be in tree order.
6833 			var items = [];
6834 			for (var ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
6835 				if (isNamedHtmlElement(ancestor, 'li')) {
6836 					items.unshift(ancestor);
6837 				}
6838 			}
6839 
6840 			// "Normalize sublists of each item in items."
6841 			for (var i = 0; i < items.length; i++) {
6842 				normalizeSublists(items[i], range);
6843 			}
6844 
6845 			// "Record the values of the one-node list consisting of node, and
6846 			// let values be the result."
6847 			var values = recordValues([node]);
6848 
6849 			// "Split the parent of the one-node list consisting of node."
6850 			splitParent([node], range);
6851 
6852 			// "Restore the values from values."
6853 			restoreValues(values, range);
6854 
6855 			// "If node is a dd or dt, and it is not an allowed child of any of
6856 			// its ancestors in the same editing host, set the tag name of node
6857 			// to the default single-line container name and let node be the
6858 			// result."
6859 			if (isHtmlElementInArray(node, ["dd", "dt"])
6860 			&& $_(getAncestors(node)).every(function(ancestor) {
6861 				return !inSameEditingHost(node, ancestor)
6862 					|| !isAllowedChild(node, ancestor)
6863 			})) {
6864 				node = setTagName(node, defaultSingleLineContainerName, range);
6865 			}
6866 
6867 			// "Fix disallowed ancestors of node."
6868 			fixDisallowedAncestors(node, range);
6869 
6870 			// fix the lists to be html5 conformant
6871 			for (var i = 0; i < items.length; i++) {
6872 				unNormalizeSublists(items[i].parentNode, range);
6873 			}
6874 
6875 			// "Abort these steps."
6876 			return;
6877 		}
6878 
6879 		// "Let start node equal node and let start offset equal offset."
6880 		var startNode = node;
6881 		var startOffset = offset;
6882 
6883 		// "Repeat the following steps:"
6884 		while (true) {
6885 			// "If start offset is zero, set start offset to the index of start
6886 			// node and then set start node to its parent."
6887 			if (startOffset == 0) {
6888 				startOffset = getNodeIndex(startNode);
6889 				startNode = startNode.parentNode;
6890 
6891 			// "Otherwise, if start node has an editable invisible child with
6892 			// index start offset minus one, remove it from start node and
6893 			// subtract one from start offset."
6894 			} else if (0 <= startOffset - 1
6895 			&& startOffset - 1 < startNode.childNodes.length
6896 			&& isEditable(startNode.childNodes[startOffset - 1])
6897 			&& isInvisible(startNode.childNodes[startOffset - 1])) {
6898 				startNode.removeChild(startNode.childNodes[startOffset - 1]);
6899 				startOffset--;
6900 
6901 			// "Otherwise, break from this loop."
6902 			} else {
6903 				break;
6904 			}
6905 		}
6906 
6907 		// "If offset is zero, and node has an editable ancestor container in
6908 		// the same editing host that's an indentation element:"
6909 		if (offset == 0
6910 		&& $_( getAncestors(node).concat(node) ).filter(function(ancestor) {
6911 			return isEditable(ancestor)
6912 				&& inSameEditingHost(ancestor, node)
6913 				&& isIndentationElement(ancestor);
6914 		}).length) {
6915 			// "Block-extend the range whose start and end are both (node, 0),
6916 			// and let new range be the result."
6917 			var newRange = Aloha.createRange();
6918 			newRange.setStart(node, 0);
6919 			newRange.setEnd(node, 0);
6920 			newRange = blockExtend(newRange);
6921 
6922 			// "Let node list be a list of nodes, initially empty."
6923 			//
6924 			// "For each node current node contained in new range, append
6925 			// current node to node list if the last member of node list (if
6926 			// any) is not an ancestor of current node, and current node is
6927 			// editable but has no editable descendants."
6928 			var nodeList = getContainedNodes(newRange, function(currentNode) {
6929 				return isEditable(currentNode)
6930 					&& !hasEditableDescendants(currentNode);
6931 			});
6932 
6933 			// "Outdent each node in node list."
6934 			for (var i = 0; i < nodeList.length; i++) {
6935 				outdentNode(nodeList[i], range);
6936 			}
6937 
6938 			// "Abort these steps."
6939 			return;
6940 		}
6941 
6942 		// "If the child of start node with index start offset is a table,
6943 		// abort these steps."
6944 		if (isNamedHtmlElement(startNode.childNodes[startOffset], "table")) {
6945 			return;
6946 		}
6947 
6948 		// "If start node has a child with index start offset − 1, and that
6949 		// child is a table:"
6950 		if (0 <= startOffset - 1
6951 		&& startOffset - 1 < startNode.childNodes.length
6952 		&& isNamedHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
6953 			// "Call collapse(start node, start offset − 1) on the context
6954 			// object's Selection."
6955 			range.setStart(startNode, startOffset - 1);
6956 
6957 			// "Call extend(start node, start offset) on the context object's
6958 			// Selection."
6959 			range.setEnd(startNode, startOffset);
6960 
6961 			// "Abort these steps."
6962 			return;
6963 		}
6964 
6965 		// "If offset is zero; and either the child of start node with index
6966 		// start offset minus one is an hr, or the child is a br whose
6967 		// previousSibling is either a br or not an inline node:"
6968 		if (offset == 0
6969 		&& (isNamedHtmlElement(startNode.childNodes[startOffset - 1], "hr")
6970 			|| (
6971 				isNamedHtmlElement(startNode.childNodes[startOffset - 1], "br")
6972 				&& (
6973 					isNamedHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br")
6974 					|| !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling)
6975 				)
6976 			)
6977 		)) {
6978 			// "Call collapse(node, offset) on the Selection."
6979 			range.setStart(node, offset);
6980 			range.setEnd(node, offset);
6981 
6982 			// "Delete the contents of the range with start (start node, start
6983 			// offset − 1) and end (start node, start offset)."
6984 			deleteContents(startNode, startOffset - 1, startNode, startOffset);
6985 
6986 			// "Abort these steps."
6987 			return;
6988 		}
6989 
6990 		// "If the child of start node with index start offset is an li or dt
6991 		// or dd, and that child's firstChild is an inline node, and start
6992 		// offset is not zero:"
6993 		if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"])
6994 		&& isInlineNode(startNode.childNodes[startOffset].firstChild)
6995 		&& startOffset != 0) {
6996 			// "Let previous item be the child of start node with index start
6997 			// offset minus one."
6998 			var previousItem = startNode.childNodes[startOffset - 1];
6999 
7000 			// "If previous item's lastChild is an inline node other than a br,
7001 			// call createElement("br") on the context object and append the
7002 			// result as the last child of previous item."
7003 			if (isInlineNode(previousItem.lastChild)
7004 			&& !isNamedHtmlElement(previousItem.lastChild, "br")) {
7005 				previousItem.appendChild(document.createElement("br"));
7006 			}
7007 
7008 			// "If previous item's lastChild is an inline node, call
7009 			// createElement("br") on the context object and append the result
7010 			// as the last child of previous item."
7011 			if (isInlineNode(previousItem.lastChild)) {
7012 				previousItem.appendChild(document.createElement("br"));
7013 			}
7014 		}
7015 
7016 		// "If the child of start node with index start offset is an li or dt
7017 		// or dd, and its previousSibling is also an li or dt or dd, set start
7018 		// node to its child with index start offset − 1, then set start offset
7019 		// to start node's length, then set node to start node's nextSibling,
7020 		// then set offset to 0."
7021 		if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"])
7022 		&& isHtmlElementInArray(startNode.childNodes[startOffset - 1], ["li", "dt", "dd"])) {
7023 			startNode = startNode.childNodes[startOffset - 1];
7024 			startOffset = getNodeLength(startNode);
7025 			node = startNode.nextSibling;
7026 			offset = 0;
7027 
7028 		// "Otherwise, while start node has a child with index start offset
7029 		// minus one:"
7030 		} else {
7031 			while (0 <= startOffset - 1
7032 			&& startOffset - 1 < startNode.childNodes.length) {
7033 				// "If start node's child with index start offset minus one is
7034 				// editable and invisible, remove it from start node, then
7035 				// subtract one from start offset."
7036 				if (isEditable(startNode.childNodes[startOffset - 1])
7037 				&& isInvisible(startNode.childNodes[startOffset - 1])) {
7038 					startNode.removeChild(startNode.childNodes[startOffset - 1]);
7039 					startOffset--;
7040 
7041 				// "Otherwise, set start node to its child with index start
7042 				// offset minus one, then set start offset to the length of
7043 				// start node."
7044 				} else {
7045 					startNode = startNode.childNodes[startOffset - 1];
7046 					startOffset = getNodeLength(startNode);
7047 				}
7048 			}
7049 		}
7050 
7051 		// "Delete the contents of the range with start (start node, start
7052 		// offset) and end (node, offset)."
7053 		var delRange = Aloha.createRange();
7054 		delRange.setStart(startNode, startOffset);
7055 		delRange.setEnd(node, offset);
7056 		deleteContents(delRange);
7057 
7058 		if (!isAncestorContainer(document.body, range.startContainer)) {
7059 			if (delRange.startContainer.hasChildNodes() || delRange.startContainer.nodeType == $_.Node.TEXT_NODE) {
7060 				range.setStart(delRange.startContainer, delRange.startOffset);
7061 				range.setEnd(delRange.startContainer, delRange.startOffset);
7062 			} else {
7063 				range.setStart(delRange.startContainer.parentNode, getNodeIndex(delRange.startContainer));
7064 				range.setEnd(delRange.startContainer.parentNode, getNodeIndex(delRange.startContainer));
7065 			}
7066 		}
7067 	}
7068 };
7069 
7070 //@}
7071 ///// The formatBlock command /////
7072 //@{
7073 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
7074 // "h4", "h5", "h6", "p", or "pre"."
7075 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
7076 	"h4", "h5", "h6", "p", "pre"];
7077 
7078 commands.formatblock = {
7079 	action: function(value) {
7080 		// "If value begins with a "<" character and ends with a ">" character,
7081 		// remove the first and last characters from it."
7082 		if (/^<.*>$/.test(value)) {
7083 			value = value.slice(1, -1);
7084 		}
7085 
7086 		// "Let value be converted to ASCII lowercase."
7087 		value = value.toLowerCase();
7088 
7089 		// "If value is not a formattable block name, abort these steps and do
7090 		// nothing."
7091 		if ($_(formattableBlockNames).indexOf(value) == -1) {
7092 			return;
7093 		}
7094 
7095 		// "Block-extend the active range, and let new range be the result."
7096 		var newRange = blockExtend(getActiveRange());
7097 
7098 		// "Let node list be an empty list of nodes."
7099 		//
7100 		// "For each node node contained in new range, append node to node list
7101 		// if it is editable, the last member of original node list (if any) is
7102 		// not an ancestor of node, node is either a non-list single-line
7103 		// container or an allowed child of "p" or a dd or dt, and node is not
7104 		// the ancestor of a prohibited paragraph child."
7105 		var nodeList = getContainedNodes(newRange, function(node) {
7106 			return isEditable(node)
7107 				&& (isNonListSingleLineContainer(node)
7108 				|| isAllowedChild(node, "p")
7109 				|| isHtmlElementInArray(node, ["dd", "dt"]))
7110 				&& !$_( getDescendants(node) ).some(isProhibitedParagraphChild);
7111 		});
7112 
7113 		// "Record the values of node list, and let values be the result."
7114 		var values = recordValues(nodeList);
7115 
7116 		// "For each node in node list, while node is the descendant of an
7117 		// editable HTML element in the same editing host, whose local name is
7118 		// a formattable block name, and which is not the ancestor of a
7119 		// prohibited paragraph child, split the parent of the one-node list
7120 		// consisting of node."
7121 		for (var i = 0; i < nodeList.length; i++) {
7122 			var node = nodeList[i];
7123 			while ($_( getAncestors(node) ).some(function(ancestor) {
7124 				return isEditable(ancestor)
7125 					&& inSameEditingHost(ancestor, node)
7126 					&& isHtmlElement_obsolete(ancestor, formattableBlockNames)
7127 					&& !$_( getDescendants(ancestor) ).some(isProhibitedParagraphChild);
7128 			})) {
7129 				splitParent([node], range);
7130 			}
7131 		}
7132 
7133 		// "Restore the values from values."
7134 		restoreValues(values, range);
7135 
7136 		// "While node list is not empty:"
7137 		while (nodeList.length) {
7138 			var sublist;
7139 
7140 			// "If the first member of node list is a single-line
7141 			// container:"
7142 			if (isSingleLineContainer(nodeList[0])) {
7143 				// "Let sublist be the children of the first member of node
7144 				// list."
7145 				sublist = [].slice.call(toArray(nodeList[0].childNodes));
7146 
7147 				// "Record the values of sublist, and let values be the
7148 				// result."
7149 				var values = recordValues(sublist);
7150 
7151 				// "Remove the first member of node list from its parent,
7152 				// preserving its descendants."
7153 				removePreservingDescendants(nodeList[0], range);
7154 
7155 				// "Restore the values from values."
7156 				restoreValues(values, range);
7157 
7158 				// "Remove the first member from node list."
7159 				nodeList.shift();
7160 
7161 			// "Otherwise:"
7162 			} else {
7163 				// "Let sublist be an empty list of nodes."
7164 				sublist = [];
7165 
7166 				// "Remove the first member of node list and append it to
7167 				// sublist."
7168 				sublist.push(nodeList.shift());
7169 
7170 				// "While node list is not empty, and the first member of
7171 				// node list is the nextSibling of the last member of
7172 				// sublist, and the first member of node list is not a
7173 				// single-line container, and the last member of sublist is
7174 				// not a br, remove the first member of node list and
7175 				// append it to sublist."
7176 				while (nodeList.length
7177 				&& nodeList[0] == sublist[sublist.length - 1].nextSibling
7178 				&& !isSingleLineContainer(nodeList[0])
7179 				&& !isNamedHtmlElement(sublist[sublist.length - 1], "BR")) {
7180 					sublist.push(nodeList.shift());
7181 				}
7182 			}
7183 
7184 			// "Wrap sublist. If value is "div" or "p", sibling criteria
7185 			// returns false; otherwise it returns true for an HTML element
7186 			// with local name value and no attributes, and false otherwise.
7187 			// New parent instructions return the result of running
7188 			// createElement(value) on the context object. Then fix disallowed
7189 			// ancestors of the result."
7190 			fixDisallowedAncestors(
7191 				wrap(sublist,
7192 					jQuery.inArray(value, ["div", "p"]) == - 1
7193 						? function(node) { return isHtmlElement_obsolete(node, value) && !node.attributes.length }
7194 						: function() { return false },
7195 					function() { return document.createElement(value) },
7196 					range
7197 				),
7198 				range
7199 			);
7200 		}
7201 	}, indeterm: function() {
7202 		// "Block-extend the active range, and let new range be the result."
7203 		var newRange = blockExtend(getActiveRange());
7204 
7205 		// "Let node list be all visible editable nodes that are contained in
7206 		// new range and have no children."
7207 		var nodeList = getAllContainedNodes(newRange, function(node) {
7208 			return isVisible(node)
7209 				&& isEditable(node)
7210 				&& !node.hasChildNodes();
7211 		});
7212 
7213 		// "If node list is empty, return false."
7214 		if (!nodeList.length) {
7215 			return false;
7216 		}
7217 
7218 		// "Let type be null."
7219 		var type = null;
7220 
7221 		// "For each node in node list:"
7222 		for (var i = 0; i < nodeList.length; i++) {
7223 			var node = nodeList[i];
7224 
7225 			// "While node's parent is editable and in the same editing host as
7226 			// node, and node is not an HTML element whose local name is a
7227 			// formattable block name, set node to its parent."
7228 			while (isEditable(node.parentNode)
7229 			&& inSameEditingHost(node, node.parentNode)
7230 			&& !isHtmlElement_obsolete(node, formattableBlockNames)) {
7231 				node = node.parentNode;
7232 			}
7233 
7234 			// "Let current type be the empty string."
7235 			var currentType = "";
7236 
7237 			// "If node is an editable HTML element whose local name is a
7238 			// formattable block name, and node is not the ancestor of a
7239 			// prohibited paragraph child, set current type to node's local
7240 			// name."
7241 			if (isEditable(node)
7242 			&& isHtmlElement_obsolete(node, formattableBlockNames)
7243 			&& !$_( getDescendants(node) ).some(isProhibitedParagraphChild)) {
7244 				currentType = node.tagName;
7245 			}
7246 
7247 			// "If type is null, set type to current type."
7248 			if (type === null) {
7249 				type = currentType;
7250 
7251 			// "Otherwise, if type does not equal current type, return true."
7252 			} else if (type != currentType) {
7253 				return true;
7254 			}
7255 		}
7256 
7257 		// "Return false."
7258 		return false;
7259 	}, value: function() {
7260 		// "Block-extend the active range, and let new range be the result."
7261 		var newRange = blockExtend(getActiveRange());
7262 
7263 		// "Let node be the first visible editable node that is contained in
7264 		// new range and has no children. If there is no such node, return the
7265 		// empty string."
7266 		var nodes = getAllContainedNodes(newRange, function(node) {
7267 			return isVisible(node)
7268 				&& isEditable(node)
7269 				&& !node.hasChildNodes();
7270 		});
7271 		if (!nodes.length) {
7272 			return "";
7273 		}
7274 		var node = nodes[0];
7275 
7276 		// "While node's parent is editable and in the same editing host as
7277 		// node, and node is not an HTML element whose local name is a
7278 		// formattable block name, set node to its parent."
7279 		while (isEditable(node.parentNode)
7280 		&& inSameEditingHost(node, node.parentNode)
7281 		&& !isHtmlElement_obsolete(node, formattableBlockNames)) {
7282 			node = node.parentNode;
7283 		}
7284 
7285 		// "If node is an editable HTML element whose local name is a
7286 		// formattable block name, and node is not the ancestor of a prohibited
7287 		// paragraph child, return node's local name, converted to ASCII
7288 		// lowercase."
7289 		if (isEditable(node)
7290 		&& isHtmlElement_obsolete(node, formattableBlockNames)
7291 		&& !$_( getDescendants(node) ).some(isProhibitedParagraphChild)) {
7292 			return node.tagName.toLowerCase();
7293 		}
7294 
7295 		// "Return the empty string."
7296 		return "";
7297 	}
7298 };
7299 
7300 //@}
7301 ///// The forwardDelete command /////
7302 //@{
7303 commands.forwarddelete = {
7304 	action: function(value, range) {
7305 		// special behaviour for skipping zero-width whitespaces in IE7
7306 		if (jQuery.browser.msie && jQuery.browser.version <= 7) {
7307 			moveOverZWSP(range, true);
7308 		}
7309 
7310 		// "If the active range is not collapsed, delete the contents of the
7311 		// active range and abort these steps."
7312 		if (!range.collapsed) {
7313 			deleteContents(range);
7314 			return;
7315 		}
7316 
7317 		// "Canonicalize whitespace at (active range's start node, active
7318 		// range's start offset)."
7319 		canonicalizeWhitespace(range.startContainer, range.startOffset);
7320 
7321 		// "Let node and offset be the active range's start node and offset."
7322 		var node = range.startContainer;
7323 		var offset = range.startOffset;
7324 		var isBr = false;
7325 		var isHr = false;
7326 
7327 		// "Repeat the following steps:"
7328 		while (true) {
7329 			// check whether the next element is a br or hr
7330 			if ( offset < node.childNodes.length ) {
7331 //				isBr = isHtmlElement_obsolete(node.childNodes[offset], "br") || false;
7332 //				isHr = isHtmlElement_obsolete(node.childNodes[offset], "hr") || false;
7333 			}
7334 
7335 			// "If offset is the length of node and node's nextSibling is an
7336 			// editable invisible node, remove node's nextSibling from its
7337 			// parent."
7338 			if (offset == getNodeLength(node)
7339 			&& isEditable(node.nextSibling)
7340 			&& isInvisible(node.nextSibling)) {
7341 				node.parentNode.removeChild(node.nextSibling);
7342 
7343 			// "Otherwise, if node has a child with index offset and that child
7344 			// is an editable invisible node, remove that child from node."
7345 			} else if (offset < node.childNodes.length
7346 			&& isEditable(node.childNodes[offset])
7347 			&& (isInvisible(node.childNodes[offset]) || isBr || isHr )) {
7348 				node.removeChild(node.childNodes[offset]);
7349 				if (isBr || isHr) {
7350 					ensureContainerEditable(node);
7351 					range.setStart(node, offset);
7352 					range.setEnd(node, offset);
7353 					return;
7354 				}
7355 
7356 			// "Otherwise, if node has a child with index offset and that child
7357 			// is a collapsed block prop, add one to offset."
7358 			} else if (offset < node.childNodes.length
7359 			&& isCollapsedBlockProp(node.childNodes[offset])) {
7360 				offset++;
7361 
7362 			// "Otherwise, if offset is the length of node and node is an
7363 			// inline node, or if node is invisible, set offset to one plus the
7364 			// index of node, then set node to its parent."
7365 			} else if ((offset == getNodeLength(node)
7366 			&& isInlineNode(node))
7367 			|| isInvisible(node)) {
7368 				offset = 1 + getNodeIndex(node);
7369 				node = node.parentNode;
7370 
7371 			// "Otherwise, if node has a child with index offset and that child
7372 			// is not a block node or a br or an img, set node to that child,
7373 			// then set offset to zero."
7374 			} else if (offset < node.childNodes.length
7375 			&& !isBlockNode(node.childNodes[offset])
7376 			&& !isHtmlElementInArray(node.childNodes[offset], ["br", "img"])) {
7377 				node = node.childNodes[offset];
7378 				offset = 0;
7379 
7380 			// "Otherwise, break from this loop."
7381 			} else {
7382 				break;
7383 			}
7384 		}
7385 
7386 		// collapse whitespace in the node, if it is a text node
7387 		canonicalizeWhitespace(range.startContainer, range.startOffset);
7388 
7389 		// if the next node is an aloha-table we want to delete it
7390 		var delBlock;
7391 		if (delBlock = getBlockAtNextPosition(node, offset)) {
7392 			delBlock.parentNode.removeChild(delBlock);
7393 			return;
7394 		}
7395 		
7396 		// "If node is a Text node and offset is not node's length:"
7397 		if (node.nodeType == $_.Node.TEXT_NODE
7398 		&& offset != getNodeLength(node)) {
7399 			// "Call collapse(node, offset) on the Selection."
7400 			range.setStart(node, offset);
7401 			range.setEnd(node, offset);
7402 
7403 			// "Let end offset be offset plus one."
7404 			var endOffset = offset + 1;
7405 
7406 			// "While end offset is not node's length and the end offsetth
7407 			// element of node's data has general category M when interpreted
7408 			// as a Unicode code point, add one to end offset."
7409 			//
7410 			// TODO: Not even going to try handling anything beyond the most
7411 			// basic combining marks, since I couldn't find a good list.  I
7412 			// special-case a few Hebrew diacritics too to test basic coverage
7413 			// of non-Latin stuff.
7414 			while (endOffset != node.length
7415 			&& /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) {
7416 				endOffset++;
7417 			}
7418 
7419 			// "Delete the contents of the range with start (node, offset) and
7420 			// end (node, end offset)."
7421 			deleteContents(node, offset, node, endOffset);
7422 
7423 			// "Abort these steps."
7424 			return;
7425 		}
7426 
7427 		// "If node is an inline node, abort these steps."
7428 		if (isInlineNode(node)) {
7429 			return;
7430 		}
7431 
7432 		// "If node has a child with index offset and that child is a br or hr
7433 		// or img, call collapse(node, offset) on the Selection. Then delete
7434 		// the contents of the range with start (node, offset) and end (node,
7435 		// offset + 1) and abort these steps."
7436 		if (offset < node.childNodes.length
7437 		&& isHtmlElementInArray(node.childNodes[offset], ["br", "hr", "img"])) {
7438 			range.setStart(node, offset);
7439 			range.setEnd(node, offset);
7440 			deleteContents(node, offset, node, offset + 1);
7441 			return;
7442 		}
7443 
7444 		// "Let end node equal node and let end offset equal offset."
7445 		var endNode = node;
7446 		var endOffset = offset;
7447 
7448 		// "Repeat the following steps:"
7449 		while (true) {
7450 			// "If end offset is the length of end node, set end offset to one
7451 			// plus the index of end node and then set end node to its parent."
7452 			if (endOffset == getNodeLength(endNode)) {
7453 				endOffset = 1 + getNodeIndex(endNode);
7454 				endNode = endNode.parentNode;
7455 
7456 			// "Otherwise, if end node has a an editable invisible child with
7457 			// index end offset, remove it from end node."
7458 			} else if (endOffset < endNode.childNodes.length
7459 			&& isEditable(endNode.childNodes[endOffset])
7460 			&& isInvisible(endNode.childNodes[endOffset])) {
7461 				endNode.removeChild(endNode.childNodes[endOffset]);
7462 
7463 			// "Otherwise, break from this loop."
7464 			} else {
7465 				break;
7466 			}
7467 		}
7468 
7469 		// "If the child of end node with index end offset minus one is a
7470 		// table, abort these steps."
7471 		if (isNamedHtmlElement(endNode.childNodes[endOffset - 1], "table")) {
7472 			return;
7473 		}
7474 
7475 		// "If the child of end node with index end offset is a table:"
7476 		if (isNamedHtmlElement(endNode.childNodes[endOffset], "table")) {
7477 			// "Call collapse(end node, end offset) on the context object's
7478 			// Selection."
7479 			range.setStart(endNode, endOffset);
7480 
7481 			// "Call extend(end node, end offset + 1) on the context object's
7482 			// Selection."
7483 			range.setEnd(endNode, endOffset + 1);
7484 
7485 			// "Abort these steps."
7486 			return;
7487 		}
7488 
7489 		// "If offset is the length of node, and the child of end node with
7490 		// index end offset is an hr or br:"
7491 		if (offset == getNodeLength(node)
7492 		&& isHtmlElementInArray(endNode.childNodes[endOffset], ["br", "hr"])) {
7493 			// "Call collapse(node, offset) on the Selection."
7494 			range.setStart(node, offset);
7495 			range.setEnd(node, offset);
7496 
7497 			// "Delete the contents of the range with end (end node, end
7498 			// offset) and end (end node, end offset + 1)."
7499 			deleteContents(endNode, endOffset, endNode, endOffset + 1);
7500 
7501 			// "Abort these steps."
7502 			return;
7503 		}
7504 
7505 		// "While end node has a child with index end offset:"
7506 		while (endOffset < endNode.childNodes.length) {
7507 			// "If end node's child with index end offset is editable and
7508 			// invisible, remove it from end node."
7509 			if (isEditable(endNode.childNodes[endOffset])
7510 			&& isInvisible(endNode.childNodes[endOffset])) {
7511 				endNode.removeChild(endNode.childNodes[endOffset]);
7512 
7513 			// "Otherwise, set end node to its child with index end offset and
7514 			// set end offset to zero."
7515 			} else {
7516 				endNode = endNode.childNodes[endOffset];
7517 				endOffset = 0;
7518 			}
7519 		}
7520 
7521 		// "Delete the contents of the range with start (node, offset) and end
7522 		// (end node, end offset)."
7523 		deleteContents(node, offset, endNode, endOffset);
7524 	}
7525 };
7526 
7527 //@}
7528 ///// The indent command /////
7529 //@{
7530 commands.indent = {
7531 	action: function() {
7532 		// "Let items be a list of all lis that are ancestor containers of the
7533 		// active range's start and/or end node."
7534 		//
7535 		// Has to be in tree order, remember!
7536 		var items = [];
7537 		for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7538 			if (isNamedHtmlElement(node, "LI")) {
7539 				items.unshift(node);
7540 			}
7541 		}
7542 		for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
7543 			if (isNamedHtmlElement(node, "LI")) {
7544 				items.unshift(node);
7545 			}
7546 		}
7547 		for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
7548 			if (isNamedHtmlElement(node, "LI")) {
7549 				items.unshift(node);
7550 			}
7551 		}
7552 
7553 		// "For each item in items, normalize sublists of item."
7554 		for (var i = 0; i < items.length; i++) {
7555 			normalizeSublists(items[i, range]);
7556 		}
7557 
7558 		// "Block-extend the active range, and let new range be the result."
7559 		var newRange = blockExtend(getActiveRange());
7560 
7561 		// "Let node list be a list of nodes, initially empty."
7562 		var nodeList = [];
7563 
7564 		// "For each node node contained in new range, if node is editable and
7565 		// is an allowed child of "div" or "ol" and if the last member of node
7566 		// list (if any) is not an ancestor of node, append node to node list."
7567 		nodeList = getContainedNodes(newRange, function(node) {
7568 			return isEditable(node)
7569 				&& (isAllowedChild(node, "div")
7570 				|| isAllowedChild(node, "ol"));
7571 		});
7572 
7573 		// "If the first member of node list is an li whose parent is an ol or
7574 		// ul, and its previousSibling is an li as well, normalize sublists of
7575 		// its previousSibling."
7576 		if (nodeList.length
7577 		&& isNamedHtmlElement(nodeList[0], "LI")
7578 		&& isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"])
7579 		&& isNamedHtmlElement(nodeList[0].previousSibling, "LI")) {
7580 			normalizeSublists(nodeList[0].previousSibling, range);
7581 		}
7582 
7583 		// "While node list is not empty:"
7584 		while (nodeList.length) {
7585 			// "Let sublist be a list of nodes, initially empty."
7586 			var sublist = [];
7587 
7588 			// "Remove the first member of node list and append it to sublist."
7589 			sublist.push(nodeList.shift());
7590 
7591 			// "While the first member of node list is the nextSibling of the
7592 			// last member of sublist, remove the first member of node list and
7593 			// append it to sublist."
7594 			while (nodeList.length
7595 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling) {
7596 				sublist.push(nodeList.shift());
7597 			}
7598 
7599 			// "Indent sublist."
7600 			indentNodes(sublist, range);
7601 		}
7602 	}
7603 };
7604 
7605 //@}
7606 ///// The insertHorizontalRule command /////
7607 //@{
7608 commands.inserthorizontalrule = {
7609 	action: function(value, range) {
7610 		
7611 		// "While range's start offset is 0 and its start node's parent is not
7612 		// null, set range's start to (parent of start node, index of start
7613 		// node)."
7614 		while (range.startOffset == 0
7615 		&& range.startContainer.parentNode) {
7616 			range.setStart(range.startContainer.parentNode, getNodeIndex(range.startContainer));
7617 		}
7618 
7619 		// "While range's end offset is the length of its end node, and its end
7620 		// node's parent is not null, set range's end to (parent of end node, 1
7621 		// + index of start node)."
7622 		while (range.endOffset == getNodeLength(range.endContainer)
7623 		&& range.endContainer.parentNode) {
7624 			range.setEnd(range.endContainer.parentNode, 1 + getNodeIndex(range.endContainer));
7625 		}
7626 
7627 		// "Delete the contents of range, with block merging false."
7628 		deleteContents(range, {blockMerging: false});
7629 
7630 		// "If the active range's start node is neither editable nor an editing
7631 		// host, abort these steps."
7632 		if (!isEditable(getActiveRange().startContainer)
7633 		&& !isEditingHost(getActiveRange().startContainer)) {
7634 			return;
7635 		}
7636 
7637 		// "If the active range's start node is a Text node and its start
7638 		// offset is zero, set the active range's start and end to (parent of
7639 		// start node, index of start node)."
7640 		if (getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE
7641 		&& getActiveRange().startOffset == 0) {
7642 			getActiveRange().setStart(getActiveRange().startContainer.parentNode, getNodeIndex(getActiveRange().startContainer));
7643 			getActiveRange().collapse(true);
7644 		}
7645 
7646 		// "If the active range's start node is a Text node and its start
7647 		// offset is the length of its start node, set the active range's start
7648 		// and end to (parent of start node, 1 + index of start node)."
7649 		if (getActiveRange().startContainer.nodeType == $_.Node.TEXT_NODE
7650 		&& getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) {
7651 			getActiveRange().setStart(getActiveRange().startContainer.parentNode, 1 + getNodeIndex(getActiveRange().startContainer));
7652 			getActiveRange().collapse(true);
7653 		}
7654 
7655 		// "Let hr be the result of calling createElement("hr") on the
7656 		// context object."
7657 		var hr = document.createElement("hr");
7658 
7659 		// "Run insertNode(hr) on the range."
7660 		range.insertNode(hr);
7661 
7662 		// "Fix disallowed ancestors of hr."
7663 		fixDisallowedAncestors(hr, range);
7664 
7665 		// "Run collapse() on the Selection, with first argument equal to the
7666 		// parent of hr and the second argument equal to one plus the index of
7667 		// hr."
7668 		//
7669 		// Not everyone actually supports collapse(), so we do it manually
7670 		// instead.  Also, we need to modify the actual range we're given as
7671 		// well, for the sake of autoimplementation.html's range-filling-in.
7672 		range.setStart(hr.parentNode, 1 + getNodeIndex(hr));
7673 		range.setEnd(hr.parentNode, 1 + getNodeIndex(hr));
7674 		Aloha.getSelection().removeAllRanges();
7675 		Aloha.getSelection().addRange(range);
7676 	}
7677 };
7678 
7679 //@}
7680 ///// The insertHTML command /////
7681 //@{
7682 commands.inserthtml = {
7683 	action: function(value, range) {
7684 		
7685 		
7686 		// "Delete the contents of the active range."
7687 		deleteContents(range);
7688 
7689 		// "If the active range's start node is neither editable nor an editing
7690 		// host, abort these steps."
7691 		if (!isEditable(range.startContainer)
7692 		&& !isEditingHost(range.startContainer)) {
7693 			return;
7694 		}
7695 
7696 		// "Let frag be the result of calling createContextualFragment(value)
7697 		// on the active range."
7698 		var frag = range.createContextualFragment(value);
7699 
7700 		// "Let last child be the lastChild of frag."
7701 		var lastChild = frag.lastChild;
7702 
7703 		// "If last child is null, abort these steps."
7704 		if (!lastChild) {
7705 			return;
7706 		}
7707 
7708 		// "Let descendants be all descendants of frag."
7709 		var descendants = getDescendants(frag);
7710 
7711 		// "If the active range's start node is a block node:"
7712 		if (isBlockNode(range.startContainer)) {
7713 			// "Let collapsed block props be all editable collapsed block prop
7714 			// children of the active range's start node that have index
7715 			// greater than or equal to the active range's start offset."
7716 			//
7717 			// "For each node in collapsed block props, remove node from its
7718 			// parent."
7719 			$_(range.startContainer.childNodes).filter(function(node, range) {
7720 				return isEditable(node)
7721 					&& isCollapsedBlockProp(node)
7722 					&& getNodeIndex(node) >= range.startOffset;
7723 			}, true).forEach(function(node) {
7724 				node.parentNode.removeChild(node);
7725 			});
7726 		}
7727 
7728 		// "Call insertNode(frag) on the active range."
7729 		range.insertNode(frag);
7730 
7731 		// "If the active range's start node is a block node with no visible
7732 		// children, call createElement("br") on the context object and append
7733 		// the result as the last child of the active range's start node."
7734 		if (isBlockNode(range.startContainer)) {
7735 			ensureContainerEditable(range.startContainer);
7736 		}
7737 
7738 		// "Call collapse() on the context object's Selection, with last
7739 		// child's parent as the first argument and one plus its index as the
7740 		// second."
7741 		range.setStart(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7742 		range.setEnd(lastChild.parentNode, 1 + getNodeIndex(lastChild));
7743 
7744 		// "Fix disallowed ancestors of each member of descendants."
7745 		for (var i = 0; i < descendants.length; i++) {
7746 			fixDisallowedAncestors(descendants[i], range);
7747 		}
7748 		
7749 		setActiveRange( range );
7750 	}
7751 };
7752 
7753 //@}
7754 ///// The insertImage command /////
7755 //@{
7756 commands.insertimage = {
7757 	action: function(value) {
7758 		// "If value is the empty string, abort these steps and do nothing."
7759 		if (value === "") {
7760 			return;
7761 		}
7762 
7763 		// "Let range be the active range."
7764 		var range = getActiveRange();
7765 
7766 		// "Delete the contents of range, with strip wrappers false."
7767 		deleteContents(range, {stripWrappers: false});
7768 
7769 		// "If the active range's start node is neither editable nor an editing
7770 		// host, abort these steps."
7771 		if (!isEditable(getActiveRange().startContainer)
7772 		&& !isEditingHost(getActiveRange().startContainer)) {
7773 			return;
7774 		}
7775 
7776 		// "If range's start node is a block node whose sole child is a br, and
7777 		// its start offset is 0, remove its start node's child from it."
7778 		if (isBlockNode(range.startContainer)
7779 		&& range.startContainer.childNodes.length == 1
7780 		&& isNamedHtmlElement(range.startContainer.firstChild, "br")
7781 		&& range.startOffset == 0) {
7782 			range.startContainer.removeChild(range.startContainer.firstChild);
7783 		}
7784 
7785 		// "Let img be the result of calling createElement("img") on the
7786 		// context object."
7787 		var img = document.createElement("img");
7788 
7789 		// "Run setAttribute("src", value) on img."
7790 		img.setAttribute("src", value);
7791 
7792 		// "Run insertNode(img) on the range."
7793 		range.insertNode(img);
7794 
7795 		// "Run collapse() on the Selection, with first argument equal to the
7796 		// parent of img and the second argument equal to one plus the index of
7797 		// img."
7798 		//
7799 		// Not everyone actually supports collapse(), so we do it manually
7800 		// instead.  Also, we need to modify the actual range we're given as
7801 		// well, for the sake of autoimplementation.html's range-filling-in.
7802 		range.setStart(img.parentNode, 1 + getNodeIndex(img));
7803 		range.setEnd(img.parentNode, 1 + getNodeIndex(img));
7804 		Aloha.getSelection().removeAllRanges();
7805 		Aloha.getSelection().addRange(range);
7806 
7807 		// IE adds width and height attributes for some reason, so remove those
7808 		// to actually do what the spec says.
7809 		img.removeAttribute("width");
7810 		img.removeAttribute("height");
7811 	}
7812 };
7813 
7814 //@}
7815 ///// The insertLineBreak command /////
7816 //@{
7817 commands.insertlinebreak = {
7818 	action: function(value, range) {
7819 		// "Delete the contents of the active range, with strip wrappers false."
7820 		deleteContents(range, {stripWrappers: false});
7821 
7822 		// "If the active range's start node is neither editable nor an editing
7823 		// host, abort these steps."
7824 		if (!isEditable(range.startContainer)
7825 		&& !isEditingHost(range.startContainer)) {
7826 			return;
7827 		}
7828 
7829 		// "If the active range's start node is an Element, and "br" is not an
7830 		// allowed child of it, abort these steps."
7831 		if (range.startContainer.nodeType == $_.Node.ELEMENT_NODE
7832 		&& !isAllowedChild("br", range.startContainer)) {
7833 			return;
7834 7835 		}
7836 
7837 		// "If the active range's start node is not an Element, and "br" is not
7838 		// an allowed child of the active range's start node's parent, abort
7839 		// these steps."
7840 		if (range.startContainer.nodeType != $_.Node.ELEMENT_NODE
7841 		&& !isAllowedChild("br", range.startContainer.parentNode)) {
7842 			return;
7843 		}
7844 
7845 		// "If the active range's start node is a Text node and its start
7846 		// offset is zero, call collapse() on the context object's Selection,
7847 		// with first argument equal to the active range's start node's parent
7848 		// and second argument equal to the active range's start node's index."
7849 		if (range.startContainer.nodeType == $_.Node.TEXT_NODE
7850 		&& range.startOffset == 0) {
7851 			var newNode = range.startContainer.parentNode;
7852 			var newOffset = getNodeIndex(range.startContainer);
7853 			Aloha.getSelection().collapse(newNode, newOffset);
7854 			range.setStart(newNode, newOffset);
7855 			range.setEnd(newNode, newOffset);
7856 		}
7857 
7858 		// "If the active range's start node is a Text node and its start
7859 		// offset is the length of its start node, call collapse() on the
7860 		// context object's Selection, with first argument equal to the active
7861 		// range's start node's parent and second argument equal to one plus
7862 		// the active range's start node's index."
7863 		if (range.startContainer.nodeType == $_.Node.TEXT_NODE
7864 		&& range.startOffset == getNodeLength(range.startContainer)) {
7865 			var newNode = range.startContainer.parentNode;
7866 			var newOffset = 1 + getNodeIndex(range.startContainer);
7867 			Aloha.getSelection().collapse(newNode, newOffset);
7868 			range.setStart(newNode, newOffset);
7869 			range.setEnd(newNode, newOffset);
7870 		}
7871 
7872 		// "Let br be the result of calling createElement("br") on the context
7873 		// object."
7874 		var br = document.createElement("br");
7875 
7876 		// "Call insertNode(br) on the active range."
7877 		range.insertNode(br);
7878 
7879 		// "Call collapse() on the context object's Selection, with br's parent
7880 		// as the first argument and one plus br's index as the second
7881 		// argument."
7882 		Aloha.getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7883 		range.setStart(br.parentNode, 1 + getNodeIndex(br));
7884 		range.setEnd(br.parentNode, 1 + getNodeIndex(br));
7885 
7886 		// "If br is a collapsed line break, call createElement("br") on the
7887 		// context object and let extra br be the result, then call
7888 		// insertNode(extra br) on the active range."
7889 		if (isCollapsedLineBreak(br)) {
7890 			// TODO
7891 			range.insertNode(createEndBreak());
7892 
7893 			// Compensate for nonstandard implementations of insertNode
7894 			Aloha.getSelection().collapse(br.parentNode, 1 + getNodeIndex(br));
7895 			range.setStart(br.parentNode, 1 + getNodeIndex(br));
7896 			range.setEnd(br.parentNode, 1 + getNodeIndex(br));
7897 		}
7898 		
7899 		// IE7 is adding this styles: height: auto; min-height: 0px; max-height: none;
7900 		// with that there is the ugly "IE-editable-outline"
7901 		if (jQuery.browser.msie && jQuery.browser.version < 8) {
7902 			br.parentNode.removeAttribute("style");
7903 		}
7904 	}
7905 };
7906 
7907 //@}
7908 ///// The insertOrderedList command /////
7909 //@{
7910 commands.insertorderedlist = {
7911 	// "Toggle lists with tag name "ol"."
7912 	action: function() { toggleLists("ol") },
7913 	// "True if the selection's list state is "mixed" or "mixed ol", false
7914 	// otherwise."
7915 	indeterm: function() { return /^mixed( ol)?$/.test(getSelectionListState()) },
7916 	// "True if the selection's list state is "ol", false otherwise."
7917 	state: function() { return getSelectionListState() == "ol" }
7918 };
7919 
7920 var listRelatedElements = {"LI": true, "DT": true, "DD": true};
7921 
7922 //@}
7923 ///// The insertParagraph command /////
7924 //@{
7925 commands.insertparagraph = {
7926 	action: function(value, range) {
7927 
7928 		// "Delete the contents of the active range."
7929 		deleteContents(range);
7930 
7931 		// clean lists in the editing host, this will remove any whitespace nodes around lists
7932 		// because the following algorithm is not prepared to deal with them
7933 		cleanLists(getEditingHostOf(range.startContainer), range);
7934 
7935 		// "If the active range's start node is neither editable nor an editing
7936 		// host, abort these steps."
7937 		if (!isEditable(range.startContainer)
7938 		&& !isEditingHost(range.startContainer)) {
7939 			return;
7940 		}
7941 
7942 		// "Let node and offset be the active range's start node and offset."
7943 		var node = range.startContainer;
7944 		var offset = range.startOffset;
7945 
7946 		// "If node is a Text node, and offset is neither 0 nor the length of
7947 		// node, call splitText(offset) on node."
7948 		if (node.nodeType == $_.Node.TEXT_NODE
7949 		&& offset != 0
7950 		&& offset != getNodeLength(node)) {
7951 			node.splitText(offset);
7952 		}
7953 
7954 		// "If node is a Text node and offset is its length, set offset to one
7955 		// plus the index of node, then set node to its parent."
7956 		if (node.nodeType == $_.Node.TEXT_NODE
7957 		&& offset == getNodeLength(node)) {
7958 			offset = 1 + getNodeIndex(node);
7959 			node = node.parentNode;
7960 		}
7961 
7962 		// "If node is a Text or Comment node, set offset to the index of node,
7963 		// then set node to its parent."
7964 		if (node.nodeType == $_.Node.TEXT_NODE
7965 		|| node.nodeType == $_.Node.COMMENT_NODE) {
7966 			offset = getNodeIndex(node);
7967 			node = node.parentNode;
7968 		}
7969 
7970 		// "Call collapse(node, offset) on the context object's Selection."
7971 		Aloha.getSelection().collapse(node, offset);
7972 		range.setStart(node, offset);
7973 		range.setEnd(node, offset);
7974 
7975 		// "Let container equal node."
7976 		var container = node;
7977 
7978 		// "While container is not a single-line container, and container's
7979 		// parent is editable and in the same editing host as node, set
7980 		// container to its parent."
7981 		while (!isSingleLineContainer(container)
7982 		&& isEditable(container.parentNode)
7983 		&& inSameEditingHost(node, container.parentNode)) {
7984 			container = container.parentNode;
7985 		}
7986 
7987 		// "If container is not editable or not in the same editing host as
7988 		// node or is not a single-line container:"
7989 		if (!isEditable(container)
7990 		|| !inSameEditingHost(container, node)
7991 		|| !isSingleLineContainer(container)) {
7992 			// "Let tag be the default single-line container name."
7993 			var tag = defaultSingleLineContainerName;
7994 
7995 			// "Block-extend the active range, and let new range be the
7996 			// result."
7997 			var newRange = blockExtend(range);
7998 
7999 			// "Let node list be a list of nodes, initially empty."
8000 			//
8001 			// "Append to node list the first node in tree order that is
8002 			// contained in new range and is an allowed child of "p", if any."
8003 			var nodeList = getContainedNodes(newRange, function(node) { return isAllowedChild(node, "p") })
8004 				.slice(0, 1);
8005 
8006 			// "If node list is empty:"
8007 			if (!nodeList.length) {
8008 				// "If tag is not an allowed child of the active range's start
8009 				// node, abort these steps."
8010 				if (!isAllowedChild(tag, range.startContainer)) {
8011 					return;
8012 				}
8013 
8014 				// "Set container to the result of calling createElement(tag)
8015 				// on the context object."
8016 				container = document.createElement(tag);
8017 
8018 				// "Call insertNode(container) on the active range."
8019 				range.insertNode(container);
8020 
8021 				// "Call createElement("br") on the context object, and append
8022 				// the result as the last child of container."
8023 				// TODO not always
8024 				container.appendChild(createEndBreak());
8025 
8026 				// "Call collapse(container, 0) on the context object's
8027 				// Selection."
8028 				// TODO: remove selection from command
8029 				Aloha.getSelection().collapse(container, 0); 
8030 				range.setStart(container, 0);
8031 				range.setEnd(container, 0);
8032 
8033 				// "Abort these steps."
8034 				return;
8035 			}
8036 
8037 			// "While the nextSibling of the last member of node list is not
8038 			// null and is an allowed child of "p", append it to node list."
8039 			while (nodeList[nodeList.length - 1].nextSibling
8040 			&& isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) {
8041 				nodeList.push(nodeList[nodeList.length - 1].nextSibling);
8042 			}
8043 
8044 			// "Wrap node list, with sibling criteria returning false and new
8045 			// parent instructions returning the result of calling
8046 			// createElement(tag) on the context object. Set container to the
8047 8048 			// result."
8049 			container = wrap(nodeList,
8050 				function() { return false },
8051 				function() { return document.createElement(tag) },
8052 				range
8053 			);
8054 		}
8055 
8056 		// "If container's local name is "address", "listing", or "pre":"
8057 		if (container.tagName == "ADDRESS"
8058 		|| container.tagName == "LISTING"
8059 		|| container.tagName == "PRE") {
8060 			// "Let br be the result of calling createElement("br") on the
8061 			// context object."
8062 			var br = document.createElement("br");
8063 
8064 			// remember the old height
8065 			var oldHeight = container.offsetHeight;
8066 
8067 			// "Call insertNode(br) on the active range."
8068 			range.insertNode(br);
8069 
8070 			// determine the new height
8071 			var newHeight = container.offsetHeight;
8072 
8073 			// "Call collapse(node, offset + 1) on the context object's
8074 			// Selection."
8075 			Aloha.getSelection().collapse(node, offset + 1);
8076 			range.setStart(node, offset + 1);
8077 			range.setEnd(node, offset + 1);
8078 
8079 			// "If br is the last descendant of container, let br be the result
8080 			// of calling createElement("br") on the context object, then call
8081 			// insertNode(br) on the active range." (Fix: only do this, if the container height did not change by inserting a single <br/>)
8082 			//
8083 			// Work around browser bugs: some browsers select the
8084 			// newly-inserted node, not per spec.
8085 			if (oldHeight == newHeight && !isDescendant(nextNode(br), container)) {
8086 				// TODO check
8087 				range.insertNode(createEndBreak());
8088 				Aloha.getSelection().collapse(node, offset + 1);
8089 				range.setEnd(node, offset + 1);
8090 			}
8091 8092 
			// "Abort these steps."
8093 			return;
8094 		}
8095 
8096 		// "If container's local name is "li", "dt", or "dd"; and either it has
8097 		// no children or it has a single child and that child is a br:"
8098 		if (listRelatedElements[container.tagName]
8099 		&& (!container.hasChildNodes()
8100 		|| (container.childNodes.length == 1
8101 		&& isNamedHtmlElement(container.firstChild, "br")))) {
8102 			// "Split the parent of the one-node list consisting of container."
8103 			splitParent([container], range);
8104 
8105 			// "If container has no children, call createElement("br") on the
8106 			// context object and append the result as the last child of
8107 			// container."
8108 			// only do this, if inserting the br does NOT modify the offset height of the container
8109 //			if (!container.hasChildNodes()) {
8110 //				var oldHeight = container.offsetHeight, endBr = createEndBreak();
8111 //				container.appendChild(endBr);
8112 //				if (container.offsetHeight !== oldHeight) {
8113 //					container.removeChild(endBr);
8114 //				}
8115 //			}
8116 8117 
			// "If container is a dd or dt, and it is not an allowed child of
8118 			// any of its ancestors in the same editing host, set the tag name
8119 			// of container to the default single-line container name and let
8120 			// container be the result."
8121 			if (isHtmlElementInArray(container, ["dd", "dt"])
8122 			&& $_( getAncestors(container) ).every(function(ancestor) {
8123 				return !inSameEditingHost(container, ancestor)
8124 					|| !isAllowedChild(container, ancestor)
8125 			})) {
8126 				container = setTagName(container, defaultSingleLineContainerName, range);
8127 			}
8128 
8129 			// "Fix disallowed ancestors of container."
8130 			fixDisallowedAncestors(container, range);
8131 
8132 			// fix invalid nested lists
8133 			if (isNamedHtmlElement(container, 'li')
8134 			&& isNamedHtmlElement(container.nextSibling, "li")
8135 			&& isHtmlElementInArray(container.nextSibling.firstChild, ["ol", "ul"])) {
8136 				// we found a li containing only a br followed by a li containing a list as first element: merge the two li's
8137 				var listParent = container.nextSibling, length = container.nextSibling.childNodes.length;
8138 				for (var i = 0; i < length; i++) {
8139 					// we always move the first child into the container
8140 					container.appendChild(listParent.childNodes[0]);
8141 				}
8142 				listParent.parentNode.removeChild(listParent);
8143 			}
8144 
8145 			// "Abort these steps."
8146 			return;
8147 		}
8148 
8149 		// "Let new line range be a new range whose start is the same as
8150 		// the active range's, and whose end is (container, length of
8151 		// container)."
8152 		var newLineRange = Aloha.createRange();
8153 		newLineRange.setStart(range.startContainer, range.startOffset);
8154 		newLineRange.setEnd(container, getNodeLength(container));
8155 
8156 		// "While new line range's start offset is zero and its start node is
8157 		// not container, set its start to (parent of start node, index of
8158 		// start node)."
8159 		while (newLineRange.startOffset == 0
8160 		&& newLineRange.startContainer != container) {
8161 			newLineRange.setStart(newLineRange.startContainer.parentNode, getNodeIndex(newLineRange.startContainer));
8162 		}
8163 
8164 		// "While new line range's start offset is the length of its start node
8165 		// and its start node is not container, set its start to (parent of
8166 		// start node, 1 + index of start node)."
8167 		while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer)
8168 		&& newLineRange.startContainer != container) {
8169 			newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getNodeIndex(newLineRange.startContainer));
8170 		}
8171 
8172 		// "Let end of line be true if new line range contains either nothing
8173 		// or a single br, and false otherwise."
8174 		var containedInNewLineRange = getContainedNodes(newLineRange);
8175 		var endOfLine = !containedInNewLineRange.length
8176 			|| (containedInNewLineRange.length == 1
8177 			&& isNamedHtmlElement(containedInNewLineRange[0], "br"));
8178 
8179 		// "If the local name of container is "h1", "h2", "h3", "h4", "h5", or
8180 		// "h6", and end of line is true, let new container name be the default
8181 		// single-line container name."
8182 		var newContainerName;
8183 		if (/^H[1-6]$/.test(container.tagName)
8184 		&& endOfLine) {
8185 			newContainerName = defaultSingleLineContainerName;
8186 
8187 		// "Otherwise, if the local name of container is "dt" and end of line
8188 		// is true, let new container name be "dd"."
8189 		} else if (container.tagName == "DT"
8190 		&& endOfLine) {
8191 			newContainerName = "dd";
8192 
8193 		// "Otherwise, if the local name of container is "dd" and end of line
8194 		// is true, let new container name be "dt"."
8195 		} else if (container.tagName == "DD"
8196 		&& endOfLine) {
8197 			newContainerName = "dt";
8198 
8199 		// "Otherwise, let new container name be the local name of container."
8200 		} else {
8201 			newContainerName = container.tagName.toLowerCase();
8202 		}
8203 
8204 		// "Let new container be the result of calling createElement(new
8205 		// container name) on the context object."
8206 		var newContainer = document.createElement(newContainerName);
8207 
8208 		// "Copy all non empty attributes of the container to new container."
8209 		copyAttributes( container,  newContainer );
8210 
8211 		// "If new container has an id attribute, unset it."
8212 		newContainer.removeAttribute("id");
8213 
8214 		// "Insert new container into the parent of container immediately after
8215 		// container."
8216 		container.parentNode.insertBefore(newContainer, container.nextSibling);
8217 
8218 		// "Let contained nodes be all nodes contained in new line range."
8219 		var containedNodes = getAllContainedNodes(newLineRange);
8220 
8221 		// "Let frag be the result of calling extractContents() on new line
8222 		// range."
8223 		var frag = newLineRange.extractContents();
8224 
8225 		// "Unset the id attribute (if any) of each Element descendant of frag
8226 		// that is not in contained nodes."
8227 		var descendants = getDescendants(frag);
8228 		for (var i = 0; i < descendants.length; i++) {
8229 			if (descendants[i].nodeType == $_.Node.ELEMENT_NODE
8230 			&& $_(containedNodes).indexOf(descendants[i]) == -1) {
8231 				descendants[i].removeAttribute("id");
8232 			}
8233 		}
8234 
8235 		var fragChildren = [], fragChild = frag.firstChild;
8236 		if (fragChild) {
8237 			do {
8238 				if (!isWhitespaceNode(fragChild)) {
8239 					fragChildren.push(fragChild);
8240 				}
8241 			} while(fragChild = fragChild.nextSibling);
8242 		}
8243 
8244 		// 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)
8245 		if (isNamedHtmlElement(newContainer, 'li') && fragChildren.length && isHtmlElementInArray(fragChildren[0], ["ul", "ol"])) {
8246 			var oldHeight = newContainer.offsetHeight;
8247 			var endBr = createEndBreak();
8248 			newContainer.appendChild(endBr);
8249 			var newHeight = newContainer.offsetHeight;
8250 			if (oldHeight !== newHeight) {
8251 				newContainer.removeChild(endBr);
8252 			}
8253 		}
8254 
8255 		// "Call appendChild(frag) on new container."
8256 		newContainer.appendChild(frag);
8257 
8258 		// "If container has no visible children, call createElement("br") on
8259 		// the context object, and append the result as the last child of
8260 		// container."
8261 		ensureContainerEditable(container);
8262 
8263 		// "If new container has no visible children, call createElement("br")
8264 		// on the context object, and append the result as the last child of
8265 		// new container."
8266 		ensureContainerEditable(newContainer);
8267 
8268 		// "Call collapse(new container, 0) on the context object's Selection."
8269 		Aloha.getSelection().collapse(newContainer, 0);
8270 		range.setStart(newContainer, 0);
8271 		range.setEnd(newContainer, 0);
8272 	}
8273 };
8274 
8275 //@}
8276 ///// The insertText command /////
8277 //@{
8278 commands.inserttext = {
8279 	action: function(value, range) {
8280 		// "Delete the contents of the active range, with strip wrappers
8281 		// false."
8282 		deleteContents(range, {stripWrappers: false});
8283 
8284 		// "If the active range's start node is neither editable nor an editing
8285 		// host, abort these steps."
8286 		if (!isEditable(range.startContainer)
8287 		&& !isEditingHost(range.startContainer)) {
8288 			return;
8289 		}
8290 
8291 		// "If value's length is greater than one:"
8292 		if (value.length > 1) {
8293 			// "For each element el in value, take the action for the
8294 			// insertText command, with value equal to el."
8295 			for (var i = 0; i < value.length; i++) {
8296 				commands.inserttext.action( value[i], range );
8297 			}
8298 
8299 			// "Abort these steps."
8300 			return;
8301 		}
8302 
8303 		// "If value is the empty string, abort these steps."
8304 		if (value == "") {
8305 			return;
8306 		}
8307 
8308 		// "If value is a newline (U+00A0), take the action for the
8309 		// insertParagraph command and abort these steps."
8310 		if (value == "\n") {
8311 			commands.insertparagraph.action( '', range );
8312 			return;
8313 		}
8314 
8315 		// "Let node and offset be the active range's start node and offset."
8316 		var node = range.startContainer;
8317 		var offset = range.startOffset;
8318 
8319 		// "If node has a child whose index is offset − 1, and that child is a
8320 		// Text node, set node to that child, then set offset to node's
8321 		// length."
8322 		if (0 <= offset - 1
8323 		&& offset - 1 < node.childNodes.length
8324 		&& node.childNodes[offset - 1].nodeType == $_.Node.TEXT_NODE) {
8325 			node = node.childNodes[offset - 1];
8326 			offset = getNodeLength(node);
8327 		}
8328 
8329 		// "If node has a child whose index is offset, and that child is a Text
8330 		// node, set node to that child, then set offset to zero."
8331 		if (0 <= offset
8332 		&& offset < node.childNodes.length
8333 		&& node.childNodes[offset].nodeType == $_.Node.TEXT_NODE) {
8334 			node = node.childNodes[offset];
8335 			offset = 0;
8336 		}
8337 
8338 		// "If value is a space (U+0020), and either node is an Element whose
8339 		// resolved value for "white-space" is neither "pre" nor "pre-wrap" or
8340 		// node is not an Element but its parent is an Element whose resolved
8341 		// value for "white-space" is neither "pre" nor "pre-wrap", set value
8342 		// to a non-breaking space (U+00A0)."
8343 		var refElement = node.nodeType == $_.Node.ELEMENT_NODE ? node : node.parentNode;
8344 		if (value == " "
8345 		&& refElement.nodeType == $_.Node.ELEMENT_NODE
8346 		&& jQuery.inArray($_.getComputedStyle(refElement).whiteSpace, ["pre", "pre-wrap"]) == -1) {
8347 			value = "\xa0";
8348 		}
8349 
8350 		// "Record current overrides, and let overrides be the result."
8351 		var overrides = recordCurrentOverrides( range );
8352 
8353 		// "If node is a Text node:"
8354 		if (node.nodeType == $_.Node.TEXT_NODE) {
8355 			// "Call insertData(offset, value) on node."
8356 			node.insertData(offset, value);
8357 
8358 			// "Call collapse(node, offset) on the context object's Selection."
8359 			Aloha.getSelection().collapse(node, offset);
8360 			range.setStart(node, offset);
8361 
8362 			// "Call extend(node, offset + 1) on the context object's
8363 			// Selection."
8364 			Aloha.getSelection().extend(node, offset + 1);
8365 			range.setEnd(node, offset + 1);
8366 
8367 		// "Otherwise:"
8368 		} else {
8369 			// "If node has only one child, which is a collapsed line break,
8370 			// remove its child from it."
8371 			//
8372 			// FIXME: IE incorrectly returns false here instead of true
8373 			// sometimes?
8374 			if (node.childNodes.length == 1
8375 			&& isCollapsedLineBreak(node.firstChild)) {
8376 				node.removeChild(node.firstChild);
8377 			}
8378 
8379 			// "Let text be the result of calling createTextNode(value) on the
8380 			// context object."
8381 			var text = document.createTextNode(value);
8382 
8383 			// "Call insertNode(text) on the active range."
8384 			range.insertNode(text);
8385 
8386 			// "Call collapse(text, 0) on the context object's Selection."
8387 			Aloha.getSelection().collapse(text, 0);
8388 			range.setStart(text, 0);
8389 
8390 			// "Call extend(text, 1) on the context object's Selection."
8391 			Aloha.getSelection().extend(text, 1);
8392 			range.setEnd(text, 1);
8393 		}
8394 
8395 		// "Restore states and values from overrides."
8396 		restoreStatesAndValues(overrides);
8397 
8398 		// "Canonicalize whitespace at the active range's start."
8399 		canonicalizeWhitespace(range.startContainer, range.startOffset);
8400 
8401 		// "Canonicalize whitespace at the active range's end."
8402 		canonicalizeWhitespace(range.endContainer, range.endOffset);
8403 
8404 		// "Call collapseToEnd() on the context object's Selection."
8405 		Aloha.getSelection().collapseToEnd();
8406 		range.collapse(false);
8407 	}
8408 };
8409 
8410 //@}
8411 ///// The insertUnorderedList command /////
8412 //@{
8413 commands.insertunorderedlist = {
8414 	// "Toggle lists with tag name "ul"."
8415 	action: function() { toggleLists("ul") },
8416 	// "True if the selection's list state is "mixed" or "mixed ul", false
8417 	// otherwise."
8418 	indeterm: function() { return /^mixed( ul)?$/.test(getSelectionListState()) },
8419 	// "True if the selection's list state is "ul", false otherwise."
8420 	state: function() { return getSelectionListState() == "ul" }
8421 };
8422 
8423 //@}
8424 ///// The justifyCenter command /////
8425 //@{
8426 commands.justifycenter = {
8427 	// "Justify the selection with alignment "center"."
8428 	action: function(value, range) { justifySelection("center", range) },
8429 	indeterm: function() {
8430 		// "Block-extend the active range. Return true if among visible
8431 		// editable nodes that are contained in the result and have no
8432 		// children, at least one has alignment value "center" and at least one
8433 		// does not. Otherwise return false."
8434 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8435 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8436 		});
8437 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "center" })
8438 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "center" });
8439 	}, state: function() {
8440 		// "Block-extend the active range. Return true if there is at least one
8441 		// visible editable node that is contained in the result and has no
8442 		// children, and all such nodes have alignment value "center".
8443 		// Otherwise return false."
8444 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8445 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8446 		});
8447 		return nodes.length
8448 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "center" });
8449 	}, value: function() {
8450 		// "Block-extend the active range, and return the alignment value of
8451 		// the first visible editable node that is contained in the result and
8452 		// has no children. If there is no such node, return "left"."
8453 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8454 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8455 		});
8456 		if (nodes.length) {
8457 			return getAlignmentValue(nodes[0]);
8458 		} else {
8459 			return "left";
8460 		}
8461 	}
8462 };
8463 
8464 //@}
8465 ///// The justifyFull command /////
8466 //@{
8467 commands.justifyfull = {
8468 	// "Justify the selection with alignment "justify"."
8469 	action: function(value, range) { justifySelection("justify", range) },
8470 	indeterm: function() {
8471 		// "Block-extend the active range. Return true if among visible
8472 		// editable nodes that are contained in the result and have no
8473 		// children, at least one has alignment value "justify" and at least
8474 		// one does not. Otherwise return false."
8475 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8476 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8477 		});
8478 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "justify" })
8479 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "justify" });
8480 	}, state: function() {
8481 		// "Block-extend the active range. Return true if there is at least one
8482 		// visible editable node that is contained in the result and has no
8483 		// children, and all such nodes have alignment value "justify".
8484 		// Otherwise return false."
8485 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8486 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8487 		});
8488 		return nodes.length
8489 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "justify" });
8490 	}, value: function() {
8491 		// "Block-extend the active range, and return the alignment value of
8492 		// the first visible editable node that is contained in the result and
8493 		// has no children. If there is no such node, return "left"."
8494 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8495 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8496 		});
8497 		if (nodes.length) {
8498 			return getAlignmentValue(nodes[0]);
8499 		} else {
8500 			return "left";
8501 		}
8502 	}
8503 };
8504 
8505 //@}
8506 ///// The justifyLeft command /////
8507 //@{
8508 commands.justifyleft = {
8509 	// "Justify the selection with alignment "left"."
8510 	action: function(value, range) { justifySelection("left", range) },
8511 	indeterm: function() {
8512 		// "Block-extend the active range. Return true if among visible
8513 		// editable nodes that are contained in the result and have no
8514 		// children, at least one has alignment value "left" and at least one
8515 		// does not. Otherwise return false."
8516 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8517 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8518 		});
8519 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "left" })
8520 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "left" });
8521 	}, state: function() {
8522 		// "Block-extend the active range. Return true if there is at least one
8523 		// visible editable node that is contained in the result and has no
8524 		// children, and all such nodes have alignment value "left".  Otherwise
8525 		// return false."
8526 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8527 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8528 		});
8529 		return nodes.length
8530 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "left" });
8531 	}, value: function() {
8532 		// "Block-extend the active range, and return the alignment value of
8533 		// the first visible editable node that is contained in the result and
8534 		// has no children. If there is no such node, return "left"."
8535 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8536 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8537 		});
8538 		if (nodes.length) {
8539 			return getAlignmentValue(nodes[0]);
8540 		} else {
8541 			return "left";
8542 		}
8543 	}
8544 };
8545 
8546 //@}
8547 ///// The justifyRight command /////
8548 //@{
8549 commands.justifyright = {
8550 	// "Justify the selection with alignment "right"."
8551 	action: function(value, range) { justifySelection("right", range) },
8552 	indeterm: function() {
8553 		// "Block-extend the active range. Return true if among visible
8554 		// editable nodes that are contained in the result and have no
8555 		// children, at least one has alignment value "right" and at least one
8556 		// does not. Otherwise return false."
8557 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8558 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8559 		});
8560 		return $_( nodes ).some(function(node) { return getAlignmentValue(node) == "right" })
8561 			&& $_( nodes ).some(function(node) { return getAlignmentValue(node) != "right" });
8562 	}, state: function() {
8563 		// "Block-extend the active range. Return true if there is at least one
8564 		// visible editable node that is contained in the result and has no
8565 8566 		// children, and all such nodes have alignment value "right".
8567 		// Otherwise return false."
8568 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8569 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8570 		});
8571 		return nodes.length
8572 			&& $_( nodes ).every(function(node) { return getAlignmentValue(node) == "right" });
8573 	}, value: function() {
8574 		// "Block-extend the active range, and return the alignment value of
8575 		// the first visible editable node that is contained in the result and
8576 		// has no children. If there is no such node, return "left"."
8577 		var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function(node) {
8578 			return isEditable(node) && isVisible(node) && !node.hasChildNodes();
8579 		});
8580 		if (nodes.length) {
8581 			return getAlignmentValue(nodes[0]);
8582 		} else {
8583 			return "left";
8584 		}
8585 	}
8586 };
8587 
8588 //@}
8589 ///// The outdent command /////
8590 //@{
8591 commands.outdent = {
8592 	action: function() {
8593 		// "Let items be a list of all lis that are ancestor containers of the
8594 		// range's start and/or end node."
8595 		//
8596 		// It's annoying to get this in tree order using functional stuff
8597 		// without doing getDescendants(document), which is slow, so I do it
8598 		// imperatively.
8599 		var items = [];
8600 		(function(){
8601 			for (
8602 				var ancestorContainer = getActiveRange().endContainer;
8603 				ancestorContainer != getActiveRange().commonAncestorContainer;
8604 				ancestorContainer = ancestorContainer.parentNode
8605 			) {
8606 				if (isNamedHtmlElement(ancestorContainer, "li")) {
8607 					items.unshift(ancestorContainer);
8608 				}
8609 			}
8610 			for (
8611 				var ancestorContainer = getActiveRange().startContainer;
8612 				ancestorContainer;
8613 				ancestorContainer = ancestorContainer.parentNode
8614 			) {
8615 				if (isNamedHtmlElement(ancestorContainer, "li")) {
8616 					items.unshift(ancestorContainer);
8617 				}
8618 			}
8619 		})();
8620 
8621 		// "For each item in items, normalize sublists of item."
8622 		$_( items ).forEach( function( thisArg) {
8623 			normalizeSublists( thisArg, range);
8624 		});
8625 
8626 		// "Block-extend the active range, and let new range be the result."
8627 		var newRange = blockExtend(getActiveRange());
8628 
8629 		// "Let node list be a list of nodes, initially empty."
8630 		//
8631 		// "For each node node contained in new range, append node to node list
8632 		// if the last member of node list (if any) is not an ancestor of node;
8633 		// node is editable; and either node has no editable descendants, or is
8634 		// an ol or ul, or is an li whose parent is an ol or ul."
8635 		var nodeList = getContainedNodes(newRange, function(node) {
8636 			return isEditable(node)
8637 				&& (!$_( getDescendants(node) ).some(isEditable)
8638 				|| isHtmlElementInArray(node, ["ol", "ul"])
8639 				|| (isNamedHtmlElement(node, 'li') && isHtmlElementInArray(node.parentNode, ["ol", "ul"])));
8640 		});
8641 
8642 		// "While node list is not empty:"
8643 		while (nodeList.length) {
8644 			// "While the first member of node list is an ol or ul or is not
8645 			// the child of an ol or ul, outdent it and remove it from node
8646 			// list."
8647 			while (nodeList.length
8648 			&& (isHtmlElementInArray(nodeList[0], ["OL", "UL"])
8649 			|| !isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"]))) {
8650 				outdentNode(nodeList.shift(), range);
8651 			}
8652 
8653 			// "If node list is empty, break from these substeps."
8654 			if (!nodeList.length) {
8655 				break;
8656 			}
8657 
8658 			// "Let sublist be a list of nodes, initially empty."
8659 			var sublist = [];
8660 
8661 			// "Remove the first member of node list and append it to sublist."
8662 			sublist.push(nodeList.shift());
8663 
8664 			// "While the first member of node list is the nextSibling of the
8665 			// last member of sublist, and the first member of node list is not
8666 			// an ol or ul, remove the first member of node list and append it
8667 			// to sublist."
8668 			while (nodeList.length
8669 			&& nodeList[0] == sublist[sublist.length - 1].nextSibling
8670 			&& !isHtmlElementInArray(nodeList[0], ["OL", "UL"])) {
8671 				sublist.push(nodeList.shift());
8672 			}
8673 
8674 			// "Record the values of sublist, and let values be the result."
8675 			var values = recordValues(sublist);
8676 
8677 			// "Split the parent of sublist, with new parent null."
8678 			splitParent(sublist, range);
8679 
8680 			// "Fix disallowed ancestors of each member of sublist."
8681 			$_( sublist ).forEach(fixDisallowedAncestors);
8682 
8683 			// "Restore the values from values."
8684 			restoreValues(values, range);
8685 		}
8686 	}
8687 };
8688 
8689 //@}
8690 
8691 //////////////////////////////////
8692 ///// Miscellaneous commands /////
8693 //////////////////////////////////
8694 
8695 ///// The selectAll command /////
8696 //@{
8697 commands.selectall = {
8698 	// Note, this ignores the whole globalRange/getActiveRange() thing and
8699 	// works with actual selections.  Not suitable for autoimplementation.html.
8700 	action: function() {
8701 		// "Let target be the body element of the context object."
8702 		var target = document.body;
8703 
8704 		// "If target is null, let target be the context object's
8705 		// documentElement."
8706 		if (!target) {
8707 			target = document.documentElement;
8708 		}
8709 
8710 		// "If target is null, call getSelection() on the context object, and
8711 		// call removeAllRanges() on the result."
8712 		if (!target) {
8713 			Aloha.getSelection().removeAllRanges();
8714 
8715 		// "Otherwise, call getSelection() on the context object, and call
8716 		// selectAllChildren(target) on the result."
8717 		} else {
8718 			Aloha.getSelection().selectAllChildren(target);
8719 		}
8720 	}
8721 };
8722 
8723 //@}
8724 ///// The styleWithCSS command /////
8725 //@{
8726 commands.stylewithcss = {
8727 	action: function(value) {
8728 		// "If value is an ASCII case-insensitive match for the string
8729 		// "false", set the CSS styling flag to false. Otherwise, set the
8730 		// CSS styling flag to true."
8731 		cssStylingFlag = String(value).toLowerCase() != "false";
8732 	}, state: function() { return cssStylingFlag }
8733 };
8734 
8735 //@}
8736 ///// The useCSS command /////
8737 //@{
8738 commands.usecss = {
8739 	action: function(value) {
8740 		// "If value is an ASCII case-insensitive match for the string "false",
8741 		// set the CSS styling flag to true. Otherwise, set the CSS styling
8742 		// flag to false."
8743 		cssStylingFlag = String(value).toLowerCase() == "false";
8744 	}
8745 };
8746 //@}
8747 
8748 // Some final setup
8749 //@{
8750 (function() {
8751 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit
8752 // temporary, which means I need an extra closure to not leak the temporaries
8753 // into the global namespace.  >:(
8754 var commandNames = [];
8755 for (var command in commands) {
8756 	commandNames.push(command);
8757 }
8758 $_( commandNames ).forEach(function(command) {
8759 	// "If a command does not have a relevant CSS property specified, it
8760 	// defaults to null."
8761 	if (!("relevantCssProperty" in commands[command])) {
8762 		commands[command].relevantCssProperty = null;
8763 	}
8764 
8765 	// "If a command has inline command activated values defined but
8766 	// nothing else defines when it is indeterminate, it is indeterminate
8767 	// if among editable Text nodes effectively contained in the active
8768 	// range, there is at least one whose effective command value is one of
8769 	// the given values and at least one whose effective command value is
8770 	// not one of the given values."
8771 	if ("inlineCommandActivatedValues" in commands[command]
8772 	&& !("indeterm" in commands[command])) {
8773 		commands[command].indeterm = function( range ) {
8774 			var values = $_( getAllEffectivelyContainedNodes(range, function(node) {
8775 				return isEditable(node)
8776 					&& node.nodeType == $_.Node.TEXT_NODE;
8777 			}) ).map(function(node) { return getEffectiveCommandValue(node, command) });
8778 
8779 			var matchingValues = $_( values ).filter(function(value) {
8780 				return $_( commands[command].inlineCommandActivatedValues ).indexOf(value) != -1;
8781 			});
8782 
8783 			return matchingValues.length >= 1
8784 				&& values.length - matchingValues.length >= 1;
8785 		};
8786 	}
8787 
8788 	// "If a command has inline command activated values defined, its state
8789 	// is true if either no editable Text node is effectively contained in
8790 	// the active range, and the active range's start node's effective
8791 	// command value is one of the given values; or if there is at least
8792 	// one editable Text node effectively contained in the active range,
8793 	// and all of them have an effective command value equal to one of the
8794 	// given values."
8795 	if ("inlineCommandActivatedValues" in commands[command]) {
8796 		commands[command].state = function(range) {
8797 			var nodes = getAllEffectivelyContainedNodes(range, function(node) {
8798 				return isEditable(node)
8799 					&& node.nodeType == $_.Node.TEXT_NODE;
8800 			});
8801 
8802 			if (nodes.length == 0) {
8803 				return $_( commands[command].inlineCommandActivatedValues )
8804 					.indexOf(getEffectiveCommandValue(range.startContainer, command)) != -1;
8805 				return ret;
8806 			} else {
8807 				return $_( nodes ).every(function(node) {
8808 					return $_( commands[command].inlineCommandActivatedValues )
8809 						.indexOf(getEffectiveCommandValue(node, command)) != -1;
8810 				});
8811 			}
8812 		};
8813 	}
8814 
8815 	// "If a command is a standard inline value command, it is
8816 	// indeterminate if among editable Text nodes that are effectively
8817 	// contained in the active range, there are two that have distinct
8818 	// effective command values. Its value is the effective command value
8819 	// of the first editable Text node that is effectively contained in the
8820 	// active range, or if there is no such node, the effective command
8821 	// value of the active range's start node."
8822 	if ("standardInlineValueCommand" in commands[command]) {
8823 		commands[command].indeterm = function() {
8824 			var values = $_(getAllEffectivelyContainedNodes(getActiveRange()))
8825 				.filter(function(node) { return isEditable(node) && node.nodeType == $_.Node.TEXT_NODE }, true)
8826 				.map(function(node) { return getEffectiveCommandValue(node, command) });
8827 			for (var i = 1; i < values.length; i++) {
8828 				if (values[i] != values[i - 1]) {
8829 					return true;
8830 				}
8831 			}
8832 			return false;
8833 		};
8834 
8835 		commands[command].value = function(range) {
8836 			var refNode = getAllEffectivelyContainedNodes(range, function(node) {
8837 				return isEditable(node)
8838 					&& node.nodeType == $_.Node.TEXT_NODE;
8839 			})[0];
8840 
8841 			if (typeof refNode == "undefined") {
8842 				refNode = range.startContainer;
8843 			}
8844 
8845 			return getEffectiveCommandValue(refNode, command);
8846 		};
8847 	}
8848 });
8849 })();
8850 //@}
8851 return {
8852 	commands: commands,
8853 	execCommand: myExecCommand,
8854 	queryCommandIndeterm: myQueryCommandIndeterm,
8855 	queryCommandState: myQueryCommandState,
8856 	queryCommandValue: myQueryCommandValue,
8857 	queryCommandEnabled: myQueryCommandEnabled,
8858 	queryCommandSupported: myQueryCommandSupported,
8859 	copyAttributes: copyAttributes,
8860 	createEndBreak: createEndBreak,
8861 	isEndBreak: isEndBreak,
8862 	ensureContainerEditable: ensureContainerEditable,
8863 	isEditingHost: isEditingHost,
8864 	isEditable: isEditable
8865 }
8866 }); // end define
8867 // vim: foldmarker=@{,@} foldmethod=marker
8868