Commit a whole bunch more stuff
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Thu, 14 Jul 2011 15:40:44 -0600
changeset 430 940a2b6441d3
parent 429 e0a8c4f04335
child 431 d1545a64f831
Commit a whole bunch more stuff

The theoretical reason for this commit was to support state and value
for createLink and unlink. Other stuff that happened along the way was
improving valuesEqual, fixing a formatBlock bug while also making it
more readable, making my list of invariants more correct, and making the
thing that outputs the indeterm/state/value work a lot better.

I wanted to use git add -i to break this up into a bunch of unrelated
commits, because this kind of big messy commit gets on my nerves, but
nobody's likely to be reviewing these in much detail, so it's really not
worth the time . . .
editcommands.html
implementation.js
source.html
tests.js
--- a/editcommands.html	Thu Jul 14 14:27:17 2011 -0600
+++ b/editcommands.html	Thu Jul 14 15:40:44 2011 -0600
@@ -686,8 +686,8 @@
 
 <div class=note>
 <p>The methods in this section have mostly been designed so that the following
-invariants hold after <code><a href=#execcommand()>execCommand()</a></code> is called, assuming it didn't
-throw an exception:
+invariants hold after <code title="">execCommand()</code> is called, assuming it
+didn't throw an exception:
 
 <ul>
   <li><code title="">queryCommandIndeterm()</code> will return false (or throw an
@@ -705,11 +705,12 @@
 <p>The first two points do not always hold for <code title="">strikethrough</code>
 or <code title="">underline</code>, because it can be impossible to unset
 text-decoration in CSS.  Also, by design, the state of <code title="">insertOrderedList</code> and <code title="">insertOrderedList</code> might
-not be the opposite after calling as before calling.  Finally, the state of the
-various <code title="">justify</code> commands should always be true after
-calling, and the value should always be the appropriate string ("center",
-"justify", "left", or "right").  Any other deviations from these invariants are
-bugs in the specification.
+be true both before and after calling, because they only remove one level of
+indentation.  <code title="">unlink</code> should set the value to null.  And
+finally, the state of the various <code title="">justify</code> commands should
+always be true after calling, and the value should always be the appropriate
+string ("center", "justify", "left", or "right").  Any other deviations from
+these invariants are bugs in the specification.
 </div>
 
 
@@ -2510,7 +2511,23 @@
   <li><a href="#set-the-selection's-value">Set the selection's value</a> to <var title="">value</var>.
 </ol>
 
-<p class=XXX>Define state and value, although browsers don't.
+<!-- IE10PP2, Firefox 7.0a2, Chrome 14 dev, and Opera 11.50 all do not support
+indeterminate, state, or value for createLink or unlink.  I define
+indeterminate and value anyway because they make sense. -->
+<p><a href=#indeterminate>Indeterminate</a>: True if among <a href=#editable>editable</a> <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#text>Text</a></code>
+nodes that are <a href=#effectively-contained>effectively contained</a> in the <a href=#active-range>active
+range</a>, there are two that have distinct <a href=#effective-command-value title="effective command
+value">effective command values</a>.  Otherwise false.
+
+<p><a href=#value>Value</a>: The <a href=#effective-command-value>effective command value</a> of the first
+<a href=#editable>editable</a> <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#text>Text</a></code> node that is <a href=#effectively-contained>effectively contained</a>
+in the <a href=#active-range>active range</a>, or if there is no such node, the
+<a href=#effective-command-value>effective command value</a> of the <a href=#active-range>active range</a>'s
+<a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range-start title=concept-range-start>start</a> <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-boundary-point-node title=concept-boundary-point-node>node</a>.
+
+<p class=note>The effective command value of the active range's start node
+cannot be null, since the boundary point node of a selection must always be
+either an element or a text node that's the child of an element.
 
 
 <h3 id=the-fontname-command><span class=secno>7.10 </span><dfn>The <code title="">fontName</code> command</dfn></h3>
@@ -3254,6 +3271,25 @@
   <li><a href=#clear-the-value>Clear the value</a> of each member of <var title="">hyperlinks</var>.
 </ol>
 
+<!-- IE10PP2, Firefox 7.0a2, Chrome 14 dev, and Opera 11.50 all do not support
+indeterminate, state, or value for createLink or unlink.  I define
+indeterminate and value anyway because they make sense. -->
+<p><a href=#indeterminate>Indeterminate</a>: True if among <a href=#editable>editable</a> <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#text>Text</a></code>
+nodes that are <a href=#effectively-contained>effectively contained</a> in the <a href=#active-range>active
+range</a>, there are two that have distinct <a href=#effective-command-value title="effective command
+value">effective command values</a>.  Otherwise false.
+
+<p><a href=#value>Value</a>: The <a href=#effective-command-value>effective command value</a> of the first
+<a href=#editable>editable</a> <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#text>Text</a></code> node that is <a href=#effectively-contained>effectively contained</a>
+in the <a href=#active-range>active range</a>, or if there is no such node, the
+<a href=#effective-command-value>effective command value</a> of the <a href=#active-range>active range</a>'s
+<a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range-start title=concept-range-start>start</a> <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-boundary-point-node title=concept-boundary-point-node>node</a>.
+
+<p class=note>The effective command value of the active range's start node
+cannot be null, since the boundary point node of a selection must always be
+either an element or a text node that's the child of an element.
+
+
 
 <h2 id=block-formatting-commands><span class=secno>8 </span>Block formatting commands</h2>
 
@@ -5677,6 +5713,9 @@
 sense for <address>/<h*>/<pre>, but other browsers don't do it.
 -->
 
+<p>A <dfn id=formattable-block-name>formattable block name</dfn> is "address", "dd", "div", "dt", "h1",
+"h2", "h3", "h4", "h5", "h6", "p", or "pre".
+
 <p><a href=#action>Action</a>:
 
 <ol>
@@ -5688,8 +5727,8 @@
   <li>Let <var title="">value</var> be <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#converted-to-ascii-lowercase>converted to
   ASCII lowercase</a>.
 
-  <li>If <var title="">value</var> is not "address", "dd", "div", "dt", "h1", "h2",
-  "h3", "h4", "h5", "h6", "p", or "pre", raise a <code class=external data-anolis-spec=domcore title=dom-DOMException-SYNTAX_ERR><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-domexception-syntax_err>SYNTAX_ERR</a></code> exception.
+  <li>If <var title="">value</var> is not a <a href=#formattable-block-name>formattable block name</a>, raise a
+  <code class=external data-anolis-spec=domcore title=dom-DOMException-SYNTAX_ERR><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-domexception-syntax_err>SYNTAX_ERR</a></code> exception.
   <!--
   Opera 11.10 throws NOT_SUPPORTED_ERR for bad elements, all other tested
   browsers ignore the input.  Testing in IE9, Firefox 4.0, Chrome 13 dev, and
@@ -5728,11 +5767,10 @@
 
   <li>For each <var title="">node</var> in <var title="">node list</var>, while <var title="">node</var>
   is the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-descendant title=concept-tree-descendant>descendant</a> of an <a href=#editable>editable</a> <a href=#html-element>HTML element</a>
-  <a href=#in-the-same-editing-host>in the same editing host</a>, which has <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> "address",
-  "dd", "div", "dt", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", and
-  which is not the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of a <a href=#prohibited-paragraph-child>prohibited paragraph child</a>,
-  <a href=#split-the-parent>split the parent</a> of the one-<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> list consisting of
-  <var title="">node</var>.
+  <a href=#in-the-same-editing-host>in the same editing host</a>, whose <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> is a
+  <a href=#formattable-block-name>formattable block name</a>, and which is not the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of a
+  <a href=#prohibited-paragraph-child>prohibited paragraph child</a>, <a href=#split-the-parent>split the parent</a> of the
+  one-<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> list consisting of <var title="">node</var>.
   <!--
   This tries to avoid misnesting if only some lines of an element are selected,
   so <h1>[foo]<br>bar</h1> becomes <p>[foo]</p><h1>bar</h1> instead of
@@ -5867,15 +5905,15 @@
   <ol>
     <li>While <var title="">node</var>'s <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> is <a href=#editable>editable</a> and
     <a href=#in-the-same-editing-host>in the same editing host</a> as <var title="">node</var>, and
-    <var title="">node</var> is not an <a href=#html-element>HTML element</a> with <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a>
-    "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", set
-    <var title="">node</var> to its <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>.
+    <var title="">node</var> is not an <a href=#html-element>HTML element</a> whose <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a>
+    is a <a href=#formattable-block-name>formattable block name</a>, set <var title="">node</var> to its
+    <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>.
 
     <li>Let <var title="">current type</var> be the empty string.
 
     <li>If <var title="">node</var> is an <a href=#editable>editable</a> <a href=#html-element>HTML
-    element</a> with <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> "address", "div", "h1", "h2", "h3", "h4",
-    "h5", "h6", "p", or "pre", and <var title="">node</var> is not the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of a
+    element</a> whose <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> is a <a href=#formattable-block-name>formattable block
+    name</a>, and <var title="">node</var> is not the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of a
     <a href=#prohibited-paragraph-child>prohibited paragraph child</a>, set <var title="">current type</var> to
     <var title="">node</var>'s <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a>.
 
@@ -5915,15 +5953,15 @@
 
   <li>While <var title="">node</var>'s <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> is <a href=#editable>editable</a> and <a href=#in-the-same-editing-host>in
   the same editing host</a> as <var title="">node</var>, and <var title="">node</var> is not
-  an <a href=#html-element>HTML element</a> with <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> "address", "div", "h1", "h2",
-  "h3", "h4", "h5", "h6", "p", or "pre", set <var title="">node</var> to its <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>.
+  an <a href=#html-element>HTML element</a> whose <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> is a <a href=#formattable-block-name>formattable block
+  name</a>, set <var title="">node</var> to its <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>.
   <!-- Opera 11.11 doesn't require it be editable, so it will return "DIV"
   instead of "" for <div contenteditable>foo</div>.  -->
 
   <li>If <var title="">node</var> is an <a href=#editable>editable</a> <a href=#html-element>HTML element</a>
-  with <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p",
-  or "pre", and <var title="">node</var> is not the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of a <a href=#prohibited-paragraph-child>prohibited
-  paragraph child</a>, return <var title="">node</var>'s <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a>, <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#converted-to-ascii-lowercase>converted to ASCII lowercase</a>.
+  whose <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a> is a <a href=#formattable-block-name>formattable block name</a>, and
+  <var title="">node</var> is not the <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-ancestor title=concept-tree-ancestor>ancestor</a> of a <a href=#prohibited-paragraph-child>prohibited paragraph
+  child</a>, return <var title="">node</var>'s <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-element-local-name title=concept-element-local-name>local name</a>, <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#converted-to-ascii-lowercase>converted to ASCII lowercase</a>.
   <!--
   Chrome 14 dev will report "div" for <div><ol><li>foo</ol></div> or such.
   Opera 11.11 reports "".  IE and Firefox didn't cooperate with testing.  Opera
--- a/implementation.js	Thu Jul 14 14:27:17 2011 -0600
+++ b/implementation.js	Thu Jul 14 15:40:44 2011 -0600
@@ -134,6 +134,10 @@
 // test implementation.  It's not clear how all this should actually be specced
 // in practice, since CSS defines no notion of equality, does it?
 function valuesEqual(command, val1, val2) {
+	if (commands[command].relevantCssProperty === null) {
+		return val1 === val2;
+	}
+
 	if (val1 === null || val2 === null) {
 		return val1 === val2;
 	}
@@ -150,15 +154,6 @@
 			|| (val2.toLowerCase() == "normal" && val1 == "400");
 	}
 
-	// This code path should probably only be hit by queryOutputHelper() in
-	// tests.js.  Anything else is most likely a bug.
-	if (command == "fontname" && /^[1-7]$/.test(val1)) {
-		val1 = [, "xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"][val1];
-	}
-	if (command == "fontname" && /^[1-7]$/.test(val2)) {
-		val2 = [, "xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"][val2];
-	}
-
 	var property = commands[command].relevantCssProperty;
 	var test1 = document.createElement("span");
 	test1.style[property] = val1;
@@ -3065,6 +3060,28 @@
 
 		// "Set the selection's value to value."
 		setSelectionValue("createlink", value);
+	}, indeterm: function() {
+		// "True if among editable Text nodes that are effectively contained in
+		// the active range, there are two that have distinct effective command
+		// values.  Otherwise false."
+		return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
+			return isEditable(node) && node.nodeType == Node.TEXT_NODE;
+		}).map(function(node) {
+			return getEffectiveCommandValue(node, "createlink");
+		}).filter(function(value, i, arr) {
+			return arr.slice(0, i).indexOf(value) == -1;
+		}).length >= 2;
+	}, value: function() {
+		// "The effective command value of the first editable Text node that is
+		// effectively contained in the active range, or if there is no such
+		// node, the effective command value of the active range's start node."
+		var node = getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
+			return isEditable(node) && node.nodeType == Node.TEXT_NODE;
+		})[0];
+		if (node === undefined) {
+			node = getActiveRange().startContainer;
+		}
+		return getEffectiveCommandValue(node, "createlink");
 	}
 };
 
@@ -3681,6 +3698,28 @@
 		for (var i = 0; i < hyperlinks.length; i++) {
 			clearValue(hyperlinks[i], "unlink");
 		}
+	}, indeterm: function() {
+		// "True if among editable Text nodes that are effectively contained in
+		// the active range, there are two that have distinct effective command
+		// values.  Otherwise false."
+		return getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
+			return isEditable(node) && node.nodeType == Node.TEXT_NODE;
+		}).map(function(node) {
+			return getEffectiveCommandValue(node, "unlink");
+		}).filter(function(value, i, arr) {
+			return arr.slice(0, i).indexOf(value) == -1;
+		}).length >= 2;
+	}, value: function() {
+		// "The effective command value of the first editable Text node that is
+		// effectively contained in the active range, or if there is no such
+		// node, the effective command value of the active range's start node."
+		var node = getAllEffectivelyContainedNodes(getActiveRange(), function(node) {
+			return isEditable(node) && node.nodeType == Node.TEXT_NODE;
+		})[0];
+		if (node === undefined) {
+			node = getActiveRange().startContainer;
+		}
+		return getEffectiveCommandValue(node, "unlink");
 	}
 };
 
@@ -5967,6 +6006,11 @@
 //@}
 ///// The formatBlock command /////
 //@{
+// "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3",
+// "h4", "h5", "h6", "p", or "pre"."
+var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3",
+	"h4", "h5", "h6", "p", "pre"];
+
 commands.formatblock = {
 	action: function(value) {
 		// "If value begins with a "<" character and ends with a ">" character,
@@ -5978,10 +6022,9 @@
 		// "Let value be converted to ASCII lowercase."
 		value = value.toLowerCase();
 
-		// "If value is not "address", "dd", "div", "dt", "h1", "h2", "h3",
-		// "h4", "h5", "h6", "p", or "pre", raise a SYNTAX_ERR exception."
-		if (["address", "dd", "div", "dt", "h1", "h2", "h3", "h4", "h5", "h6",
-		"p", "pre"].indexOf(value) == -1) {
+		// "If value is not a formattable block name, raise a SYNTAX_ERR
+		// exception."
+		if (formattableBlockNames.indexOf(value) == -1) {
 			throw "SYNTAX_ERR";
 		}
 
@@ -6007,17 +6050,16 @@
 		var values = recordValues(nodeList);
 
 		// "For each node in node list, while node is the descendant of an
-		// editable HTML element in the same editing host, which has local name
-		// "address", "dd", "div", "dt", "h1", "h2", "h3", "h4", "h5", "h6",
-		// "p", or "pre", and which is not the ancestor of a prohibited
-		// paragraph child, split the parent of the one-node list consisting of
-		// node."
+		// editable HTML element in the same editing host, whose local name is
+		// a formattable block name, and which is not the ancestor of a
+		// prohibited paragraph child, split the parent of the one-node list
+		// consisting of node."
 		for (var i = 0; i < nodeList.length; i++) {
 			var node = nodeList[i];
 			while (getAncestors(node).some(function(ancestor) {
 				return isEditable(ancestor)
 					&& inSameEditingHost(ancestor, node)
-					&& isHtmlElement(ancestor, ["address", "dd", "div", "dt", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre"])
+					&& isHtmlElement(ancestor, formattableBlockNames)
 					&& !getDescendants(ancestor).some(isProhibitedParagraphChild);
 			})) {
 				splitParent([node]);
@@ -6111,24 +6153,23 @@
 			var node = nodeList[i];
 
 			// "While node's parent is editable and in the same editing host as
-			// node, and node is not an HTML element with local name "address",
-			// "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", set
-			// node to its parent."
+			// node, and node is not an HTML element whose local name is a
+			// formattable block name, set node to its parent."
 			while (isEditable(node.parentNode)
 			&& inSameEditingHost(node, node.parentNode)
-			&& !isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre"])) {
+			&& !isHtmlElement(node, formattableBlockNames)) {
 				node = node.parentNode;
 			}
 
 			// "Let current type be the empty string."
 			var currentType = "";
 
-			// "If node is an editable HTML element with local name "address",
-			// "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", and
-			// node is not the ancestor of a prohibited paragraph child, set
-			// current type to node's local name."
+			// "If node is an editable HTML element whose local name is a
+			// formattable block name, and node is not the ancestor of a
+			// prohibited paragraph child, set current type to node's local
+			// name."
 			if (isEditable(node)
-			&& isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre"])
+			&& isHtmlElement(node, formattableBlockNames)
 			&& !getDescendants(node).some(isProhibitedParagraphChild)) {
 				currentType = node.tagName;
 			}
@@ -6163,21 +6204,20 @@
 		var node = nodes[0];
 
 		// "While node's parent is editable and in the same editing host as
-		// node, and node is not an HTML element with local name "address",
-		// "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", set node
-		// to its parent."
+		// node, and node is not an HTML element whose local name is a
+		// formattable block name, set node to its parent."
 		while (isEditable(node.parentNode)
 		&& inSameEditingHost(node, node.parentNode)
-		&& !isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre"])) {
+		&& !isHtmlElement(node, formattableBlockNames)) {
 			node = node.parentNode;
 		}
 
-		// "If node is an editable HTML element with local name "address",
-		// "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", and node
-		// is not the ancestor of a prohibited paragraph child, return node's
-		// local name, converted to ASCII lowercase."
+		// "If node is an editable HTML element whose local name is a
+		// formattable block name, and node is not the ancestor of a prohibited
+		// paragraph child, return node's local name, converted to ASCII
+		// lowercase."
 		if (isEditable(node)
-		&& isHtmlElement(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre"])
+		&& isHtmlElement(node, formattableBlockNames)
 		&& !getDescendants(node).some(isProhibitedParagraphChild)) {
 			return node.tagName.toLowerCase();
 		}
--- a/source.html	Thu Jul 14 14:27:17 2011 -0600
+++ b/source.html	Thu Jul 14 15:40:44 2011 -0600
@@ -635,8 +635,8 @@
 
 <div class=note>
 <p>The methods in this section have mostly been designed so that the following
-invariants hold after <code>execCommand()</code> is called, assuming it didn't
-throw an exception:
+invariants hold after <code title>execCommand()</code> is called, assuming it
+didn't throw an exception:
 
 <ul>
   <li><code title>queryCommandIndeterm()</code> will return false (or throw an
@@ -655,11 +655,12 @@
 or <code title>underline</code>, because it can be impossible to unset
 text-decoration in CSS.  Also, by design, the state of <code
 title>insertOrderedList</code> and <code title>insertOrderedList</code> might
-not be the opposite after calling as before calling.  Finally, the state of the
-various <code title>justify</code> commands should always be true after
-calling, and the value should always be the appropriate string ("center",
-"justify", "left", or "right").  Any other deviations from these invariants are
-bugs in the specification.
+be true both before and after calling, because they only remove one level of
+indentation.  <code title>unlink</code> should set the value to null.  And
+finally, the state of the various <code title>justify</code> commands should
+always be true after calling, and the value should always be the appropriate
+string ("center", "justify", "left", or "right").  Any other deviations from
+these invariants are bugs in the specification.
 </div>
 <!-- @} -->
 
@@ -2490,7 +2491,23 @@
   <li><span>Set the selection's value</span> to <var>value</var>.
 </ol>
 
-<p class=XXX>Define state and value, although browsers don't.
+<!-- IE10PP2, Firefox 7.0a2, Chrome 14 dev, and Opera 11.50 all do not support
+indeterminate, state, or value for createLink or unlink.  I define
+indeterminate and value anyway because they make sense. -->
+<p><span>Indeterminate</span>: True if among <span>editable</span> [[text]]
+nodes that are <span>effectively contained</span> in the <span>active
+range</span>, there are two that have distinct <span title="effective command
+value">effective command values</span>.  Otherwise false.
+
+<p><span>Value</span>: The <span>effective command value</span> of the first
+<span>editable</span> [[text]] node that is <span>effectively contained</span>
+in the <span>active range</span>, or if there is no such node, the
+<span>effective command value</span> of the <span>active range</span>'s
+[[startnode]].
+
+<p class=note>The effective command value of the active range's start node
+cannot be null, since the boundary point node of a selection must always be
+either an element or a text node that's the child of an element.
 
 <!-- @} -->
 <h3><dfn>The <code title>fontName</code> command</dfn></h3>
@@ -3238,6 +3255,25 @@
 
   <li><span>Clear the value</span> of each member of <var>hyperlinks</var>.
 </ol>
+
+<!-- IE10PP2, Firefox 7.0a2, Chrome 14 dev, and Opera 11.50 all do not support
+indeterminate, state, or value for createLink or unlink.  I define
+indeterminate and value anyway because they make sense. -->
+<p><span>Indeterminate</span>: True if among <span>editable</span> [[text]]
+nodes that are <span>effectively contained</span> in the <span>active
+range</span>, there are two that have distinct <span title="effective command
+value">effective command values</span>.  Otherwise false.
+
+<p><span>Value</span>: The <span>effective command value</span> of the first
+<span>editable</span> [[text]] node that is <span>effectively contained</span>
+in the <span>active range</span>, or if there is no such node, the
+<span>effective command value</span> of the <span>active range</span>'s
+[[startnode]].
+
+<p class=note>The effective command value of the active range's start node
+cannot be null, since the boundary point node of a selection must always be
+either an element or a text node that's the child of an element.
+
 <!-- @} -->
 
 <h2>Block formatting commands</h2>
@@ -5676,6 +5712,9 @@
 sense for <address>/<h*>/<pre>, but other browsers don't do it.
 -->
 
+<p>A <dfn>formattable block name</dfn> is "address", "dd", "div", "dt", "h1",
+"h2", "h3", "h4", "h5", "h6", "p", or "pre".
+
 <p><span>Action</span>:
 
 <ol>
@@ -5687,8 +5726,8 @@
   <li>Let <var>value</var> be <span data-anolis-spec=domcore>converted to
   ASCII lowercase</span>.
 
-  <li>If <var>value</var> is not "address", "dd", "div", "dt", "h1", "h2",
-  "h3", "h4", "h5", "h6", "p", or "pre", raise a [[SYNTAX_ERR]] exception.
+  <li>If <var>value</var> is not a <span>formattable block name</span>, raise a
+  [[SYNTAX_ERR]] exception.
   <!--
   Opera 11.10 throws NOT_SUPPORTED_ERR for bad elements, all other tested
   browsers ignore the input.  Testing in IE9, Firefox 4.0, Chrome 13 dev, and
@@ -5727,11 +5766,10 @@
 
   <li>For each <var>node</var> in <var>node list</var>, while <var>node</var>
   is the [[descendant]] of an <span>editable</span> <span>HTML element</span>
-  <span>in the same editing host</span>, which has [[localname]] "address",
-  "dd", "div", "dt", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", and
-  which is not the [[ancestor]] of a <span>prohibited paragraph child</span>,
-  <span>split the parent</span> of the one-[[node]] list consisting of
-  <var>node</var>.
+  <span>in the same editing host</span>, whose [[localname]] is a
+  <span>formattable block name</span>, and which is not the [[ancestor]] of a
+  <span>prohibited paragraph child</span>, <span>split the parent</span> of the
+  one-[[node]] list consisting of <var>node</var>.
   <!--
   This tries to avoid misnesting if only some lines of an element are selected,
   so <h1>[foo]<br>bar</h1> becomes <p>[foo]</p><h1>bar</h1> instead of
@@ -5866,15 +5904,15 @@
   <ol>
     <li>While <var>node</var>'s [[parent]] is <span>editable</span> and
     <span>in the same editing host</span> as <var>node</var>, and
-    <var>node</var> is not an <span>HTML element</span> with [[localname]]
-    "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", or "pre", set
-    <var>node</var> to its [[parent]].
+    <var>node</var> is not an <span>HTML element</span> whose [[localname]]
+    is a <span>formattable block name</span>, set <var>node</var> to its
+    [[parent]].
 
     <li>Let <var>current type</var> be the empty string.
 
     <li>If <var>node</var> is an <span>editable</span> <span>HTML
-    element</span> with [[localname]] "address", "div", "h1", "h2", "h3", "h4",
-    "h5", "h6", "p", or "pre", and <var>node</var> is not the [[ancestor]] of a
+    element</span> whose [[localname]] is a <span>formattable block
+    name</span>, and <var>node</var> is not the [[ancestor]] of a
     <span>prohibited paragraph child</span>, set <var>current type</var> to
     <var>node</var>'s [[localname]].
 
@@ -5914,15 +5952,15 @@
 
   <li>While <var>node</var>'s [[parent]] is <span>editable</span> and <span>in
   the same editing host</span> as <var>node</var>, and <var>node</var> is not
-  an <span>HTML element</span> with [[localname]] "address", "div", "h1", "h2",
-  "h3", "h4", "h5", "h6", "p", or "pre", set <var>node</var> to its [[parent]].
+  an <span>HTML element</span> whose [[localname]] is a <span>formattable block
+  name</span>, set <var>node</var> to its [[parent]].
   <!-- Opera 11.11 doesn't require it be editable, so it will return "DIV"
   instead of "" for <div contenteditable>foo</div>.  -->
 
   <li>If <var>node</var> is an <span>editable</span> <span>HTML element</span>
-  with [[localname]] "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p",
-  or "pre", and <var>node</var> is not the [[ancestor]] of a <span>prohibited
-  paragraph child</span>, return <var>node</var>'s [[localname]], <span
+  whose [[localname]] is a <span>formattable block name</span>, and
+  <var>node</var> is not the [[ancestor]] of a <span>prohibited paragraph
+  child</span>, return <var>node</var>'s [[localname]], <span
   data-anolis-spec=domcore>converted to ASCII lowercase</span>.
   <!--
   Chrome 14 dev will report "div" for <div><ol><li>foo</ol></div> or such.
--- a/tests.js	Thu Jul 14 14:27:17 2011 -0600
+++ b/tests.js	Thu Jul 14 15:40:44 2011 -0600
@@ -3393,6 +3393,13 @@
 }
 //@}
 
+function trivialPrettyPrint(value) {
+	if (typeof value == "string") {
+		return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
+	}
+	return value;
+}
+
 function queryOutputHelper(beforeIndeterm, beforeState, beforeValue, afterIndeterm, afterState, afterValue, command, value) {
 //@{
 	var frag = document.createDocumentFragment();
@@ -3406,46 +3413,87 @@
 
 	beforeDiv.appendChild(document.createElement("span"));
 	afterDiv.appendChild(document.createElement("span"));
-	if (afterIndeterm !== "Exception") {
+	if ("indeterm" in commands[command]) {
+		// We only know it has to be either true or false.
+		if (beforeIndeterm !== true && beforeIndeterm !== false) {
+			beforeDiv.lastChild.className = "bad-result";
+		}
+	} else {
+		// It always has to be an exception.
+		beforeDiv.lastChild.className = beforeIndeterm === "Exception"
+			? "good-result"
+			: "bad-result";
+	}
+	// After running the command, indeterminate must always be false, except if
+	// it's an exception, or if it's insert*list and the state was true to
+	// begin with.
+	if (/^insert(un)?orderedlist$/.test(command) && beforeState) {
+		if (afterIndeterm !== true && afterIndeterm !== false) {
+			afterDiv.lastChild.className = "bad-result";
+		}
+	} else {
 		afterDiv.lastChild.className =
-			!afterIndeterm
+			("indeterm" in commands[command] && afterIndeterm === false)
+			|| (!("indeterm" in commands[command]) && afterIndeterm === "Exception")
 				? "good-result"
 				: "bad-result";
 	}
-	beforeDiv.lastChild.textContent = "indeterm " + beforeIndeterm;
-	afterDiv.lastChild.textContent = "indeterm " + afterIndeterm;
+	beforeDiv.lastChild.textContent = "indeterm " + trivialPrettyPrint(beforeIndeterm);
+	afterDiv.lastChild.textContent = "indeterm " + trivialPrettyPrint(afterIndeterm);
 
 	beforeDiv.appendChild(document.createTextNode(", "));
 	afterDiv.appendChild(document.createTextNode(", "));
 
 	beforeDiv.appendChild(document.createElement("span"));
 	afterDiv.appendChild(document.createElement("span"));
-	if ((beforeState !== "Exception" || afterState !== "Exception")
-	&& !/insert(un)?orderedlist|justify(center|full|left|right)/.test(command)) {
+	if (/^insert(un)?orderedlist$/.test(command)) {
+		// If the before state is true, the after state could be either true or
+		// false.  But if the before state is false, the after state has to be
+		// true.
+		if (beforeState !== true && beforeState !== false) {
+			beforeDiv.lastChild.className = "bad-result";
+		}
+		if (!beforeState) {
+			afterDiv.lastChild.className = afterState === true
+				? "good-result"
+				: "bad-result";
+		} else if (afterState !== true && afterState !== false) {
+			afterDiv.lastChild.className = "bad-result";
+		}
+	} else if (/^justify(center|full|left|right)$/.test(command)) {
+		// We don't know about the before state, but the after state is always
+		// supposed to be true.
+		if (beforeState !== true && beforeState !== false) {
+			beforeDiv.lastChild.className = "bad-result";
+		}
+		afterDiv.lastChild.className = afterState === true
+			? "good-result"
+			: "bad-result";
+	} else {
+		// The general rule is it must either throw an exception or flip the
+		// state.
 		beforeDiv.lastChild.className =
 		afterDiv.lastChild.className =
-			beforeState !== "Exception" && afterState !== "Exception" && beforeState === !afterState
+			("state" in commands[command] && typeof beforeState == "boolean" && typeof afterState == "boolean" && beforeState === !afterState)
+			|| (!("state" in commands[command]) && beforeState === "Exception" && afterState === "Exception")
 				? "good-result"
 				: "bad-result";
 	}
-	if (/^justify(center|full|left|right)$/.test(command)) {
-		afterDiv.lastChild.className =
-			beforeState !== "Exception" && afterState !== "Exception" && afterState
-				? "good-result"
-				: "bad-result";
-	}
-	beforeDiv.lastChild.textContent = "state " + beforeState;
-	afterDiv.lastChild.textContent = "state " + afterState;
+	beforeDiv.lastChild.textContent = "state " + trivialPrettyPrint(beforeState);
+	afterDiv.lastChild.textContent = "state " + trivialPrettyPrint(afterState);
 
 	beforeDiv.appendChild(document.createTextNode(", "));
 	afterDiv.appendChild(document.createTextNode(", "));
 
+	beforeDiv.appendChild(document.createElement("span"));
+	afterDiv.appendChild(document.createElement("span"));
+
+	// Direct equality comparison doesn't make sense in a bunch of cases.
 	if (command == "backcolor" || command == "forecolor" || command == "hilitecolor") {
 		if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) {
 			value = "#" + value;
 		}
-	}
-	if (command == "fontsize") {
+	} else if (command == "fontsize") {
 		value = normalizeFontSize(value);
 		var font = document.createElement("font");
 		document.body.appendChild(font);
@@ -3456,27 +3504,42 @@
 		}
 		value = getLegacyFontSize(parseInt(getComputedStyle(font).fontSize));
 		document.body.removeChild(font);
+	} else if (command == "formatblock") {
+		value = value.replace(/^<(.*)>$/, "$1").toLowerCase();
+	} else if (command == "unlink") {
+		value = null;
 	}
 
-	beforeDiv.appendChild(document.createElement("span"));
-	afterDiv.appendChild(document.createElement("span"));
-	if (afterValue !== "Exception"
-	&& typeof value != "undefined") {
+	if (/^justify(center|full|left|right)$/.test(command)) {
+		// We know there are only four correct values beforehand, and afterward
+		// the value has to be the one we set.
+		if (!/^(center|justify|left|right)$/.test(beforeValue)) {
+			beforeDiv.lastChild.className = "bad-result";
+		}
+		var expectedValue = command == "justifyfull"
+			? "justify"
+			: command.replace("justify", "");
+		afterDiv.lastChild.className = afterValue === expectedValue
+			? "good-result"
+			: "bad-result";
+	} else if (!("value" in commands[command])) {
+		// As usual, if it's not defined we want an exception.
+		beforeDiv.lastChild.className = beforeValue === "Exception"
+			? "good-result"
+			: "bad-result";
+		afterDiv.lastChild.className = afterValue === "Exception"
+			? "good-result"
+			: "bad-result";
+	} else {
+		// And in all other cases, the value afterwards has to be the one we
+		// set.
 		afterDiv.lastChild.className =
 			valuesEqual(command, afterValue, value)
 				? "good-result"
 				: "bad-result";
 	}
-	if (/^justify(center|full|left|right)$/.test(command)) {
-		var expectedValue = command == "justifyfull"
-			? "justify"
-			: command.replace("justify", "");
-		afterDiv.lastChild.className = afterValue == expectedValue
-			? "good-result"
-			: "bad-result";
-	}
-	beforeDiv.lastChild.textContent = "value " + beforeValue;
-	afterDiv.lastChild.textContent = "value " + afterValue;
+	beforeDiv.lastChild.textContent = "value " + trivialPrettyPrint(beforeValue);
+	afterDiv.lastChild.textContent = "value " + trivialPrettyPrint(afterValue);
 
 	return frag;
 }