Finish refactor of implementation.js
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Wed, 15 Jun 2011 12:33:24 -0600
changeset 271 63a2f1a1378e
parent 270 6e503195be17
child 272 e6efe73e13c5
Finish refactor of implementation.js

Also some minor tweaks to other files.
autoimplementation.html
editcommands.html
implementation.js
source.html
tests.js
--- a/autoimplementation.html	Tue Jun 14 14:53:50 2011 -0600
+++ b/autoimplementation.html	Wed Jun 15 12:33:24 2011 -0600
@@ -28,24 +28,26 @@
 "use strict";
 // Set up all the HTML automatically so I can add new commands to test without
 // copy-pasting in five places
-var toc = document.querySelector("ul");
-for (var command in tests) {
-	var li = document.createElement("li");
-	li.innerHTML = "<a href=#" + command + ">" + command + "</a>";
-	toc.appendChild(li);
+(function() {
+	var toc = document.querySelector("ul");
+	for (var command in tests) {
+		var li = document.createElement("li");
+		li.innerHTML = "<a href=#" + command + ">" + command + "</a>";
+		toc.appendChild(li);
 
-	var div = document.createElement("div");
-	div.id = command;
-	div.innerHTML = "<h1>" + command + "</h1>"
-		+ "<button onclick=\"runTests('" + command + "')\">Run tests</button>"
-		+ (command in notes ? "<p>" + notes[command] : "")
-		+ "<table border=1><tr><th>Input <th>Spec <th>Browser <th>Same?</table>"
-		+ (doubleTestingCommands.indexOf(command) != -1 ? "<table border=1><tr><th>Input <th>Spec <th>Browser <th>Same?</table>" : "")
-		+ "<p><label>New test input: <input></label>"
-		+ (command in defaultValues ? "<label>New test value: <input></label>" : "")
-		+ "<button onclick=\"addTest('" + command + "')\">Add test</button>";
-	document.body.appendChild(div);
-}
+		var div = document.createElement("div");
+		div.id = command;
+		div.innerHTML = "<h1>" + command + "</h1>"
+			+ "<button onclick=\"runTests('" + command + "')\">Run tests</button>"
+			+ (command in notes ? "<p>" + notes[command] : "")
+			+ "<table border=1><tr><th>Input <th>Spec <th>Browser <th>Same?</table>"
+			+ (doubleTestingCommands.indexOf(command) != -1 ? "<table border=1><tr><th>Input <th>Spec <th>Browser <th>Same?</table>" : "")
+			+ "<p><label>New test input: <input></label>"
+			+ (command in defaultValues ? "<label>New test value: <input></label>" : "")
+			+ "<button onclick=\"addTest('" + command + "')\">Add test</button>";
+		document.body.appendChild(div);
+	}
+})();
 
 function runTests(command) {
 	var runTestsButton = document.querySelector("#" + command + " button");
--- a/editcommands.html	Tue Jun 14 14:53:50 2011 -0600
+++ b/editcommands.html	Wed Jun 15 12:33:24 2011 -0600
@@ -351,6 +351,14 @@
 in Firefox 4b11.  It returns boolean false in Chrome 10, and the empty string
 in Opera 11. -->
 
+<!-- We have lots of options for the default value of commands.  Using bold as
+an example, IE 9 RC returns the boolean false, Firefox 4b11 and Opera 11 both
+return the empty string, Chrome 10 returns the string "false".  The HTML5 spec
+as of February 2011 mandates WebKit's behavior.  It makes sense to always
+return a string, a majority of string-returners return the empty string, and
+three out of the four return something that evaluates to false as a boolean, so
+I'll go with Firefox and Opera. -->
+
 <p>When <code><a href=#execcommand()>execCommand()</a></code> is invoked, the user agent must follow the
 <a href=#action>action</a> instructions given in this specification for
 <var title="">command</var>, with <var title="">showUI</var> and <var title="">value</var> passed to the
@@ -2414,15 +2422,6 @@
 general idea as the spec, considering a range bold only if all text in it is
 bold, and this seems to match at least OpenOffice.org's bold feature. -->
 
-<p><a href=#value>Value</a>: Always the empty string.
-<!-- We have lots of options here (and presumably for all the others where
-value is meaningless).  IE 9 RC returns the boolean false, Firefox 4b11 and
-Opera 11 both return the empty string, Chrome 10 returns the string "false".
-The HTML5 spec as of February 2011 mandates WebKit's behavior.  It makes sense
-to always return a string, a majority of string-returners return the empty
-string, and three out of the four return something that evaluates to false as a
-boolean, so I'll go with Firefox and Opera. -->
-
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "font-weight"
 
 
@@ -2478,10 +2477,8 @@
   <var title="">value</var>.
 </ol>
 
-<p><a href=#state>State</a>: Always false.
-
-<p><a href=#value>Value</a>: Always the empty string.
-<!-- I'd have expected the value to be the URL, but guess not. -->
+<!-- I'd have expected the value to be the URL, but guess not: it's always
+false. -->
 
 
 <h3 id=the-fontname-command><span class=secno>6.11 </span><dfn>The <code title="">fontName</code> command</dfn></h3>
@@ -2509,8 +2506,6 @@
 understand CSS font-family syntax?), so I don't think such usability concerns
 apply. -->
 
-<p><a href=#state>State</a>: Always false.
-
 <p><a href=#value>Value</a>: The computed value of the CSS property "font-family" for
 . . .
 <!-- Complicated.
@@ -2712,12 +2707,11 @@
   value</a> of each returned <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>node</a> to <var title="">value</var>.
 </ol>
 
-<p><a href=#state>State</a>: Always false.
-<!-- This matches IE 9 RC and Chrome 10.  Opera 11 seems to return true if
-there's some color style applied, false otherwise, which seems fairly useless;
-authors want to use value here, not state.  Firefox 4b11 throws an exception,
-which is an interesting approach, but I'll go with IE/WebKit, which makes at
-least as much sense. -->
+<!-- The state is undefined, thus always false.  This matches IE 9 RC and
+Chrome 10.  Opera 11 seems to return true if there's some color style applied,
+false otherwise, which seems fairly useless; authors want to use value here,
+not state.  Firefox 4b11 throws an exception, which is an interesting approach,
+but I'll go with IE/WebKit, which makes at least as much sense. -->
 
 <p><a href=#value>Value</a>: ?
 <!-- IE 9 RC returns the number 0 always, which makes no sense at all. -->
@@ -2760,8 +2754,6 @@
   value</a> of each returned <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>node</a> to <var title="">value</var>.
 </ol>
 
-<p><a href=#state>State</a>: Always false.
-
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "background-color"
 
 
@@ -2811,7 +2803,7 @@
 
   <li>Let <var title="">range</var> be the <a href=#active-range>active range</a>.
 
-  <li><a href=#delete-the-contents>Delete the contents</a> of the <a href=#active-range>active range</a>.
+  <li><a href=#delete-the-contents>Delete the contents</a> of <var title="">range</var>.
 
   <li>Let <var title="">img</var> be the result of calling <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("img")</a></code> on the
   <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#context-object>context object</a>.
@@ -2843,8 +2835,6 @@
 is <a href=#effectively-contained>effectively contained</a> in the <a href=#active-range>active range</a> has
 <a href=#effective-value>effective value</a> either "italic" or "oblique".  Otherwise false.
 
-<p><a href=#value>Value</a>: Always the empty string.
-
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "font-style"
 
 
@@ -2983,8 +2973,6 @@
 is <a href=#effectively-contained>effectively contained</a> in the <a href=#active-range>active range</a> has
 <a href=#effective-value>effective value</a> "line-through".  Otherwise false.
 
-<p><a href=#value>Value</a>: Always the empty string.
-
 
 <h3 id=the-subscript-command><span class=secno>6.20 </span><dfn>The <code title="">subscript</code> command</dfn></h3>
 
@@ -3095,8 +3083,6 @@
 is <a href=#effectively-contained>effectively contained</a> in the <a href=#active-range>active range</a> has
 <a href=#effective-value>effective value</a> "underline".  Otherwise false.
 
-<p><a href=#value>Value</a>: Always the empty string.
-
 
 <h3 id=the-unlink-command><span class=secno>6.23 </span><dfn>The <code title="">unlink</code> command</dfn></h3>
 
@@ -3118,10 +3104,6 @@
   <li><a href=#clear-the-value>Clear the value</a> of each member of <var title="">hyperlinks</var>.
 </ol>
 
-<p><a href=#state>State</a>: Always false.
-
-<p><a href=#value>Value</a>: Always the empty string.
-
 
 <h2 id=block-formatting-commands><span class=secno>7 </span>Block formatting commands</h2>
 
--- a/implementation.js	Tue Jun 14 14:53:50 2011 -0600
+++ b/implementation.js	Wed Jun 15 12:33:24 2011 -0600
@@ -15,7 +15,6 @@
 ///////////////////////////////////////////////////////////////////////////////
 //@{
 
-
 function nextNode(node) {
 	if (node.hasChildNodes()) {
 		return node.firstChild;
@@ -141,7 +140,7 @@
 			|| (val1.toLowerCase() == "normal" && val2 == "400")
 			|| (val2.toLowerCase() == "normal" && val1 == "400");
 	}
-	var property = getRelevantCssProperty(command);
+	var property = commands[command].relevantCssProperty;
 	var test1 = document.createElement("span");
 	test1.style[property] = val1;
 	var test2 = document.createElement("span");
@@ -202,6 +201,35 @@
 	return ns === null
 		|| ns === htmlNamespace;
 }
+
+// For computing states of the form "True if every editable Text node that is
+// effectively contained in the active range (has property X).  Otherwise
+// false."
+function stateHelper(callback) {
+	var range = getActiveRange();
+	// XXX: This algorithm for getting all effectively contained nodes might be
+	// wrong . . .
+	var node = range.startContainer;
+	while (node.parentNode && node.parentNode.firstChild == node) {
+		node = node.parentNode;
+	}
+	var stop = nextNodeDescendants(range.endContainer);
+
+	for (; node && node != stop; node = nextNode(node)) {
+		if (!isEffectivelyContained(node, range)
+		|| node.nodeType != Node.TEXT_NODE
+		|| !isEditable(node)) {
+			continue;
+		}
+
+		if (!callback(node)) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
 //@}
 
 
@@ -451,6 +479,84 @@
 /////////////////////////// Edit command functions ///////////////////////////
 //////////////////////////////////////////////////////////////////////////////
 
+/////////////////////////////////////////////////
+///// Methods of the HTMLDocument interface /////
+/////////////////////////////////////////////////
+//@{
+function myExecCommand(command, showUI, value, range) {
+	command = command.toLowerCase();
+
+	if (typeof range != "undefined") {
+		globalRange = range;
+	} else {
+		globalRange = getActiveRange();
+	}
+
+	// "If the active range is null, all commands must behave as though they
+	// were not defined except those in the miscellaneous commands section."
+	if (!globalRange && command != "selectall" && command != "stylewithcss" && command != "usecss") {
+		return;
+	}
+
+	commands[command].action(value);
+
+	globalRange = null;
+}
+
+function myQueryCommandState(command) {
+	command = command.toLowerCase();
+
+	if (typeof range != "undefined") {
+		globalRange = range;
+	} else {
+		globalRange = getActiveRange();
+	}
+
+	if (!globalRange && command != "selectall" && command != "stylewithcss" && command != "usecss") {
+		return;
+	}
+
+	return commands[command].state();
+
+	globalRange = null;
+}
+
+function myQueryCommandValue(command) {
+	command = command.toLowerCase();
+
+	if (typeof range != "undefined") {
+		globalRange = range;
+	} else {
+		globalRange = getActiveRange();
+	}
+
+	if (!globalRange && command != "selectall" && command != "stylewithcss" && command != "usecss") {
+		return;
+	}
+
+	return commands[command].value();
+
+	globalRange = null;
+}
+
+/**
+ * "Most commands act on the active range. This is defined to be the first
+ * range in the Selection given by calling getSelection() on the context
+ * object, or null if there is no such range."
+ *
+ * We cheat and return globalRange if that's defined.
+ */
+function getActiveRange() {
+	if (globalRange) {
+		return globalRange;
+	}
+	if (getSelection().rangeCount) {
+		return getSelection().getRangeAt(0);
+	}
+	return null;
+}
+//@}
+
 //////////////////////////////
 ///// Common definitions /////
 //////////////////////////////
@@ -1840,7 +1946,7 @@
 
 //@}
 
-///// Assorted inline formatting command definitions /////
+///// Assorted inline formatting command algorithms /////
 //@{
 
 function getEffectiveValue(node, command) {
@@ -1984,7 +2090,7 @@
 
 	// "Return the computed style for node of the relevant CSS property for
 	// command."
-	return getComputedStyle(node)[getRelevantCssProperty(command)];
+	return getComputedStyle(node)[commands[command].relevantCssProperty];
 }
 
 function getSpecifiedValue(element, command) {
@@ -2085,7 +2191,7 @@
 	}
 
 	// "Let property be the relevant CSS property for command."
-	var property = getRelevantCssProperty(command);
+	var property = commands[command].relevantCssProperty;
 
 	// "If property is null, return null."
 	if (property === null) {
@@ -2325,8 +2431,8 @@
 
 	// "If the relevant CSS property for command is not null, unset the CSS
 	// property property of element."
-	if (getRelevantCssProperty(command) !== null) {
-		element.style[getRelevantCssProperty(command)] = '';
+	if (commands[command].relevantCssProperty !== null) {
+		element.style[commands[command].relevantCssProperty] = '';
 		if (element.getAttribute("style") == "") {
 			element.removeAttribute("style");
 		}
@@ -2719,7 +2825,7 @@
 	// "If the effective value of command for new parent is not new value, and
 	// the relevant CSS property for command is not null, set that CSS property
 	// of new parent to new value (if the new value would be valid)."
-	var property = getRelevantCssProperty(command);
+	var property = commands[command].relevantCssProperty;
 	if (property !== null
 	&& !valuesEqual(command, getEffectiveValue(newParent, command), newValue)) {
 		newParent.style[property] = newValue;
@@ -2879,6 +2985,573 @@
 
 //@}
 
+///// The backColor command /////
+// Unimplemented
+commands.backcolor = {};
+
+///// The bold command /////
+//@{
+commands.bold = {
+	action: function() {
+		// "Decompose the active range. If the state is then false, set the
+		// value of each returned node to "bold", otherwise set the value to
+		// "normal"."
+		var nodeList = decomposeRange(getActiveRange());
+		var newValue = commands.bold.state() ? "normal" : "bold";
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "bold", newValue);
+		}
+	}, state: function() { return stateHelper(function(node) {
+		// "True if every editable Text node that is effectively contained in
+		// the active range has effective value at least 700. Otherwise false."
+		var fontWeight = getEffectiveValue(node, "bold");
+		return fontWeight === "bold"
+			|| fontWeight === "700"
+			|| fontWeight === "800"
+			|| fontWeight === "900";
+	})}, relevantCssProperty: "fontWeight"
+};
+//@}
+
+///// The createLink command /////
+//@{
+commands.createlink = {
+	action: function(value) {
+		// "If value is the empty string, abort these steps and do nothing."
+		if (value === "") {
+			return;
+		}
+
+		// "Decompose the active range, and let node list be the result."
+		var nodeList = decomposeRange(getActiveRange());
+
+		// "For each a element that has an href attribute and is an ancestor of
+		// some node in node list, set that element's href attribute to value."
+		for (var i = 0; i < nodeList.length; i++) {
+			var candidate = nodeList[i].parentNode;
+			while (candidate) {
+				if (isHtmlElement(candidate, "A")
+				&& candidate.hasAttribute("href")) {
+					candidate.setAttribute("href", value);
+				}
+
+				candidate = candidate.parentNode;
+			}
+		}
+
+		// "Set the value of each node in node list to value."
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "createlink", value);
+		}
+	}
+};
+//@}
+
+///// The fontName command /////
+//@{
+commands.fontname = {
+	action: function(value) {
+		// "Decompose the active range, then set the value of each returned
+		// node to value."
+		var nodeList = decomposeRange(getActiveRange());
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "fontname", value);
+		}
+	}, relevantCssProperty: "fontFamily"
+};
+//@}
+
+///// The fontSize command /////
+//@{
+commands.fontsize = {
+	action: function(value) {
+		// "If value is the empty string, do nothing and abort these steps."
+		if (value === "") {
+			return;
+		}
+
+		// "Strip leading and trailing whitespace from value."
+		//
+		// Cheap hack, not following the actual algorithm.
+		value = value.trim();
+
+		// "If value is a valid floating point number, or would be a valid
+		// floating point number if a single leading "+" character were
+		// stripped:"
+		if (/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
+			var mode;
+
+			// "If the first character of value is "+", delete the character
+			// and let mode be "relative-plus"."
+			if (value[0] == "+") {
+				value = value.slice(1);
+				mode = "relative-plus";
+			// "Otherwise, if the first character of value is "-", delete the
+			// character and let mode be "relative-minus"."
+			} else if (value[0] == "-") {
+				value = value.slice(1);
+				mode = "relative-minus";
+			// "Otherwise, let mode be "absolute"."
+			} else {
+				mode = "absolute";
+			}
+
+			// "Apply the rules for parsing non-negative integers to value, and
+			// let number be the result."
+			//
+			// Another cheap hack.
+			var num = parseInt(value);
+
+			// "If mode is "relative-plus", add three to number."
+			if (mode == "relative-plus") {
+				num += 3;
+			}
+
+			// "If mode is "relative-minus", negate number, then add three to
+			// it."
+			if (mode == "relative-minus") {
+				num = 3 - num;
+			}
+
+			// "If number is less than one, let number equal 1."
+			if (num < 1) {
+				num = 1;
+			}
+
+			// "If number is greater than seven, let number equal 7."
+			if (num > 7) {
+				num = 7;
+			}
+
+			// "Set value to the string here corresponding to number:" [table
+			// omitted]
+			value = {
+				1: "xx-small",
+				2: "small",
+				3: "medium",
+				4: "large",
+				5: "x-large",
+				6: "xx-large",
+				7: "xxx-large"
+			}[num];
+		}
+
+		// "If value is not one of the strings "xx-small", "x-small", "small",
+		// "medium", "large", "x-large", "xx-large", "xxx-large", and is not a
+		// valid CSS absolute length, then do nothing and abort these steps."
+		//
+		// More cheap hacks to skip valid CSS absolute length checks.
+		if (["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(value) == -1
+		&& !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc)$/.test(value)) {
+			return;
+		}
+
+		// "Decompose the active range, then set the value of each returned
+		// node to value."
+		var nodeList = decomposeRange(getActiveRange());
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "fontsize", value);
+		}
+	}, relevantCssProperty: "fontSize"
+};
+//@}
+
+///// The foreColor command /////
+//@{
+commands.forecolor = {
+	action: function(value) {
+		// Copy-pasted, same as hiliteColor
+
+		// "If value is not a valid CSS color, prepend "#" to it."
+		//
+		// "If value is still not a valid CSS color, or if it is currentColor,
+		// do nothing and abort these steps."
+		//
+		// Cheap hack for testing, no attempt to be comprehensive.
+		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
+			value = "#" + value;
+		}
+		if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(value)
+		&& !/^(rgba?|hsla?)\(.*\)$/.test(value)
+		// Not gonna list all the keywords, only the ones I use.
+		&& value != "red"
+		&& value != "cornsilk"
+		&& value != "transparent") {
+			return;
+		}
+
+		// "Decompose the active range, then set the value of each returned
+		// node to value."
+		var nodeList = decomposeRange(getActiveRange());
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "forecolor", value);
+		}
+	}, relevantCssProperty: "color"
+};
+//@}
+
+///// The hiliteColor command /////
+//@{
+commands.hilitecolor = {
+	action: function(value) {
+		// Copy-pasted, same as foreColor
+
+		// "If value is not a valid CSS color, prepend "#" to it."
+		//
+		// "If value is still not a valid CSS color, or if it is currentColor,
+		// do nothing and abort these steps."
+		//
+		// Cheap hack for testing, no attempt to be comprehensive.
+		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
+			value = "#" + value;
+		}
+		if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(value)
+		&& !/^(rgba?|hsla?)\(.*\)$/.test(value)
+		// Not gonna list all the keywords, only the ones I use.
+		&& value != "red"
+		&& value != "cornsilk"
+		&& value != "transparent") {
+			return;
+		}
+
+		// "Decompose the active range, then set the value of each returned
+		// node to value."
+		var nodeList = decomposeRange(getActiveRange());
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "hilitecolor", value);
+		}
+	}, relevantCssProperty: "backgroundColor"
+};
+//@}
+
+///// The insertHTML command /////
+//@{
+commands.inserthtml = {
+	action: function(value) {
+		// "Delete the contents of the active range."
+		deleteContents(getActiveRange());
+
+		// "Let frag be the result of calling createContextualFragment(value)
+		// on the active range."
+		var frag = getActiveRange().createContextualFragment(value);
+
+		// "Let descendants be all descendants of frag."
+		var descendants = getDescendants(frag);
+
+		// "Let last child be the lastChild of frag."
+		var lastChild = frag.lastChild;
+
+		// "Call insertNode(frag) on the active range."
+		getActiveRange().insertNode(frag);
+
+		// "If last child is not null, set the start and end of the active
+		// range to (last child, length of last child)."
+		if (lastChild) {
+			getActiveRange().setStart(lastChild, getNodeLength(lastChild));
+			getActiveRange().setEnd(lastChild, getNodeLength(lastChild));
+		}
+
+		// "Fix disallowed ancestors of each member of descendants."
+		for (var i = 0; i < descendants.length; i++) {
+			fixDisallowedAncestors(descendants[i]);
+		}
+	}
+};
+//@}
+
+///// The insertImage command /////
+//@{
+commands.insertimage = {
+	action: function(value) {
+		// "If value is the empty string, abort these steps and do nothing."
+		if (value === "") {
+			return;
+		}
+
+		// "Let range be the active range."
+		var range = getActiveRange();
+
+		// "Delete the contents of range."
+		deleteContents(range);
+
+		// "Let img be the result of calling createElement("img") on the
+		// context object."
+		var img = document.createElement("img");
+
+		// "Run setAttribute("src", value) on img."
+		img.setAttribute("src", value);
+
+		// "Run insertNode(img) on the range."
+		range.insertNode(img);
+
+		// "Run collapse() on the Selection, with first argument equal to the
+		// parent of img and the second argument equal to one plus the index of
+		// img."
+		//
+		// Not everyone actually supports collapse(), so we do it manually
+		// instead.  Also, we need to modify the actual range we're given as
+		// well, for the sake of autoimplementation.html's range-filling-in.
+		range.setStart(img.parentNode, 1 + getNodeIndex(img));
+		range.setEnd(img.parentNode, 1 + getNodeIndex(img));
+		getSelection().removeAllRanges();
+		getSelection().addRange(range);
+
+		// IE adds width and height attributes for some reason, so remove those
+		// to actually do what the spec says.
+		img.removeAttribute("width");
+		img.removeAttribute("height");
+	}
+};
+//@}
+
+///// The italic command /////
+//@{
+commands.italic = {
+	action: function() {
+		// "Decompose the active range. If the state is then false, set the
+		// value of each returned node to "italic", otherwise set the value to
+		// "normal"."
+		var nodeList = decomposeRange(getActiveRange());
+		var newValue = commands.italic.state() ? "normal" : "italic";
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "italic", newValue);
+		}
+	}, state: function() { return stateHelper(function(node) {
+		// "True if every editable Text node that is effectively contained in
+		// the active range has effective value either "italic" or "oblique".
+		// Otherwise false."
+		var value = getEffectiveValue(node, "italic");
+		return value == "italic" || value == "oblique";
+	})}, relevantCssProperty: "fontStyle"
+};
+//@}
+
+///// The removeFormat command /////
+//@{
+commands.removeformat = {
+	action: function() {
+		// "Decompose the active range, and let node list be the result."
+		var nodeList = decomposeRange(getActiveRange());
+
+		// "For each node in node list, unset the style attribute of node (if
+		// it's an Element) and then all its Element descendants."
+		for (var i = 0; i < nodeList.length; i++) {
+			for (
+				var node = nodeList[i];
+				node != nextNodeDescendants(nodeList[i]);
+				node = nextNode(node)
+			) {
+				if (node.nodeType == Node.ELEMENT_NODE) {
+					node.removeAttribute("style");
+				}
+			}
+		}
+
+		// "Let elements to remove be a list of all HTML elements that are the
+		// same as or descendants of some member of node list and have non-null
+		// parents and satisfy (insert conditions here)."
+		var elementsToRemove = [];
+		for (var i = 0; i < nodeList.length; i++) {
+			for (
+				var node = nodeList[i];
+				node == nodeList[i] || isDescendant(node, nodeList[i]);
+				node = nextNode(node)
+			) {
+				if (isHtmlElement(node)
+				&& node.parentNode
+				// FIXME: Extremely partial list for testing
+				&& ["A", "AUDIO", "BR", "DIV", "HR", "IMG", "P", "TD", "VIDEO", "WBR"].indexOf(node.tagName) == -1) {
+					elementsToRemove.push(node);
+				}
+			}
+		}
+
+		// "For each element in elements to remove:"
+		for (var i = 0; i < elementsToRemove.length; i++) {
+			var element = elementsToRemove[i];
+
+			// "While element has children, insert the first child of element
+			// into the parent of element immediately before element,
+			// preserving ranges."
+			while (element.childNodes.length) {
+				movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
+			}
+
+			// "Remove element from its parent."
+			element.parentNode.removeChild(element);
+		}
+
+		// "For each of the entries in the following table, in the given order:
+		// decompose the active range again; then set the value of the
+		// resulting nodes, with command and new value as given."
+		var table = {
+			"subscript": "baseline",
+			"bold": "normal",
+			"fontname": null,
+			"fontsize": null,
+			"forecolor": null,
+			"hilitecolor": null,
+			"italic": "normal",
+			"strikethrough": null,
+			"underline": null,
+		};
+		for (var command in table) {
+			var nodeList = decomposeRange(getActiveRange());
+			for (var i = 0; i < nodeList.length; i++) {
+				setNodeValue(nodeList[i], command, table[command]);
+			}
+		}
+	}
+};
+//@}
+
+///// The strikethrough command /////
+//@{
+commands.strikethrough = {
+	action: function() {
+		// "Decompose the active range. If the state is then false, set the
+		// value of each returned node to "line-through", otherwise set the
+		// value to null."
+		var nodeList = decomposeRange(getActiveRange());
+		var newValue = commands.strikethrough.state() ? null : "line-through";
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "strikethrough", newValue);
+		}
+	}, state: function() { return stateHelper(function(node) {
+		// "True if every editable Text node that is effectively contained in
+		// the active range has effective value "line-through". Otherwise
+		// false."
+		return getEffectiveValue(node, "strikethrough") == "line-through";
+	})}
+};
+//@}
+
+///// The subscript command /////
+//@{
+commands.subscript = {
+	action: function() {
+		// "Decompose the active range, and let node list be the result."
+		var nodeList = decomposeRange(getActiveRange());
+
+		// "Let state be the state."
+		var state = commands.subscript.state();
+
+		// "Set the value of each node in node list to "baseline"."
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "subscript", "baseline");
+		}
+
+		// "If state is false, decompose the active range again and set the
+		// value of each returned node to "sub"."
+		if (!state) {
+			nodeList = decomposeRange(getActiveRange());
+			for (var i = 0; i < nodeList.length; i++) {
+				setNodeValue(nodeList[i], "subscript", "sub");
+			}
+		}
+	}, state: function() { return stateHelper(function(node) {
+		// "True if every editable Text node that is effectively contained in
+		// the active range has effective value "sub". Otherwise false."
+		return getEffectiveValue(node, "subscript") == "sub";
+	})}, relevantCssProperty: "verticalAlign"
+};
+//@}
+
+///// The superscript command /////
+//@{
+commands.superscript = {
+	action: function() {
+		// "Decompose the active range, and let node list be the result."
+		var nodeList = decomposeRange(getActiveRange());
+
+		// "Let state be the state."
+		var state = commands.superscript.state();
+
+		// "Set the value of each node in node list to "baseline"."
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "superscript", "baseline");
+		}
+
+		// "If state is false, decompose the active range again and set the
+		// value of each returned node to "super"."
+		if (!state) {
+			nodeList = decomposeRange(getActiveRange());
+			for (var i = 0; i < nodeList.length; i++) {
+				setNodeValue(nodeList[i], "superscript", "super");
+			}
+		}
+	}, state: function() { return stateHelper(function(node) {
+		// "True if every editable Text node that is effectively contained in
+		// the active range has effective value "super". Otherwise false."
+		return getEffectiveValue(node, "superscript") == "super";
+	})}, relevantCssProperty: "verticalAlign"
+};
+//@}
+
+///// The underline command /////
+//@{
+commands.underline = {
+	action: function() {
+		// "Decompose the active range. If the state is then false, set the
+		// value of each returned node to "underline", otherwise set the value
+		// to null."
+		var nodeList = decomposeRange(getActiveRange());
+		var newValue = commands.underline.state() ? null : "underline";
+		for (var i = 0; i < nodeList.length; i++) {
+			setNodeValue(nodeList[i], "underline", newValue);
+		}
+	}, state: function() { return stateHelper(function(node) {
+		// "True if every editable Text node that is effectively contained in
+		// the active range has effective value "underline". Otherwise false."
+		return getEffectiveValue(node, "underline") === "underline";
+	})}
+};
+//@}
+
+///// The unlink command /////
+//@{
+commands.unlink = {
+	action: function() {
+		// "Let hyperlinks be a list of every a element that has an href
+		// attribute and is contained in the active range or is an ancestor of
+		// one of its boundary points."
+		//
+		// As usual, take care to ensure it's tree order.  The correctness of
+		// the following is left as an exercise for the reader.
+		var range = getActiveRange();
+		var hyperlinks = [];
+		for (
+			var node = range.startContainer;
+			node;
+			node = node.parentNode
+		) {
+			if (isHtmlElement(node, "A")
+			&& node.hasAttribute("href")) {
+				hyperlinks.unshift(node);
+			}
+		}
+		for (
+			var node = range.startContainer;
+			node != nextNodeDescendants(range.endContainer);
+			node = nextNode(node)
+		) {
+			if (isHtmlElement(node, "A")
+			&& node.hasAttribute("href")
+			&& (isContained(node, range)
+			|| isAncestor(node, range.endContainer)
+			|| node == range.endContainer)) {
+				hyperlinks.push(node);
+			}
+		}
+
+		// "Clear the value of each member of hyperlinks."
+		for (var i = 0; i < hyperlinks.length; i++) {
+			clearValue(hyperlinks[i], "unlink");
+		}
+	}
+};
+//@}
+
 
 /////////////////////////////////////
 ///// Block formatting commands /////
@@ -3593,7 +4266,8 @@
 ///// Toggling lists /////
 //@{
 
-function toggleLists(range, tagName) {
+function toggleLists(tagName) {
+	var range = getActiveRange();
 	tagName = tagName.toUpperCase();
 
 	// "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is
@@ -3991,91 +4665,20 @@
 
 //@}
 
-function getRelevantCssProperty(command) {
-	var prop = {
-		bold: "fontWeight",
-		fontname: "fontFamily",
-		fontsize: "fontSize",
-		forecolor: "color",
-		hilitecolor: "backgroundColor",
-		italic: "fontStyle",
-		subscript: "verticalAlign",
-		superscript: "verticalAlign",
-	}[command];
-
-	if (typeof prop == "undefined") {
-		return null;
-	}
-	return prop;
-}
-
-function myExecCommand(command, showUI, value, range) {
-	command = command.toLowerCase();
-
-	if (command != "stylewithcss" && command != "usecss") {
-		if (typeof range == "undefined" && getSelection().rangeCount) {
-			range = getSelection().getRangeAt(0);
-		}
-
-		if (!range) {
-			return;
-		}
-	}
-
-	globalRange = range;
-
-	switch (command) {
-		case "bold":
-		// "Decompose the range. If the state of the range for this command is
-		// then true, set the value of each returned node with new value
-		// "normal". Otherwise, set their value with new value "bold"."
-		var nodeList = decomposeRange(range);
-		var newValue = getState("bold", range) ? "normal" : "bold";
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, newValue);
-		}
-		break;
-
-		case "createlink":
-		// "If value is the empty string, abort these steps and do nothing."
-		if (value === "") {
-			break;
-		}
-
-		// "Decompose the range, and let node list be the result."
-		var nodeList = decomposeRange(range);
-
-		// "For each a element that has an href attribute and is an ancestor of
-		// some node in node list, set that element's href attribute to value."
-		for (var i = 0; i < nodeList.length; i++) {
-			var candidate = nodeList[i].parentNode;
-			while (candidate) {
-				if (isHtmlElement(candidate, "A")
-				&& candidate.hasAttribute("href")) {
-					candidate.setAttribute("href", value);
-				}
-
-				candidate = candidate.parentNode;
-			}
-		}
-
-		// "Set the value of each node in node list to value."
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, value);
-		}
-		break;
-
-		case "delete":
+///// The delete command /////
+//@{
+commands["delete"] = {
+	action: function() {
 		// "If the active range is not collapsed, delete the contents of the
 		// active range and abort these steps."
-		if (!range.collapsed) {
-			deleteContents(range);
+		if (!getActiveRange().collapsed) {
+			deleteContents(getActiveRange());
 			return;
 		}
 
 		// "Let node and offset be the active range's start node and offset."
-		var node = range.startContainer;
-		var offset = range.startOffset;
+		var node = getActiveRange().startContainer;
+		var offset = getActiveRange().startOffset;
 
 		// "Repeat the following steps:"
 		while (true) {
@@ -4127,8 +4730,8 @@
 		// steps."
 		if (node.nodeType == Node.TEXT_NODE
 		&& offset != 0) {
-			range.setStart(node, offset);
-			range.setEnd(node, offset);
+			getActiveRange().setStart(node, offset);
+			getActiveRange().setEnd(node, offset);
 			deleteContents(node, offset - 1, node, offset);
 			return;
 		}
@@ -4145,8 +4748,8 @@
 		if (0 <= offset - 1
 		&& offset - 1 < node.childNodes.length
 		&& isHtmlElement(node.childNodes[offset - 1], ["br", "hr", "img"])) {
-			range.setStart(node, offset);
-			range.setEnd(node, offset);
+			getActiveRange().setStart(node, offset);
+			getActiveRange().setEnd(node, offset);
 			deleteContents(node, offset - 1, node, offset);
 			return;
 		}
@@ -4244,11 +4847,11 @@
 		&& isHtmlElement(startNode.childNodes[startOffset - 1], "table")) {
 			// "Call collapse(start node, start offset − 1) on the context
 			// object's Selection."
-			range.setStart(startNode, startOffset - 1);
+			getActiveRange().setStart(startNode, startOffset - 1);
 
 			// "Call extend(start node, start offset) on the context object's
 			// Selection."
-			range.setEnd(startNode, startOffset);
+			getActiveRange().setEnd(startNode, startOffset);
 
 			// "Abort these steps."
 			return;
@@ -4268,8 +4871,8 @@
 			)
 		)) {
 			// "Call collapse(node, offset) on the Selection."
-			range.setStart(node, offset);
-			range.setEnd(node, offset);
+			getActiveRange().setStart(node, offset);
+			getActiveRange().setEnd(node, offset);
 
 			// "Delete the contents of the range with start (start node, start
 			// offset − 1) and end (start node, start offset)."
@@ -4331,137 +4934,14 @@
 		// "Delete the contents of the range with start (start node, start
 		// offset) and end (node, offset)."
 		deleteContents(startNode, startOffset, node, offset);
-		break;
-
-		case "fontname":
-		// "Decompose the range, then set the value of each returned node with
-		// new value equal to value."
-		var nodeList = decomposeRange(range);
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, value);
-		}
-		break;
-
-		case "fontsize":
-		// "If value is the empty string, do nothing and abort these steps."
-		if (value === "") {
-			return;
-		}
-
-		// "Strip leading and trailing whitespace from value."
-		//
-		// Cheap hack, not following the actual algorithm.
-		value = value.trim();
-
-		// "If value is a valid floating point number, or would be a valid
-		// floating point number if a single leading "+" character were
-		// stripped:"
-		if (/^[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/.test(value)) {
-			var mode;
-
-			// "If the first character of value is "+", delete the character
-			// and let mode be "relative-plus"."
-			if (value[0] == "+") {
-				value = value.slice(1);
-				mode = "relative-plus";
-			// "Otherwise, if the first character of value is "-", delete the
-			// character and let mode be "relative-minus"."
-			} else if (value[0] == "-") {
-				value = value.slice(1);
-				mode = "relative-minus";
-			// "Otherwise, let mode be "absolute"."
-			} else {
-				mode = "absolute";
-			}
-
-			// "Apply the rules for parsing non-negative integers to value, and
-			// let number be the result."
-			//
-			// Another cheap hack.
-			var num = parseInt(value);
-
-			// "If mode is "relative-plus", add three to number."
-			if (mode == "relative-plus") {
-				num += 3;
-			}
-
-			// "If mode is "relative-minus", negate number, then add three to
-			// it."
-			if (mode == "relative-minus") {
-				num = 3 - num;
-			}
-
-			// "If number is less than one, let number equal 1."
-			if (num < 1) {
-				num = 1;
-			}
-
-			// "If number is greater than seven, let number equal 7."
-			if (num > 7) {
-				num = 7;
-			}
-
-			// "Set value to the string here corresponding to number:" [table
-			// omitted]
-			value = {
-				1: "xx-small",
-				2: "small",
-				3: "medium",
-				4: "large",
-				5: "x-large",
-				6: "xx-large",
-				7: "xxx-large"
-			}[num];
-		}
-
-		// "If value is not one of the strings "xx-small", "x-small", "small",
-		// "medium", "large", "x-large", "xx-large", "xxx-large", and is not a
-		// valid CSS absolute length, then do nothing and abort these steps."
-		//
-		// More cheap hacks to skip valid CSS absolute length checks.
-		if (["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(value) == -1
-		&& !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc)$/.test(value)) {
-			return;
-		}
-
-		// "Decompose the range, then set the value of each returned node to
-		// value."
-		var nodeList = decomposeRange(range);
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, value);
-		}
-		break;
-
-		case "forecolor":
-		// See later for formatblock
-		case "hilitecolor":
-		// "If value is not a valid CSS color, prepend "#" to it."
-		//
-		// "If value is still not a valid CSS color, or if it is currentColor,
-		// do nothing and abort these steps."
-		//
-		// Cheap hack for testing, no attempt to be comprehensive.
-		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
-			value = "#" + value;
-		}
-		if (!/^#([0-9a-fA-F]{3}){1,2}$/.test(value)
-		&& !/^(rgba?|hsla?)\(.*\)$/.test(value)
-		// Not gonna list all the keywords, only the ones I use.
-		&& value != "red"
-		&& value != "cornsilk"
-		&& value != "transparent") {
-			return;
-		}
-
-		// "Decompose the range, then set the value of each returned node to
-		// value."
-		var nodeList = decomposeRange(range);
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, value);
-		}
-		break;
-
-		case "formatblock":
+	}
+};
+//@}
+
+///// The formatBlock command /////
+//@{
+commands.formatblock = {
+	action: function(value) {
 		// "If value begins with a "<" character and ends with a ">" character,
 		// remove the first and last characters from it."
 		if (/^<.*>$/.test(value)) {
@@ -4479,7 +4959,7 @@
 		}
 
 		// "Block-extend the active range, and let new range be the result."
-		var newRange = blockExtendRange(range);
+		var newRange = blockExtendRange(getActiveRange());
 
 		// "Let node list be an empty list of nodes."
 		var nodeList = [];
@@ -4496,25 +4976,30 @@
 
 		// "Block-format node list."
 		blockFormat(nodeList, value);
-		break;
-
-		case "indent":
+	}
+};
+//@}
+
+///// The indent command /////
+//@{
+commands.indent = {
+	action: function() {
 		// "Let items be a list of all lis that are ancestor containers of the
-		// range's start and/or end node."
+		// active range's start and/or end node."
 		//
 		// Has to be in tree order, remember!
 		var items = [];
-		for (var node = range.endContainer; node != range.commonAncestorContainer; node = node.parentNode) {
+		for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
 			if (isHtmlElement(node, "LI")) {
 				items.unshift(node);
 			}
 		}
-		for (var node = range.startContainer; node != range.commonAncestorContainer; node = node.parentNode) {
+		for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
 			if (isHtmlElement(node, "LI")) {
 				items.unshift(node);
 			}
 		}
-		for (var node = range.commonAncestorContainer; node; node = node.parentNode) {
+		for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
 			if (isHtmlElement(node, "LI")) {
 				items.unshift(node);
 			}
@@ -4525,8 +5010,8 @@
 			normalizeSublists(items[i]);
 		}
 
-		// "Block-extend the range, and let new range be the result."
-		var newRange = blockExtendRange(range);
+		// "Block-extend the active range, and let new range be the result."
+		var newRange = blockExtendRange(getActiveRange());
 
 		// "Let node list be a list of nodes, initially empty."
 		var nodeList = [];
@@ -4569,9 +5054,17 @@
 			// "Indent sublist."
 			indentNodes(sublist);
 		}
-		break;
-
-		case "inserthorizontalrule":
+	}
+};
+//@}
+
+///// The insertHorizontalRule command /////
+//@{
+commands.inserthorizontalrule = {
+	action: function() {
+		// "Let range be the active range."
+		var range = getActiveRange();
+
 		// "While range's start offset is 0 and its start node's parent is not
 		// null, set range's start to (parent of start node, index of start
 		// node)."
@@ -4612,83 +5105,25 @@
 		range.setEnd(hr.parentNode, 1 + getNodeIndex(hr));
 		getSelection().removeAllRanges();
 		getSelection().addRange(range);
-		break;
-
-		case "inserthtml":
+	}
+};
+//@}
+
+///// The insertOrderedList command /////
+commands.insertorderedlist = {
+	// "Toggle lists with tag name "ol"."
+	action: function() { toggleLists("ol") }
+};
+
+///// The insertParagraph command /////
+//@{
+commands.insertparagraph = {
+	action: function() {
 		// "Delete the contents of the active range."
-		deleteContents(range);
-
-		// "Let frag be the result of calling createContextualFragment(value)
-		// on the active range."
-		var frag = range.createContextualFragment(value);
-
-		// "Let descendants be all descendants of frag."
-		var descendants = getDescendants(frag);
-
-		// "Let last child be the lastChild of frag."
-		var lastChild = frag.lastChild;
-
-		// "Call insertNode(frag) on the active range."
-		range.insertNode(frag);
-
-		// "If last child is not null, set the start and end of the active
-		// range to (last child, length of last child)."
-		if (lastChild) {
-			range.setStart(lastChild, getNodeLength(lastChild));
-			range.setEnd(lastChild, getNodeLength(lastChild));
-		}
-
-		// "Fix disallowed ancestors of each member of descendants."
-		for (var i = 0; i < descendants.length; i++) {
-			fixDisallowedAncestors(descendants[i]);
-		}
-		break;
-
-		case "insertimage":
-		// "If value is the empty string, abort these steps and do nothing."
-		if (value === "") {
-			return;
-		}
-
-		// "Delete the contents of the active range."
-		deleteContents(range);
-
-		// "Let img be the result of calling createElement("img") on the
-		// context object."
-		var img = document.createElement("img");
-
-		// "Run setAttribute("src", value) on img."
-		img.setAttribute("src", value);
-
-		// "Run insertNode(img) on the range."
-		range.insertNode(img);
-
-		// "Run collapse() on the Selection, with first argument equal to the
-		// parent of img and the second argument equal to one plus the index of
-		// img."
-		//
-		// Not everyone actually supports collapse(), so we do it manually
-		// instead.  Also, we need to modify the actual range we're given as
-		// well, for the sake of autoimplementation.html's range-filling-in.
-		range.setStart(img.parentNode, 1 + getNodeIndex(img));
-		range.setEnd(img.parentNode, 1 + getNodeIndex(img));
-		getSelection().removeAllRanges();
-		getSelection().addRange(range);
-
-		// IE adds width and height attributes for some reason, so remove those
-		// to actually do what the spec says.
-		img.removeAttribute("width");
-		img.removeAttribute("height");
-		break;
-
-		case "insertorderedlist":
-		// "Toggle lists with tag name "ol"."
-		toggleLists(range, "ol");
-		break;
-
-		case "insertparagraph":
-		// "Delete the contents of the active range."
-		deleteContents(range);
+		deleteContents(getActiveRange());
+
+		// "Let range be the active range."
+		var range = getActiveRange();
 
 		// "Let node and offset be range's start node and offset."
 		var node = range.startContainer;
@@ -4936,57 +5371,60 @@
 
 		// "Set the start of range to (new container, 0)."
 		range.setStart(newContainer, 0);
-		break;
-
-		case "insertunorderedlist":
-		// "Toggle lists with tag name "ul"."
-		toggleLists(range, "ul");
-		break;
-
-		case "italic":
-		// "Decompose the range. If the state of the range for this command is
-		// then true, set the value of each returned node with new value
-		// "normal". Otherwise, set their value with new value "italic"."
-		var nodeList = decomposeRange(range);
-		var newValue = getState("italic", range) ? "normal" : "italic";
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, newValue);
-		}
-		break;
-
-		case "justifycenter":
-		justifySelection("center");
-		break;
-
-		case "justifyfull":
-		justifySelection("justify");
-		break;
-
-		case "justifyleft":
-		justifySelection("left");
-		break;
-
-		case "justifyright":
-		justifySelection("right");
-		break;
-
-		case "outdent":
+	}
+};
+//@}
+
+///// The insertUnorderedList command /////
+commands.insertunorderedlist = {
+	// "Toggle lists with tag name "ul"."
+	action: function() { toggleLists("ul") }
+};
+
+///// The justifyCenter command /////
+commands.justifycenter = {
+	// "Justify the selection with alignment "center"."
+	action: function() { justifySelection("center") }
+};
+
+///// The justifyFull command /////
+commands.justifyfull = {
+	// "Justify the selection with alignment "justify"."
+	action: function() { justifySelection("justify") }
+};
+
+///// The justifyLeft command /////
+commands.justifyleft = {
+	// "Justify the selection with alignment "left"."
+	action: function() { justifySelection("left") }
+};
+
+///// The justifyRight command /////
+commands.justifyright = {
+	// "Justify the selection with alignment "right"."
+	action: function() { justifySelection("right") }
+};
+
+///// The outdent command /////
+//@{
+commands.outdent = {
+	action: function() {
 		// "Let items be a list of all lis that are ancestor containers of the
-		// range's start and/or end node."
+		// active range's start and/or end node."
 		//
 		// Has to be in tree order, remember!
 		var items = [];
-		for (var node = range.endContainer; node != range.commonAncestorContainer; node = node.parentNode) {
+		for (var node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
 			if (isHtmlElement(node, "LI")) {
 				items.unshift(node);
 			}
 		}
-		for (var node = range.startContainer; node != range.commonAncestorContainer; node = node.parentNode) {
+		for (var node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) {
 			if (isHtmlElement(node, "LI")) {
 				items.unshift(node);
 			}
 		}
-		for (var node = range.commonAncestorContainer; node; node = node.parentNode) {
+		for (var node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) {
 			if (isHtmlElement(node, "LI")) {
 				items.unshift(node);
 			}
@@ -4997,8 +5435,8 @@
 			normalizeSublists(items[i]);
 		}
 
-		// "Block-extend the range, and let new range be the result."
-		var newRange = blockExtendRange(range);
+		// "Block-extend the active range, and let new range be the result."
+		var newRange = blockExtendRange(getActiveRange());
 
 		// "Let node list be a list of nodes, initially empty."
 		var nodeList = [];
@@ -5070,314 +5508,91 @@
 				fixDisallowedAncestors(sublist[i]);
 			}
 		}
-		break;
-
-		case "removeformat":
-		// "Decompose the range, and let node list be the result."
-		var nodeList = decomposeRange(range);
-
-		// "For each node in node list, unset the style attribute of node (if
-		// it's an Element) and then all its Element descendants."
-		for (var i = 0; i < nodeList.length; i++) {
-			for (
-				var node = nodeList[i];
-				node != nextNodeDescendants(nodeList[i]);
-				node = nextNode(node)
-			) {
-				if (node.nodeType == Node.ELEMENT_NODE) {
-					node.removeAttribute("style");
-				}
-			}
-		}
-
-		// "Let elements to remove be a list of all HTML elements that are the
-		// same as or descendants of some member of node list and have non-null
-		// parents and satisfy (insert conditions here)."
-		var elementsToRemove = [];
-		for (var i = 0; i < nodeList.length; i++) {
-			for (
-				var node = nodeList[i];
-				node == nodeList[i] || isDescendant(node, nodeList[i]);
-				node = nextNode(node)
-			) {
-				if (isHtmlElement(node)
-				&& node.parentNode
-				// FIXME: Extremely partial list for testing
-				&& ["A", "AUDIO", "BR", "DIV", "HR", "IMG", "P", "TD", "VIDEO", "WBR"].indexOf(node.tagName) == -1) {
-					elementsToRemove.push(node);
-				}
-			}
-		}
-
-		// "For each element in elements to remove:"
-		for (var i = 0; i < elementsToRemove.length; i++) {
-			var element = elementsToRemove[i];
-
-			// "While element has children, insert the first child of element
-			// into the parent of element immediately before element,
-			// preserving ranges."
-			while (element.childNodes.length) {
-				movePreservingRanges(element.firstChild, element.parentNode, getNodeIndex(element));
-			}
-
-			// "Remove element from its parent."
-			element.parentNode.removeChild(element);
-		}
-
-		// "For each of the entries in the following table, in the given order:
-		// decompose the range again; then set the value of the resulting
-		// nodes, with command and new value as given."
-		var table = {
-			"subscript": "baseline",
-			"bold": "normal",
-			"fontname": null,
-			"fontsize": null,
-			"forecolor": null,
-			"hilitecolor": null,
-			"italic": "normal",
-			"strikethrough": null,
-			"underline": null,
-		};
-		for (var command in table) {
-			var nodeList = decomposeRange(range);
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, table[command]);
-			}
-		}
-		break;
-
-		case "strikethrough":
-		// "Decompose the range. If the state of the range for this command is
-		// then true, set the value of each returned node to null. Otherwise,
-		// set their value to "line-through"."
-		var nodeList = decomposeRange(range);
-		var newValue = getState(command, range) ? null : "line-through";
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, newValue);
-		}
-		break;
-
-		case "stylewithcss":
-		// "Convert value to a boolean according to the algorithm in WebIDL,
-		// and set the CSS styling flag to the result."
-		cssStylingFlag = Boolean(value);
-		break;
-
-		case "subscript":
-		// "Decompose the range. If the state of the range for this command is
-		// then true, set the value of each returned node with new value
-		// "baseline". Otherwise, set their value with new value "baseline",
-		// then decompose the range again and set the value of each returned
-		// node with new value "sub"."
-		var nodeList = decomposeRange(range);
-		if (getState(command, range)) {
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, "baseline");
-			}
-		} else {
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, "baseline");
-			}
-			var nodeList = decomposeRange(range);
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, "sub");
-			}
-		}
-		break;
-
-		case "superscript":
-		// "Decompose the range. If the state of the range for this command is
-		// then true, set the value of each returned node with new value
-		// "baseline". Otherwise, set their value with new value "baseline",
-		// then decompose the range again and set the value of each returned
-		// node with new value "super"."
-		var nodeList = decomposeRange(range);
-		if (getState(command, range)) {
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, "baseline");
-			}
-		} else {
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, "baseline");
-			}
-			var nodeList = decomposeRange(range);
-			for (var i = 0; i < nodeList.length; i++) {
-				setNodeValue(nodeList[i], command, "super");
-			}
-		}
-		break;
-
-		case "underline":
-		// "Decompose the range. If the state of the range for this command is
-		// then true, set the value of each returned node to null. Otherwise,
-		// set their value to "underline"."
-		var nodeList = decomposeRange(range);
-		var newValue = getState("underline", range) ? null : "underline";
-		for (var i = 0; i < nodeList.length; i++) {
-			setNodeValue(nodeList[i], command, newValue);
-		}
-		break;
-
-		case "unlink":
-		// "Let hyperlinks be a list of every a element that has an href
-		// attribute and is contained in the range or is an ancestor of one of
-		// its boundary points."
-		//
-		// As usual, take care to ensure it's tree order.  The correctness of
-		// the following is left as an exercise for the reader.
-		var hyperlinks = [];
-		for (
-			var node = range.startContainer;
-			node;
-			node = node.parentNode
-		) {
-			if (isHtmlElement(node, "A")
-			&& node.hasAttribute("href")) {
-				hyperlinks.unshift(node);
-			}
-		}
-		for (
-			var node = range.startContainer;
-			node != nextNodeDescendants(range.endContainer);
-			node = nextNode(node)
-		) {
-			if (isHtmlElement(node, "A")
-			&& node.hasAttribute("href")
-			&& (isContained(node, range)
-			|| isAncestor(node, range.endContainer)
-			|| node == range.endContainer)) {
-				hyperlinks.push(node);
-			}
-		}
-
-		// "Clear the value of each member of hyperlinks."
-		for (var i = 0; i < hyperlinks.length; i++) {
-			clearValue(hyperlinks[i], command);
-		}
-		break;
-
-		case "usecss":
-		// "Convert value to a boolean according to the algorithm in WebIDL,
-		// and set the CSS styling flag to the negation of the result."
-		cssStylingFlag = !value;
-		break;
-
-		default:
-		break;
-	}
-}
-
-function myQueryCommandState(command) {
-	command = command.toLowerCase();
-
-	if (!getSelection().rangeCount) {
-		return false;
-	}
-
-	var range = getSelection().getRangeAt(0);
-
-	return getState(command, range);
-}
-
-function getState(command, range) {
-	if (command == "stylewithcss") {
-		return cssStylingFlag;
-	}
-
-	if (command != "bold"
-	&& command != "italic"
-	&& command != "strikethrough"
-	&& command != "underline"
-	&& command != "subscript"
-	&& command != "superscript") {
-		return false;
-	}
-
-	// XXX: This algorithm for getting all effectively contained nodes might be
-	// wrong . . .
-	var node = range.startContainer;
-	while (node.parentNode && node.parentNode.firstChild == node) {
-		node = node.parentNode;
-	}
-	var stop = nextNodeDescendants(range.endContainer);
-
-	for (; node && node != stop; node = nextNode(node)) {
-		if (!isEffectivelyContained(node, range)) {
-			continue;
-		}
-
-		if (node.nodeType != Node.TEXT_NODE) {
-			continue;
-		}
-
-		if (!isEditable(node)) {
-			continue;
-		}
-
-		if (command == "bold") {
-			// "True if every editable Text node that is effectively contained
-			// in the range has effective value at least 700. Otherwise false."
-			var fontWeight = getEffectiveValue(node, command);
-			if (fontWeight !== "bold"
-			&& fontWeight !== "700"
-			&& fontWeight !== "800"
-			&& fontWeight !== "900") {
-				return false;
-			}
-		} else if (command == "italic") {
-			// "True if every editable Text node that is effectively contained
-			// in the range has effective value either "italic" or "oblique".
-			// Otherwise false."
-			var fontStyle = getEffectiveValue(node, command);
-			if (fontStyle !== "italic"
-			&& fontStyle !== "oblique") {
-				return false;
-			}
-		} else if (command == "strikethrough") {
-			// "True if every editable Text node that is effectively contained
-			// in the range has effective value "line-through". Otherwise
-			// false."
-			var textDecoration = getEffectiveValue(node, command);
-			if (textDecoration !== "line-through") {
-				return false;
-			}
-		} else if (command == "underline") {
-			// "True if every editable Text node that is effectively contained
-			// in the range has effective value "underline". Otherwise false."
-			var textDecoration = getEffectiveValue(node, command);
-			if (textDecoration !== "underline") {
-				return false;
-			}
-		} else if (command == "subscript") {
-			// "True if every editable Text node that is effectively contained
-			// in the range has effective value "sub". Otherwise false."
-			var verticalAlign = getEffectiveValue(node, command);
-			if (verticalAlign !== "sub") {
-				return false;
-			}
-		} else if (command == "superscript") {
-			// "True if every editable Text node that is effectively contained
-			// in the range has effective value "super". Otherwise false."
-			var verticalAlign = getEffectiveValue(node, command);
-			if (verticalAlign !== "super") {
-				return false;
-			}
-		}
-	}
-
-	return true;
-}
-
-function myQueryCommandValue(command) {
-	command = command.toLowerCase();
-
-	if (!getSelection().rangeCount) {
-		return "";
-	}
-
-	var range = getSelection().getRangeAt(0);
-
-	return "";
-}
+	}
+};
+//@}
+
+
+//////////////////////////////////
+///// Miscellaneous commands /////
+//////////////////////////////////
+
+///// The selectAll command /////
+//@{
+commands.selectall = {
+	// Note, this ignores the whole globalRange/getActiveRange() thing and
+	// works with actual selections.  Not suitable for autoimplementation.html.
+	action: function() {
+		// "Let target be the body element of the context object."
+		var target = document.body;
+
+		// "If target is null, let target be the context object's
+		// documentElement."
+		if (!target) {
+			target = document.documentElement;
+		}
+
+		// "If target is null, let target be the context object."
+		if (!target) {
+			target = document;
+		}
+
+		// "Call getSelection() on the context object, and call
+		// selectAllChildren(target) on the result."
+		getSelection().selectAllChildren(target);
+	}
+};
+//@}
+
+///// The styleWithCSS command /////
+//@{
+commands.stylewithcss = {
+	action: function(value) {
+		// "If value is an ASCII case-insensitive match for the string
+		// "false", set the CSS styling flag to false. Otherwise, set the
+		// CSS styling flag to true."
+		cssStylingFlag = String(value).toLowerCase() != "false";
+	}, state: function() { return cssStylingFlag }
+};
+//@}
+
+///// The useCSS command /////
+//@{
+commands.usecss = {
+	action: function(value) {
+		// "If value is an ASCII case-insensitive match for the string "false",
+		// set the CSS styling flag to true. Otherwise, set the CSS styling
+		// flag to false."
+		cssStylingFlag = String(value).toLowerCase() == "false";
+	}
+};
+//@}
+
+
+// Done with command setup
+
+// "Commands may have an associated action, state, value, and/or relevant CSS
+// property. If not otherwise specified, the action for a command is to do
+// nothing, the state is false, the value is the empty string, and the relevant
+// CSS property is null."
+//
+// Don't dump the "command" variable into the global scope, it can cause bugs
+// because we have lots of local "command"s.
+(function() {
+	for (var command in commands) {
+		if (!("action" in commands[command])) {
+			commands[command].action = function() {};
+		}
+		if (!("state" in commands[command])) {
+			commands[command].state = function() { return false };
+		}
+		if (!("value" in commands[command])) {
+			commands[command].value = function() { return "" };
+		}
+		if (!("relevantCssProperty" in commands[command])) {
+			commands[command].relevantCssProperty = null;
+		}
+	}
+})();
 
 // vim: foldmarker=@{,@} foldmethod=marker
--- a/source.html	Tue Jun 14 14:53:50 2011 -0600
+++ b/source.html	Wed Jun 15 12:33:24 2011 -0600
@@ -294,6 +294,14 @@
 in Firefox 4b11.  It returns boolean false in Chrome 10, and the empty string
 in Opera 11. -->
 
+<!-- We have lots of options for the default value of commands.  Using bold as
+an example, IE 9 RC returns the boolean false, Firefox 4b11 and Opera 11 both
+return the empty string, Chrome 10 returns the string "false".  The HTML5 spec
+as of February 2011 mandates WebKit's behavior.  It makes sense to always
+return a string, a majority of string-returners return the empty string, and
+three out of the four return something that evaluates to false as a boolean, so
+I'll go with Firefox and Opera. -->
+
 <p>When <code>execCommand()</code> is invoked, the user agent must follow the
 <span>action</span> instructions given in this specification for
 <var>command</var>, with <var>showUI</var> and <var>value</var> passed to the
@@ -2394,15 +2402,6 @@
 general idea as the spec, considering a range bold only if all text in it is
 bold, and this seems to match at least OpenOffice.org's bold feature. -->
 
-<p><span>Value</span>: Always the empty string.
-<!-- We have lots of options here (and presumably for all the others where
-value is meaningless).  IE 9 RC returns the boolean false, Firefox 4b11 and
-Opera 11 both return the empty string, Chrome 10 returns the string "false".
-The HTML5 spec as of February 2011 mandates WebKit's behavior.  It makes sense
-to always return a string, a majority of string-returners return the empty
-string, and three out of the four return something that evaluates to false as a
-boolean, so I'll go with Firefox and Opera. -->
-
 <p><span>Relevant CSS property</span>: "font-weight"
 <!-- @} -->
 
@@ -2458,10 +2457,8 @@
   <var>value</var>.
 </ol>
 
-<p><span>State</span>: Always false.
-
-<p><span>Value</span>: Always the empty string.
-<!-- I'd have expected the value to be the URL, but guess not. -->
+<!-- I'd have expected the value to be the URL, but guess not: it's always
+false. -->
 <!-- @} -->
 
 <h3><dfn>The <code title>fontName</code> command</dfn></h3>
@@ -2489,8 +2486,6 @@
 understand CSS font-family syntax?), so I don't think such usability concerns
 apply. -->
 
-<p><span>State</span>: Always false.
-
 <p><span>Value</span>: The computed value of the CSS property "font-family" for
 . . .
 <!-- Complicated.
@@ -2692,12 +2687,11 @@
   value</span> of each returned [[node]] to <var>value</var>.
 </ol>
 
-<p><span>State</span>: Always false.
-<!-- This matches IE 9 RC and Chrome 10.  Opera 11 seems to return true if
-there's some color style applied, false otherwise, which seems fairly useless;
-authors want to use value here, not state.  Firefox 4b11 throws an exception,
-which is an interesting approach, but I'll go with IE/WebKit, which makes at
-least as much sense. -->
+<!-- The state is undefined, thus always false.  This matches IE 9 RC and
+Chrome 10.  Opera 11 seems to return true if there's some color style applied,
+false otherwise, which seems fairly useless; authors want to use value here,
+not state.  Firefox 4b11 throws an exception, which is an interesting approach,
+but I'll go with IE/WebKit, which makes at least as much sense. -->
 
 <p><span>Value</span>: ?
 <!-- IE 9 RC returns the number 0 always, which makes no sense at all. -->
@@ -2740,8 +2734,6 @@
   value</span> of each returned [[node]] to <var>value</var>.
 </ol>
 
-<p><span>State</span>: Always false.
-
 <p><span>Relevant CSS property</span>: "background-color"
 <!-- @} -->
 
@@ -2792,7 +2784,7 @@
 
   <li>Let <var>range</var> be the <span>active range</span>.
 
-  <li><span>Delete the contents</span> of the <span>active range</span>.
+  <li><span>Delete the contents</span> of <var>range</var>.
 
   <li>Let <var>img</var> be the result of calling <code
   data-anolis-spec=domcore
@@ -2829,8 +2821,6 @@
 is <span>effectively contained</span> in the <span>active range</span> has
 <span>effective value</span> either "italic" or "oblique".  Otherwise false.
 
-<p><span>Value</span>: Always the empty string.
-
 <p><span>Relevant CSS property</span>: "font-style"
 <!-- @} -->
 
@@ -2969,8 +2959,6 @@
 <p><span>State</span>: True if every <span>editable</span> [[text]] node that
 is <span>effectively contained</span> in the <span>active range</span> has
 <span>effective value</span> "line-through".  Otherwise false.
-
-<p><span>Value</span>: Always the empty string.
 <!-- @} -->
 
 <h3><dfn>The <code title>subscript</code> command</dfn></h3>
@@ -3083,8 +3071,6 @@
 <p><span>State</span>: True if every <span>editable</span> [[text]] node that
 is <span>effectively contained</span> in the <span>active range</span> has
 <span>effective value</span> "underline".  Otherwise false.
-
-<p><span>Value</span>: Always the empty string.
 <!-- @} -->
 
 <h3><dfn>The <code title>unlink</code> command</dfn></h3>
@@ -3106,10 +3092,6 @@
 
   <li><span>Clear the value</span> of each member of <var>hyperlinks</var>.
 </ol>
-
-<p><span>State</span>: Always false.
-
-<p><span>Value</span>: Always the empty string.
 <!-- @} -->
 
 <h2>Block formatting commands</h2>
--- a/tests.js	Tue Jun 14 14:53:50 2011 -0600
+++ b/tests.js	Wed Jun 15 12:33:24 2011 -0600
@@ -1,12 +1,14 @@
 // Insert the toolbar thingie as soon as the script file is loaded
-var toolbarDiv = document.createElement("div");
-toolbarDiv.id = "toolbar";
-// Note: this is completely not a hack at all.
-toolbarDiv.innerHTML = "<style id=alerts>/* body > div > table > tbody > tr:not(.alert):not(:first-child) { display: none } */</style>"
-	+ "<label><input id=alert-checkbox type=checkbox accesskey=a checked onclick='updateAlertRowStyle()'> Display rows without spec <u>a</u>lerts</label>"
-	+ "<label><input id=browser-checkbox type=checkbox accesskey=b checked onclick='localStorage[\"display-browser-tests\"] = event.target.checked'> Run <u>b</u>rowser tests as well as spec tests</label>";
+(function() {
+	var toolbarDiv = document.createElement("div");
+	toolbarDiv.id = "toolbar";
+	// Note: this is completely not a hack at all.
+	toolbarDiv.innerHTML = "<style id=alerts>/* body > div > table > tbody > tr:not(.alert):not(:first-child) { display: none } */</style>"
+		+ "<label><input id=alert-checkbox type=checkbox accesskey=a checked onclick='updateAlertRowStyle()'> Display rows without spec <u>a</u>lerts</label>"
+		+ "<label><input id=browser-checkbox type=checkbox accesskey=b checked onclick='localStorage[\"display-browser-tests\"] = event.target.checked'> Run <u>b</u>rowser tests as well as spec tests</label>";
 
-document.body.appendChild(toolbarDiv);
+	document.body.appendChild(toolbarDiv);
+})();
 
 // Confusingly, we're storing a string here, not a boolean.
 document.querySelector("#alert-checkbox").checked = localStorage["display-alerts"] != "false";