Make value comparison precisely defined
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Fri, 29 Jul 2011 14:43:56 -0600
changeset 472 7af1813df5d3
parent 471 7f2e2fbdddf6
child 473 00d77b4fbb8e
Make value comparison precisely defined

This gets rid of the old evil valuesEqual() function and replaces it by
something clearly specified and far saner.
editing.html
implementation.js
preprocess
source.html
tests.js
--- a/editing.html	Thu Jul 28 15:24:15 2011 -0600
+++ b/editing.html	Fri Jul 29 14:43:56 2011 -0600
@@ -38,7 +38,7 @@
 <body class=draft>
 <div class=head id=head>
 <h1>HTML Editing APIs</h1>
-<h2 class="no-num no-toc" id=work-in-progress-&mdash;-last-update-28-july-2011>Work in Progress &mdash; Last Update 28 July 2011</h2>
+<h2 class="no-num no-toc" id=work-in-progress-&mdash;-last-update-29-july-2011>Work in Progress &mdash; Last Update 29 July 2011</h2>
 <dl>
  <dt>Editor
  <dd>Aryeh Gregor &lt;<a href=mailto:ayg@aryeh.name>ayg@aryeh.name</a>&gt;
@@ -239,9 +239,7 @@
 <ul>
   <li>Need to make CSS terminology more precise, about setting/unsetting CSS
   properties.  The intent is to modify the style attribute, CSSOM-style.
-  Likewise, CSS value comparisons need to be done after serializing both
-  values, so "bold" == "700" and "red" == "#f00" and so on.  Suggestions
-  appreciated on how I should spec this.
+  Suggestions appreciated on how I should spec this.
 
   <li>I use <a href=http://dev.w3.org/csswg/cssom/#resolved-value>resolved value</a> instead of computed or used or anything like that, just
   because that's what my test implementation uses (via getComputedStyle).  This
@@ -1375,6 +1373,28 @@
 <p class=note>Conceptually, a simple modifiable element is a modifiable element
 which specifies a value for at most one command.
 
+<p>Two quantities are <dfn id=equivalent-values>equivalent values</dfn> for a <a href=#command>command</a>
+if either both are null, or both are strings and they're equal and the
+<a href=#command>command</a> does not define any <a href=#equivalent-values>equivalent values</a>, or
+both are strings and the <a href=#command>command</a> defines <a href=#equivalent-values>equivalent
+values</a> and they match the definition.
+
+<p>Two quantities are <dfn id=loosely-equivalent-values>loosely equivalent values</dfn> for a
+<a href=#command>command</a> if either they are <a href=#equivalent-values>equivalent values</a> for the
+<a href=#command>command</a>, or if the <a href=#command>command</a> is <a href=#the-fontsize-command>the <code title="">fontSize</code> command</a>; one of the quantities is one of
+"xx-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
+and the other quantity is the <a href=http://dev.w3.org/csswg/cssom/#resolved-value>resolved value</a> of "font-size" on a <code class=external data-anolis-spec=html title=font><a href=http://www.whatwg.org/html/#font>font</a></code> element
+whose <code class=external data-anolis-spec=html title=dom-font-size><a href=http://www.whatwg.org/html/#dom-font-size>size</a></code> attribute has the corresponding value set ("1" through "7"
+respectively).
+
+<p class=note>Loose equivalence needs to be used when comparing effective
+command values to other values, while regular equivalence is used in other
+cases.  The effective command value for fontSize is converted to pixels, so
+comparing it to a specified value literally would produce false negatives.  But
+a <em>specified</em> value in pixels is actually different from a
+<em>specified</em> value like "small" or "x-large", because there is no precise
+mapping from such keywords to pixels.
+
 <p>If a <a href=#command>command</a> has <dfn id=inline-command-activated-values>inline command activated values</dfn>
 defined but nothing else defines when it is <a href=#indeterminate>indeterminate</a>, it is
 <a href=#indeterminate>indeterminate</a> 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
@@ -1617,13 +1637,15 @@
   <var title="">candidate</var> has exactly one <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>, and that <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a> is also a
   <a href=#modifiable-element>modifiable element</a>, and <var title="">candidate</var> is not a
   <a href=#simple-modifiable-element>simple modifiable element</a> or <var title="">candidate</var>'s
-  <a href=#specified-command-value>specified command value</a> for <var title="">command</var> is not <var title="">new
-  value</var>, set <var title="">candidate</var> to its <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>.
+  <a href=#specified-command-value>specified command value</a> for <var title="">command</var> is not <a href=#equivalent-values title="equivalent values">equivalent</a> to <var title="">new value</var>, set
+  <var title="">candidate</var> to its <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>.
 
   <li>If <var title="">candidate</var> is <var title="">node</var>, or is not a <a href=#simple-modifiable-element>simple
-  modifiable element</a>, or its <a href=#specified-command-value>specified command value</a> and
-  <a href=#effective-command-value>effective command value</a> for <var title="">command</var> are not both
-  <var title="">new value</var>, abort these steps.
+  modifiable element</a>, or its <a href=#specified-command-value>specified command value</a> is not
+  <a href=#equivalent-values title="equivalent values">equivalent</a> to <var title="">new value</var>, or
+  its <a href=#effective-command-value>effective command value</a> is not <a href=#loosely-equivalent-values title="loosely
+  equivalent values">loosely equivalent</a> to <var title="">new value</var>, abort
+  these steps.
 
   <li>While <var title="">candidate</var> has <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a>, insert the first <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>
   of <var title="">candidate</var> into <var title="">candidate</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> immediately
@@ -1710,10 +1732,10 @@
     with <var title="">new value</var> null.
 
     <li>Otherwise, if <var title="">ancestor</var> is an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> and its
-    <a href=#specified-command-value>specified command value</a> for <var title="">command</var> is different
-    from <var title="">value</var>, or if <var title="">ancestor</var> is not an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> and
-    <var title="">value</var> is not null, <a href=#force-the-value>force the value</a> of
-    <var title="">command</var> to <var title="">value</var> on <var title="">node</var>.
+    <a href=#specified-command-value>specified command value</a> for <var title="">command</var> is not <a href=#equivalent-values title="equivalent values">equivalent</a> to <var title="">value</var>, or if
+    <var title="">ancestor</var> is not an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> and <var title="">value</var> is not null,
+    <a href=#force-the-value>force the value</a> of <var title="">command</var> to <var title="">value</var> on
+    <var title="">node</var>.
   </ol>
 </ol>
 
@@ -1839,7 +1861,8 @@
   algorithm. <!-- E.g., a text node child of a document fragment. -->
 
   <li>If the <a href=#effective-command-value>effective command value</a> of <var title="">command</var> is
-  <var title="">new value</var> on <var title="">node</var>, abort this algorithm.
+  <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var> on <var title="">node</var>, abort this
+  algorithm.
 
   <li>Let <var title="">current ancestor</var> be <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>.
 
@@ -1847,9 +1870,9 @@
 
   <li>While <var title="">current ancestor</var> is an <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#element>Element</a></code>
   and the <a href=#effective-command-value>effective command value</a> of <var title="">command</var> is not
-  <var title="">new value</var> on it, append <var title="">current ancestor</var> to
-  <var title="">ancestor list</var>, then set <var title="">current ancestor</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>.
+  <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var> on it, append <var title="">current
+  ancestor</var> to <var title="">ancestor list</var>, then set <var title="">current
+  ancestor</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>If <var title="">ancestor list</var> is empty, abort this algorithm.
 
@@ -1893,8 +1916,9 @@
   we're styling something that includes the first or last child).
   -->
   <li>If the <a href=#effective-command-value>effective command value</a> of <var title="">command</var> is not
-  <var title="">new value</var> on the <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> of the last member of <var title="">ancestor
-  list</var>, and <var title="">new value</var> is not null, abort this algorithm.
+  <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var> on the <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> of the last
+  member of <var title="">ancestor list</var>, and <var title="">new value</var> is not null,
+  abort this algorithm.
 
   <li>While <var title="">ancestor list</var> is not empty:
 
@@ -1922,8 +1946,8 @@
       <var title="">child</var>.
 
       <li>If <var title="">child</var> is an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> whose <a href=#specified-command-value>specified command
-      value</a> for <var title="">command</var> is neither null nor equal to
-      <var title="">propagated value</var>, continue with the next <var title="">child</var>.
+      value</a> for <var title="">command</var> is neither null nor <a href=#equivalent-values title="equivalent values">equivalent</a> to <var title="">propagated
+      value</var>, continue with the next <var title="">child</var>.
       <!--
       TODO: This will be incorrect for relative font sizes.  If the font size
       on the parent was removed and the font size on the child is in ems or
@@ -1982,24 +2006,24 @@
 
     <li><a href=#wrap>Wrap</a> 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>,
     with <a href=#sibling-criteria>sibling criteria</a> matching a <a href=#simple-modifiable-element>simple modifiable
-    element</a> whose <a href=#specified-command-value>specified command value</a> and
-    <a href=#effective-command-value>effective command value</a> for <var title="">command</var> are both
-    <var title="">new value</var>, and with <a href=#new-parent-instructions>new parent instructions</a>
-    returning null.
+    element</a> whose <a href=#specified-command-value>specified command value</a> is <a href=#equivalent-values title="equivalent values">equivalent</a>
+    to <var title="">new value</var> and whose <a href=#effective-command-value>effective command value</a> is
+    <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var>, and with <a href=#new-parent-instructions>new parent
+    instructions</a> returning null.
     <!-- The new parent instructions are too complicated to reasonably feed
     into the wrap algorithm. -->
   </ol>
 
   <li>If the <a href=#effective-command-value>effective command value</a> of <var title="">command</var> is
-  <var title="">new value</var> on <var title="">node</var>, abort this algorithm.
+  <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var> on <var title="">node</var>, abort this
+  algorithm.
 
   <li>If <var title="">node</var> is not an <a href=#allowed-child>allowed child</a> of "span":
 
   <ol>
     <li>Let <var title="">children</var> be all <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a> of <var title="">node</var>,
     omitting any that are <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code>s whose <a href=#specified-command-value>specified command
-    value</a> for <var title="">command</var> is neither null nor equal to <var title="">new
-    value</var>.
+    value</a> for <var title="">command</var> is neither null nor <a href=#equivalent-values title="equivalent values">equivalent</a> to <var title="">new value</var>.
 
     <li><a href=#force-the-value>Force the value</a> of each <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> in <var title="">children</var>,
     with <var title="">command</var> and <var title="">new value</var> as in this invocation of
@@ -2014,7 +2038,8 @@
   </ol>
 
   <li>If the <a href=#effective-command-value>effective command value</a> of <var title="">command</var> is
-  <var title="">new value</var> on <var title="">node</var>, abort this algorithm.
+  <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var> on <var title="">node</var>, abort this
+  algorithm.
 
   <li>Let <var title="">new parent</var> be null.
 
@@ -2145,10 +2170,13 @@
   -->
 
   <li>If the <a href=#effective-command-value>effective command value</a> of <var title="">command</var> for
-  <var title="">new parent</var> is not <var title="">new value</var>, and the <a href=#relevant-css-property>relevant CSS
-  property</a> for <var title="">command</var> is not null, set that CSS property of
-  <var title="">new parent</var> to <var title="">new value</var> (if the new value would be
-  valid).
+  <var title="">new parent</var> is not <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var>,
+  and the <a href=#relevant-css-property>relevant CSS property</a> for <var title="">command</var> is not
+  null, set that CSS property of <var title="">new parent</var> to <var title="">new value</var>
+  (if the new value would be valid).
+
+  <p class=XXX>Need to be explicit.  I think "if the new value would be valid"
+  means "if the new value isn't xxx-large for font-size", need to double-check.
 
   <li>If <var title="">command</var> is "strikethrough", and <var title="">new value</var> is
   "line-through", and the <a href=#effective-command-value>effective command value</a> of
@@ -2164,8 +2192,8 @@
   <a href=#preserving-ranges>preserving ranges</a>.
 
   <li>If <var title="">node</var> is an <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> and the <a href=#effective-command-value>effective command
-  value</a> of <var title="">command</var> for <var title="">node</var> is not <var title="">new
-  value</var>:
+  value</a> of <var title="">command</var> for <var title="">node</var> is not
+  <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">new value</var>:
 
   <ol>
     <li>Insert <var title="">node</var> into the <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> of <var title="">new parent</var>
@@ -2175,8 +2203,7 @@
 
     <li>Let <var title="">children</var> be all <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a> of <var title="">node</var>,
     omitting any that are <code class=external data-anolis-spec=domcore><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code>s whose <a href=#specified-command-value>specified command
-    value</a> for <var title="">command</var> is neither null nor equal to <var title="">new
-    value</var>.
+    value</a> for <var title="">command</var> is neither null nor <a href=#equivalent-values title="equivalent values">equivalent</a> to <var title="">new value</var>.
 
     <li><a href=#force-the-value>Force the value</a> of each <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> in <var title="">children</var>,
     with <var title="">command</var> and <var title="">new value</var> as in this invocation of
@@ -2280,10 +2307,6 @@
     <li>If <var title="">new value</var> is null, unset the <a href=#value-override>value override</a>
     (if any).
 
-    <li>Otherwise, if <var title="">command</var> is "fontSize", set the <a href=#value-override>value
-    override</a> to the <a href=#legacy-font-size-for>legacy font size for</a> the result of
-    converting <var title="">new value</var> to pixels.
-
     <li>Otherwise, if <var title="">command</var> has a <a href=#value>value</a> specified,
     set the <a href=#value-override>value override</a> to <var title="">new value</var>.
 
@@ -2419,6 +2442,10 @@
 
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "background-color"
 
+<p><a href=#equivalent-values>Equivalent values</a>: Either both strings are valid CSS colors and
+have the same red, green, blue, and alpha components, or neither string is a
+valid CSS color.
+
 
 <h3 id=the-bold-command><span class=secno>7.8 </span><dfn>The <code title="">bold</code> command</dfn></h3>
 
@@ -2453,6 +2480,9 @@
 
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "font-weight"
 
+<p><a href=#equivalent-values>Equivalent values</a>: Either the two strings are equal, or one is
+"bold" and the other is "700", or one is "normal" and the other is "400".
+
 
 <h3 id=the-createlink-command><span class=secno>7.9 </span><dfn>The <code title="">createLink</code> command</dfn></h3>
 
@@ -2830,6 +2860,10 @@
 
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "color"
 
+<p><a href=#equivalent-values>Equivalent values</a>: Either both strings are valid CSS colors and
+have the same red, green, blue, and alpha components, or neither string is a
+valid CSS color.
+
 
 <h3 id=the-hilitecolor-command><span class=secno>7.13 </span><dfn>The <code title="">hiliteColor</code> command</dfn></h3>
 
@@ -2873,6 +2907,10 @@
 
 <p><a href=#relevant-css-property>Relevant CSS property</a>: "background-color"
 
+<p><a href=#equivalent-values>Equivalent values</a>: Either both strings are valid CSS colors and
+have the same red, green, blue, and alpha components, or neither string is a
+valid CSS color.
+
 
 <h3 id=the-italic-command><span class=secno>7.14 </span><dfn>The <code title="">italic</code> command</dfn></h3>
 
@@ -3640,9 +3678,13 @@
   the contents".
   -->
 
-  <li>For each <var title="">command</var> in the list "fontName", "fontSize",
-  "foreColor", "hiliteColor", in order: add (<var title="">command</var>,
-  <var title="">command</var>'s <a href=#value>value</a>) to <var title="">overrides</var>.
+  <li>For each <var title="">command</var> in the list "fontName", "foreColor",
+  "hiliteColor", in order: add (<var title="">command</var>, <var title="">command</var>'s
+  <a href=#value>value</a>) to <var title="">overrides</var>.
+
+  <!-- Special case for fontSize, because its values are weird. -->
+  <li>Add ("fontSize", <var title="">node</var>'s <a href=#effective-command-value>effective command value</a>
+  for "fontSize") to <var title="">overrides</var>.
 
   <li>Return <var title="">overrides</var>.
 </ol>
@@ -3652,18 +3694,30 @@
 <a href=#record-current-states-and-values>record current states and values</a> algorithm:
 
 <ol>
-  <li>If there is some <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 <a href=#effectively-contained>effectively
-  contained</a> in the <a href=#active-range>active range</a>, then for each
-  (<var title="">command</var>, <var title="">override</var>) pair in <var title="">overrides</var>, in
-  order:
+  <li>Let <var title="">node</var> be 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
+  <a href=#effectively-contained>effectively contained</a> in the <a href=#active-range>active range</a>, or null
+  if there is none.
+
+  <li>If <var title="">node</var> is not null, then for each (<var title="">command</var>,
+  <var title="">override</var>) pair in <var title="">overrides</var>, in order:
 
   <ol>
     <li>If <var title="">override</var> is a boolean, and <code title=queryCommandState()><a href=#querycommandstate()>queryCommandState(<var title="">command</var>)</a></code>
     returns something different from <var title="">override</var>, call <code title=execCommand()><a href=#execcommand()>execCommand(<var title="">command</var>)</a></code>.
 
-    <li>If <var title="">override</var> is a string, and <code title=queryCommandValue()><a href=#querycommandvalue()>queryCommandValue(<var title="">command</var>)</a></code>
-    returns something different from <var title="">override</var>, call <code title=execCommand()><a href=#execcommand()>execCommand(<var title="">command</var>, false,
+    <li>If <var title="">override</var> is a string, and <var title="">command</var> is not
+    "fontSize", and <code title=queryCommandValue()><a href=#querycommandvalue()>queryCommandValue(<var title="">command</var>)</a></code>
+    returns something not <a href=#equivalent-values title="equivalent values">equivalent</a> to
+    <var title="">override</var>, call <code title=execCommand()><a href=#execcommand()>execCommand(<var title="">command</var>, false,
     <var title="">override</var>)</a></code>.
+
+    <li>If <var title="">override</var> is a string; and <var title="">command</var> is
+    "fontSize"; and either there is a <a href=#value-override>value override</a> for
+    "fontSize" that is not equal to <var title="">override</var>, or there is no
+    <a href=#value-override>value override</a> for "fontSize" and <var title="">node</var>'s
+    <a href=#effective-command-value>effective command value</a> for "fontSize" is not
+    <a href=#loosely-equivalent-values title="loosely equivalent values">loosely equivalent</a> to <var title="">override</var>: call
+    <code title=execCommand()><a href=#execcommand()>execCommand("fontSize", false, <var title="">override</var>)</a></code>.
   </ol>
 
   <li>Otherwise, for each (<var title="">command</var>, <var title="">override</var>) pair in
--- a/implementation.js	Thu Jul 28 15:24:15 2011 -0600
+++ b/implementation.js	Fri Jul 29 14:43:56 2011 -0600
@@ -129,86 +129,17 @@
 	}[cssVal];
 }
 
-// This entire function is a massive hack to work around browser
-// incompatibility.  It wouldn't work in real life, but it's good enough for a
-// 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
-	|| (command == "fontsize" && /^[1-7]$/.test(val1) && /^[1-7]$/.test(val2))) {
-		return val1 === val2;
-	}
-
-	if (val1 === null || val2 === null) {
-		return val1 === val2;
-	}
-
-	if (command == "subscript" || command == "superscript") {
-		return val1 === val2;
-	}
-
-	if (command == "bold") {
-		return val1 == val2
-			|| (val1.toLowerCase() == "bold" && val2 == "700")
-			|| (val2.toLowerCase() == "bold" && val1 == "700")
-			|| (val1.toLowerCase() == "normal" && val2 == "400")
-			|| (val2.toLowerCase() == "normal" && val1 == "400");
-	}
-
-	var property = commands[command].relevantCssProperty;
-	var test1 = document.createElement("span");
-	test1.style[property] = val1;
-	var test2 = document.createElement("span");
-	test2.style[property] = val2;
-
-	// Computing style doesn't seem to always work if the elements aren't in
-	// the body?
-	document.body.appendChild(test1);
-	document.body.appendChild(test2);
-
-	// We can't test xxx-large with CSS.  Also, some browsers (WebKit?) don't
-	// actually make <span style="font-size: xx-small"> have the same size as
-	// <font size="1">, and so on.  So we have to test both . . .
-	var test1b = null, test2b = null;
-	if (command == "fontsize") {
-		if (typeof cssSizeToLegacy(val1) != "undefined") {
-			test1b = document.createElement("font");
-			test1b.size = cssSizeToLegacy(val1);
-			document.body.appendChild(test1b);
-		}
-		if (typeof cssSizeToLegacy(val2) != "undefined") {
-			test2b = document.createElement("font");
-			test2b.size = cssSizeToLegacy(val2);
-			document.body.appendChild(test2b);
-		}
-	}
-
-	var computed1b = test1b
-		? getComputedStyle(test1b)[property]
-		: null;
-	var computed2b = test2b
-		? getComputedStyle(test2b)[property]
-		: null;
-	var computed1 = command == "fontsize" && val1 == "xxx-large"
-		? computed1b
-		: getComputedStyle(test1)[property];
-	var computed2 = command == "fontsize" && val2 == "xxx-large"
-		? computed2b
-		: getComputedStyle(test2)[property];
-
-	document.body.removeChild(test1);
-	document.body.removeChild(test2);
-
-	if (test1b) {
-		document.body.removeChild(test1b);
-	}
-	if (test2b) {
-		document.body.removeChild(test2b);
-	}
-
-	return computed1 == computed2
-		|| computed1 === computed2b
-		|| computed1b === computed2;
+// Return the CSS size given a legacy size.
+function legacySizeToCss(legacyVal) {
+	return {
+		1: "xx-small",
+		2: "small",
+		3: "medium",
+		4: "large",
+		5: "x-large",
+		6: "xx-large",
+		7: "xxx-large",
+	}[legacyVal];
 }
 
 // Opera 11 puts HTML elements in the null namespace, it seems.
@@ -462,12 +393,52 @@
 	return nodeList;
 }
 
+// Returns either null, or something of the form rgb(x, y, z), or something of
+// the form rgb(x, y, z, w) with w != 0.
+function normalizeColor(color) {
+	if (color.toLowerCase() == "currentcolor") {
+		return null;
+	}
+
+	var outerSpan = document.createElement("span");
+	document.body.appendChild(outerSpan);
+	outerSpan.style.color = "black";
+
+	var innerSpan = document.createElement("span");
+	outerSpan.appendChild(innerSpan);
+	innerSpan.style.color = color;
+	color = getComputedStyle(innerSpan).color;
+
+	if (color == "rgb(0, 0, 0)") {
+		// Maybe it's really black, maybe it's invalid.
+		outerSpan.color = "white";
+		color = getComputedStyle(innerSpan).color;
+		if (color != "rgb(0, 0, 0)") {
+			return null;
+		}
+	}
+
+	document.body.removeChild(outerSpan);
+
+	// I rely on the fact that browsers generally provide consistent syntax for
+	// getComputedStyle(), although it's not standardized.  There are only two
+	// exceptions I found:
+	if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
+		// IE10PP2 seems to do this sometimes.
+		return color.replace("rgba", "rgb").replace(", 1)", ")");
+	}
+	if (color == "transparent") {
+		// IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if
+		// the specified value is "transparent".
+		return "rgba(0, 0, 0, 0)";
+	}
+	return color;
+}
+
 // Returns either null, or something of the form #xxxxxx, or the color itself
 // if it's a valid keyword.
 function parseSimpleColor(color) {
 	color = color.toLowerCase();
-	// Yay for Gecko allowing you to select a column of a table without
-	// selecting anything from other columns.
 	if (["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
 	"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
 	"burlywood", "cadetblue", "chartreuse", "chocolate", "coral",
@@ -499,36 +470,7 @@
 		return color;
 	}
 
-	var outerSpan = document.createElement("span");
-	document.body.appendChild(outerSpan);
-	outerSpan.style.color = "black";
-
-	var innerSpan = document.createElement("span");
-	outerSpan.appendChild(innerSpan);
-	innerSpan.style.color = color;
-	color = getComputedStyle(innerSpan).color;
-
-	if (color == "rgb(0, 0, 0)") {
-		// Maybe it's really black, maybe it's invalid.
-		outerSpan.color = "white";
-		color = getComputedStyle(innerSpan).color;
-		if (color != "rgb(0, 0, 0)") {
-			return null;
-		}
-	}
-
-	document.body.removeChild(outerSpan);
-
-	if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) {
-		// IE10PP2 seems to do this sometimes.
-		color = color.replace("rgba", "rgb").replace(", 1)", ")");
-	}
-	// I rely on the fact that browsers generally provide consistent syntax for
-	// getComputedStyle(), although it's not standardized.  In particular, they
-	// seem to clamp the components to integers between 0 and 255, and use
-	// consistent spacing, and always return rgb() syntax.  (Firefox 7.0a2
-	// sometimes returns "transparent", but we need to return null then
-	// anyway.)
+	color = normalizeColor(color);
 	var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color);
 	if (matches) {
 		return "#"
@@ -1838,6 +1780,66 @@
 	return false;
 }
 
+// "Two quantities are equivalent values for a command if either both are null,
+// or both are strings and they're equal and the command does not define any
+// equivalent values, or both are strings and the command defines equivalent
+// values and they match the definition."
+function areEquivalentValues(command, val1, val2) {
+	if (val1 === null && val2 === null) {
+		return true;
+	}
+
+	if (typeof val1 == "string"
+	&& typeof val2 == "string"
+	&& val1 == val2
+	&& !("equivalentValues" in commands[command])) {
+		return true;
+	}
+
+	if (typeof val1 == "string"
+	&& typeof val2 == "string"
+	&& "equivalentValues" in commands[command]
+	&& commands[command].equivalentValues(val1, val2)) {
+		return true;
+	}
+
+	return false;
+}
+
+// "Two quantities are loosely equivalent values for a command if either they
+// are equivalent values for the command, or if the command is the fontSize
+// command; one of the quantities is one of "xx-small", "small", "medium",
+// "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is
+// the resolved value of "font-size" on a font element whose size attribute has
+// the corresponding value set ("1" through "7" respectively)."
+function areLooselyEquivalentValues(command, val1, val2) {
+	if (areEquivalentValues(command, val1, val2)) {
+		return true;
+	}
+
+	if (command != "fontsize"
+	|| typeof val1 != "string"
+	|| typeof val2 != "string") {
+		return false;
+	}
+
+	// Static variables in JavaScript?
+	var callee = areLooselyEquivalentValues;
+	if (callee.sizeMap === undefined) {
+		callee.sizeMap = {};
+		var font = document.createElement("font");
+		document.body.appendChild(font);
+		["xx-small", "small", "medium", "large", "x-large", "xx-large",
+		"xxx-large"].forEach(function(keyword) {
+			font.size = cssSizeToLegacy(keyword);
+			callee.sizeMap[keyword] = getComputedStyle(font).fontSize;
+		});
+		document.body.removeChild(font);
+	}
+
+	return val1 === callee.sizeMap[val2]
+		|| val2 === callee.sizeMap[val1];
+}
 
 //@}
 ///// Assorted inline formatting command algorithms /////
@@ -2152,21 +2154,23 @@
 	// "While candidate is a modifiable element, and candidate has exactly one
 	// child, and that child is also a modifiable element, and candidate is not
 	// a simple modifiable element or candidate's specified command value for
-	// command is not new value, set candidate to its child."
+	// command is not equivalent to new value, set candidate to its child."
 	while (isModifiableElement(candidate)
 	&& candidate.childNodes.length == 1
+	&& isModifiableElement(candidate.firstChild)
 	&& (!isSimpleModifiableElement(candidate)
-	|| !valuesEqual(command, getSpecifiedCommandValue(candidate, command), newValue))) {
+	|| !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) {
 		candidate = candidate.firstChild;
 	}
 
 	// "If candidate is node, or is not a simple modifiable element, or its
-	// specified command value and effective command value for command are not
-	// both new value, abort these steps."
+	// specified command value is not equivalent to new value, or its effective
+	// command value is not loosely equivalent to new value, abort these
+	// steps."
 	if (candidate == node
 	|| !isSimpleModifiableElement(candidate)
-	|| !valuesEqual(command, getSpecifiedCommandValue(candidate, command), newValue)
-	|| !valuesEqual(command, getEffectiveCommandValue(candidate, command), newValue)) {
+	|| !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue)
+	|| !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) {
 		return;
 	}
 
@@ -2256,12 +2260,12 @@
 			pushDownValues(node, command, null);
 
 		// "Otherwise, if ancestor is an Element and its specified command
-		// value for command is different from value, or if ancestor is not an
-		// Element and value is not null, force the value of command to value
-		// on node."
+		// value for command is not equivalent to value, or if ancestor is not
+		// an Element and value is not null, force the value of command to
+		// value on node."
 		} else if ((ancestor
 		&& ancestor.nodeType == Node.ELEMENT_NODE
-		&& !valuesEqual(command, getSpecifiedCommandValue(ancestor, command), value))
+		&& !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value))
 		|| ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE)
 		&& value !== null)) {
 			forceValue(node, command, value);
@@ -2391,9 +2395,9 @@
 		return;
 	}
 
-	// "If the effective command value of command is new value on node, abort
-	// this algorithm."
-	if (valuesEqual(command, getEffectiveCommandValue(node, command), newValue)) {
+	// "If the effective command value of command is loosely equivalent to new
+	// value on node, abort this algorithm."
+	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
 		return;
 	}
 
@@ -2404,11 +2408,12 @@
 	var ancestorList = [];
 
 	// "While current ancestor is an editable Element and the effective command
-	// value of command is not new value on it, append current ancestor to
-	// ancestor list, then set current ancestor to its parent."
+	// value of command is not loosely equivalent to new value on it, append
+	// current ancestor to ancestor list, then set current ancestor to its
+	// parent."
 	while (isEditable(currentAncestor)
 	&& currentAncestor.nodeType == Node.ELEMENT_NODE
-	&& !valuesEqual(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
+	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) {
 		ancestorList.push(currentAncestor);
 		currentAncestor = currentAncestor.parentNode;
 	}
@@ -2428,11 +2433,11 @@
 		return;
 	}
 
-	// "If the effective command value of command is not new value on the
-	// parent of the last member of ancestor list, and new value is not null,
-	// abort this algorithm."
+	// "If the effective command value for the parent of the last member of
+	// ancestor list is not loosely equivalent to new value, and new value is
+	// not null, abort this algorithm."
 	if (newValue !== null
-	&& !valuesEqual(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
+	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) {
 		return;
 	}
 
@@ -2467,11 +2472,11 @@
 			}
 
 			// "If child is an Element whose specified command value for
-			// command is neither null nor equal to propagated value, continue
-			// with the next child."
+			// command is neither null nor equivalent to propagated value,
+			// continue with the next child."
 			if (child.nodeType == Node.ELEMENT_NODE
 			&& getSpecifiedCommandValue(child, command) !== null
-			&& !valuesEqual(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
+			&& !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) {
 				continue;
 			}
 
@@ -2514,21 +2519,22 @@
 
 		// "Wrap the one-node list consisting of node, with sibling criteria
 		// matching a simple modifiable element whose specified command value
-		// and effective command value for command are both new value, and with
-		// new parent instructions returning null."
+		// is equivalent to new value and whose effective command value is
+		// loosely equivalent to new value, and with new parent instructions
+		// returning null."
 		wrap([node],
 			function(node) {
 				return isSimpleModifiableElement(node)
-					&& valuesEqual(command, getSpecifiedCommandValue(node, command), newValue)
-					&& valuesEqual(command, getEffectiveCommandValue(node, command), newValue);
+					&& areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue)
+					&& areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue);
 			},
 			function() { return null }
 		);
 	}
 
-	// "If the effective command value of command is new value on node, abort
-	// this algorithm."
-	if (valuesEqual(command, getEffectiveCommandValue(node, command), newValue)) {
+	// "If the effective command value of command is loosely equivalent to new
+	// value on node, abort this algorithm."
+	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
 		return;
 	}
 
@@ -2536,14 +2542,14 @@
 	if (!isAllowedChild(node, "span")) {
 		// "Let children be all children of node, omitting any that are
 		// Elements whose specified command value for command is neither null
-		// nor equal to new value."
+		// nor equivalent to new value."
 		var children = [];
 		for (var i = 0; i < node.childNodes.length; i++) {
 			if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
 				var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
 
 				if (specifiedValue !== null
-				&& !valuesEqual(command, newValue, specifiedValue)) {
+				&& !areEquivalentValues(command, newValue, specifiedValue)) {
 					continue;
 				}
 			}
@@ -2560,9 +2566,9 @@
 		return;
 	}
 
-	// "If the effective command value of command is new value on node, abort
-	// this algorithm."
-	if (valuesEqual(command, getEffectiveCommandValue(node, command), newValue)) {
+	// "If the effective command value of command is loosely equivalent to new
+	// value on node, abort this algorithm."
+	if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
 		return;
 	}
 
@@ -2658,15 +2664,7 @@
 	&& ["xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"].indexOf(newValue) != -1
 	&& (!cssStylingFlag || newValue == "xxx-large")) {
 		newParent = node.ownerDocument.createElement("font");
-		newParent.size = {
-			"xx-small": 1,
-			"small": 2,
-			"medium": 3,
-			"large": 4,
-			"x-large": 5,
-			"xx-large": 6,
-			"xxx-large": 7
-		}[newValue];
+		newParent.size = cssSizeToLegacy(newValue);
 	}
 
 	// "If command is "subscript" or "superscript" and new value is "sub", let
@@ -2694,13 +2692,13 @@
 	// "Insert new parent in node's parent before node."
 	node.parentNode.insertBefore(newParent, node);
 
-	// "If the effective command 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)."
+	// "If the effective command value of command for new parent is not loosely
+	// equivalent to 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 = commands[command].relevantCssProperty;
 	if (property !== null
-	&& !valuesEqual(command, getEffectiveCommandValue(newParent, command), newValue)) {
+	&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) {
 		newParent.style[property] = newValue;
 	}
 
@@ -2728,9 +2726,9 @@
 	movePreservingRanges(node, newParent, newParent.childNodes.length);
 
 	// "If node is an Element and the effective command value of command for
-	// node is not new value:"
+	// node is not loosely equivalent to new value:"
 	if (node.nodeType == Node.ELEMENT_NODE
-	&& !valuesEqual(command, getEffectiveCommandValue(node, command), newValue)) {
+	&& !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) {
 		// "Insert node into the parent of new parent before new parent,
 		// preserving ranges."
 		movePreservingRanges(node, newParent.parentNode, getNodeIndex(newParent));
@@ -2740,14 +2738,14 @@
 
 		// "Let children be all children of node, omitting any that are
 		// Elements whose specified command value for command is neither null
-		// nor equal to new value."
+		// nor equivalent to new value."
 		var children = [];
 		for (var i = 0; i < node.childNodes.length; i++) {
 			if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) {
 				var specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command);
 
 				if (specifiedValue !== null
-				&& !valuesEqual(command, newValue, specifiedValue)) {
+				&& !areEquivalentValues(command, newValue, specifiedValue)) {
 					continue;
 				}
 			}
@@ -2796,19 +2794,6 @@
 		if (newValue === null) {
 			unsetValueOverride(command);
 
-		// "Otherwise, if command is "fontSize", set the value override to the
-		// legacy font size for the result of converting new value to pixels."
-		} else if (command == "fontsize") {
-			var font = document.createElement("font");
-			document.body.appendChild(font);
-			if (newValue == "xxx-large") {
-				font.size = 7;
-			} else {
-				font.style.fontSize = newValue;
-			}
-			setValueOverride(command, getLegacyFontSize(parseInt(getComputedStyle(font).fontSize)));
-			document.body.removeChild(font);
-
 		// "Otherwise, if command has a value specified, set the value override
 		// to new value."
 		} else if ("value" in commands[command]) {
@@ -2908,7 +2893,13 @@
 
 		// "Set the selection's value to value."
 		setSelectionValue("backcolor", value);
-	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor"
+	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
+	equivalentValues: function(val1, val2) {
+		// "Either both strings are valid CSS colors and have the same red,
+		// green, blue, and alpha components, or neither string is a valid CSS
+		// color."
+		return normalizeColor(val1) === normalizeColor(val2);
+	},
 };
 
 //@}
@@ -2924,7 +2915,16 @@
 			setSelectionValue("bold", "bold");
 		}
 	}, inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"],
-	relevantCssProperty: "fontWeight"
+	relevantCssProperty: "fontWeight",
+	equivalentValues: function(val1, val2) {
+		// "Either the two strings are equal, or one is "bold" and the other is
+		// "700", or one is "normal" and the other is "400"."
+		return val1 == val2
+			|| (val1 == "bold" && val2 == "700")
+			|| (val1 == "700" && val2 == "bold")
+			|| (val1 == "normal" && val2 == "400")
+			|| (val1 == "400" && val2 == "normal");
+	},
 };
 
 //@}
@@ -3156,7 +3156,13 @@
 
 		// "Set the selection's value to value."
 		setSelectionValue("forecolor", value);
-	}, standardInlineValueCommand: true, relevantCssProperty: "color"
+	}, standardInlineValueCommand: true, relevantCssProperty: "color",
+	equivalentValues: function(val1, val2) {
+		// "Either both strings are valid CSS colors and have the same red,
+		// green, blue, and alpha components, or neither string is a valid CSS
+		// color."
+		return normalizeColor(val1) === normalizeColor(val2);
+	},
 };
 
 //@}
@@ -3195,7 +3201,13 @@
 		}).filter(function(value, i, arr) {
 			return arr.slice(0, i).indexOf(value) == -1;
 		}).length >= 2;
-	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor"
+	}, standardInlineValueCommand: true, relevantCssProperty: "backgroundColor",
+	equivalentValues: function(val1, val2) {
+		// "Either both strings are valid CSS colors and have the same red,
+		// green, blue, and alpha components, or neither string is a valid CSS
+		// color."
+		return normalizeColor(val1) === normalizeColor(val2);
+	},
 };
 
 //@}
@@ -3993,10 +4005,10 @@
 		}
 	});
 
-	// "For each command in the list "fontName", "fontSize", "foreColor",
-	// "hiliteColor", in order: if there is a value override for command, add
-	// (command, command's value override) to overrides."
-	["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
+	// "For each command in the list "fontName", "foreColor", "hiliteColor", in
+	// order: if there is a value override for command, add (command, command's
+	// value override) to overrides."
+	["fontname", "forecolor", "hilitecolor"].forEach(function(command) {
 		if (getValueOverride(command) !== undefined) {
 			overrides.push([command, getValueOverride(command)]);
 		}
@@ -4039,22 +4051,29 @@
 		}
 	});
 
-	// "For each command in the list "fontName", "fontSize", "foreColor",
-	// "hiliteColor", in order: add (command, command's value) to overrides."
+	// "For each command in the list "fontName", "foreColor", "hiliteColor", in
+	// order: add (command, command's value) to overrides."
 	["fontname", "fontsize", "forecolor", "hilitecolor"].forEach(function(command) {
 		overrides.push([command, commands[command].value()]);
 	});
 
+	// "Add ("fontSize", node's effective command value for "fontSize") to
+	// overrides."
+	overrides.push("fontsize", getEffectiveCommandValue(node, "fontsize"));
+
 	// "Return overrides."
 	return overrides;
 }
 
 function restoreStatesAndValues(overrides) {
-	// "If there is some editable Text node effectively contained in the active
-	// range, then for each (command, override) pair in overrides, in order:"
-	if (getAllEffectivelyContainedNodes(getActiveRange()).some(function(node) {
-		return isEditable(node) && node.nodeType == Node.TEXT_NODE
-	})) {
+	// "Let node be the first editable Text node effectively contained in the
+	// active range, or null if there is none."
+	var node = getAllEffectivelyContainedNodes(getActiveRange())
+		.filter(function(node) { return isEditable(node) && node.nodeType == Node.TEXT_NODE })[0];
+
+	// "If node is not null, then for each (command, override) pair in
+	// overrides, in order:"
+	if (node) {
 		for (var i = 0; i < overrides.length; i++) {
 			var command = overrides[i][0];
 			var override = overrides[i][1];
@@ -4067,13 +4086,34 @@
 				myExecCommand(command);
 			}
 
-			// "If override is a string, and queryCommandValue(command) returns
-			// something different from override, call execCommand(command,
-			// false, override)."
+			// "If override is a string, and command is not "fontSize", and
+			// queryCommandValue(command) returns something not equivalent to
+			// override, call execCommand(command, false, override)."
 			if (typeof override == "string"
-			&& !valuesEqual(command, myQueryCommandValue(command), override)) {
+			&& command != "fontsize"
+			&& !areEquivalentValues(command, myQueryCommandValue(command), override)) {
 				myExecCommand(command, false, override);
 			}
+
+			// "If override is a string; and command is "fontSize"; and either
+			// there is a value override for "fontSize" that is not equal to
+			// override, or there is no value override for "fontSize" and
+			// node's effective command value for "fontSize" is not loosely
+			// equivalent to override: call execCommand("fontSize", false,
+			// override)."
+			if (typeof override == "string"
+			&& command == "fontsize"
+			&& (
+				(
+					getValueOverride("fontsize") !== undefined
+					&& getValueOverride("fontsize") !== override
+				) || (
+					getValueOverride("fontsize") === undefined
+					&& !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)
+				)
+			)) {
+				myExecCommand("fontsize", false, override);
+			}
 		}
 
 	// "Otherwise, for each (command, override) pair in overrides, in order:"
--- a/preprocess	Thu Jul 28 15:24:15 2011 -0600
+++ b/preprocess	Fri Jul 29 14:43:56 2011 -0600
@@ -44,6 +44,7 @@
     'em': '<code data-anolis-spec=html title="the em element">em</code>',
     'endnode': '<span data-anolis-spec=domrange title=concept-range-end>end</span> <span data-anolis-spec=domrange title=concept-boundary-point-node>node</span>',
     'endoffset': '<span data-anolis-spec=domrange title=concept-range-end>end</span> <span data-anolis-spec=domrange title=concept-boundary-point-offset>offset</span>',
+    'equivalent': '<span title="equivalent values">equivalent</span>',
     'extractcontents': '<code data-anolis-spec=domrange title=dom-Range-extractContents>extractContents()</code>',
     'firstchild': '<code data-anolis-spec=domcore title=dom-Node-firstChild>firstChild</code>',
     'followingsibling': '<span data-anolis-spec=domcore title="concept-tree-following-sibling">following sibling</span>',
@@ -64,6 +65,7 @@
     'length': '<span data-anolis-spec=domrange title=concept-node-length>length</span>',
     'li': '<code data-anolis-spec=html title="the li element">li</code>',
     'localname': '<span data-anolis-spec=domcore title=concept-element-local-name>local name</span>',
+    'looselyequivalent': '<span title="loosely equivalent values">loosely equivalent</span>',
     'namespace': '<span data-anolis-spec=domcore title=concept-element-namespace>namespace</span>',
     'nextsibling': '<code data-anolis-spec=domcore title=dom-Node-nextSibling>nextSibling</code>',
     'node': '<span data-anolis-spec=domcore title=concept-node>node</span>',
@@ -133,6 +135,7 @@
     'appendchild': '<code data-anolis-spec=domcore title=dom-Node-appendChild>appendChild(\\1)</code>',
     'createelement': '<code data-anolis-spec=domcore title=dom-Document-createElement>createElement(\\1)</code>',
     'deletedata': '<code data-anolis-spec=domcore title=dom-CharacterData-deleteData>deleteData(\\1)</code>',
+    'execcommand': '<code title=execCommand()>execCommand(\\1)</code>',
     'extend': '<code data-anolis-spec=domrange title=dom-Selection-extend>extend(\\1)</code>',
     'insertdata': '<code data-anolis-spec=domcore title=dom-CharacterData-insertData>insertData(\\1)</code>',
     'insertnode': '<code data-anolis-spec=domrange title=dom-Range-insertNode>insertNode(\\1)</code>',
--- a/source.html	Thu Jul 28 15:24:15 2011 -0600
+++ b/source.html	Fri Jul 29 14:43:56 2011 -0600
@@ -165,9 +165,7 @@
 <ul>
   <li>Need to make CSS terminology more precise, about setting/unsetting CSS
   properties.  The intent is to modify the style attribute, CSSOM-style.
-  Likewise, CSS value comparisons need to be done after serializing both
-  values, so "bold" == "700" and "red" == "#f00" and so on.  Suggestions
-  appreciated on how I should spec this.
+  Suggestions appreciated on how I should spec this.
 
   <li>I use [[resval]] instead of computed or used or anything like that, just
   because that's what my test implementation uses (via getComputedStyle).  This
@@ -1335,6 +1333,29 @@
 <p class=note>Conceptually, a simple modifiable element is a modifiable element
 which specifies a value for at most one command.
 
+<p>Two quantities are <dfn>equivalent values</dfn> for a <span>command</span>
+if either both are null, or both are strings and they're equal and the
+<span>command</span> does not define any <span>equivalent values</span>, or
+both are strings and the <span>command</span> defines <span>equivalent
+values</span> and they match the definition.
+
+<p>Two quantities are <dfn>loosely equivalent values</dfn> for a
+<span>command</span> if either they are <span>equivalent values</span> for the
+<span>command</span>, or if the <span>command</span> is <span>the <code
+title>fontSize</code> command</span>; one of the quantities is one of
+"xx-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
+and the other quantity is the [[resval]] of "font-size" on a [[font]] element
+whose [[fontsize]] attribute has the corresponding value set ("1" through "7"
+respectively).
+
+<p class=note>Loose equivalence needs to be used when comparing effective
+command values to other values, while regular equivalence is used in other
+cases.  The effective command value for fontSize is converted to pixels, so
+comparing it to a specified value literally would produce false negatives.  But
+a <em>specified</em> value in pixels is actually different from a
+<em>specified</em> value like "small" or "x-large", because there is no precise
+mapping from such keywords to pixels.
+
 <p>If a <span>command</span> has <dfn>inline command activated values</dfn>
 defined but nothing else defines when it is <span>indeterminate</span>, it is
 <span>indeterminate</span> if among <span>editable</span> [[text]] nodes
@@ -1577,13 +1598,16 @@
   <var>candidate</var> has exactly one [[child]], and that [[child]] is also a
   <span>modifiable element</span>, and <var>candidate</var> is not a
   <span>simple modifiable element</span> or <var>candidate</var>'s
-  <span>specified command value</span> for <var>command</var> is not <var>new
-  value</var>, set <var>candidate</var> to its [[child]].
+  <span>specified command value</span> for <var>command</var> is not <span
+  title="equivalent values">equivalent</span> to <var>new value</var>, set
+  <var>candidate</var> to its [[child]].
 
   <li>If <var>candidate</var> is <var>node</var>, or is not a <span>simple
-  modifiable element</span>, or its <span>specified command value</span> and
-  <span>effective command value</span> for <var>command</var> are not both
-  <var>new value</var>, abort these steps.
+  modifiable element</span>, or its <span>specified command value</span> is not
+  <span title="equivalent values">equivalent</span> to <var>new value</var>, or
+  its <span>effective command value</span> is not <span title="loosely
+  equivalent values">loosely equivalent</span> to <var>new value</var>, abort
+  these steps.
 
   <li>While <var>candidate</var> has [[children]], insert the first [[child]]
   of <var>candidate</var> into <var>candidate</var>'s [[parent]] immediately
@@ -1670,10 +1694,11 @@
     with <var>new value</var> null.
 
     <li>Otherwise, if <var>ancestor</var> is an [[element]] and its
-    <span>specified command value</span> for <var>command</var> is different
-    from <var>value</var>, or if <var>ancestor</var> is not an [[element]] and
-    <var>value</var> is not null, <span>force the value</span> of
-    <var>command</var> to <var>value</var> on <var>node</var>.
+    <span>specified command value</span> for <var>command</var> is not <span
+    title="equivalent values">equivalent</span> to <var>value</var>, or if
+    <var>ancestor</var> is not an [[element]] and <var>value</var> is not null,
+    <span>force the value</span> of <var>command</var> to <var>value</var> on
+    <var>node</var>.
   </ol>
 </ol>
 
@@ -1799,7 +1824,8 @@
   algorithm. <!-- E.g., a text node child of a document fragment. -->
 
   <li>If the <span>effective command value</span> of <var>command</var> is
-  <var>new value</var> on <var>node</var>, abort this algorithm.
+  [[looselyequivalent]] to <var>new value</var> on <var>node</var>, abort this
+  algorithm.
 
   <li>Let <var>current ancestor</var> be <var>node</var>'s [[parent]].
 
@@ -1807,9 +1833,9 @@
 
   <li>While <var>current ancestor</var> is an <span>editable</span> [[element]]
   and the <span>effective command value</span> of <var>command</var> is not
-  <var>new value</var> on it, append <var>current ancestor</var> to
-  <var>ancestor list</var>, then set <var>current ancestor</var> to its
-  [[parent]].
+  [[looselyequivalent]] to <var>new value</var> on it, append <var>current
+  ancestor</var> to <var>ancestor list</var>, then set <var>current
+  ancestor</var> to its [[parent]].
 
   <li>If <var>ancestor list</var> is empty, abort this algorithm.
 
@@ -1853,8 +1879,9 @@
   we're styling something that includes the first or last child).
   -->
   <li>If the <span>effective command value</span> of <var>command</var> is not
-  <var>new value</var> on the [[parent]] of the last member of <var>ancestor
-  list</var>, and <var>new value</var> is not null, abort this algorithm.
+  [[looselyequivalent]] to <var>new value</var> on the [[parent]] of the last
+  member of <var>ancestor list</var>, and <var>new value</var> is not null,
+  abort this algorithm.
 
   <li>While <var>ancestor list</var> is not empty:
 
@@ -1882,8 +1909,9 @@
       <var>child</var>.
 
       <li>If <var>child</var> is an [[element]] whose <span>specified command
-      value</span> for <var>command</var> is neither null nor equal to
-      <var>propagated value</var>, continue with the next <var>child</var>.
+      value</span> for <var>command</var> is neither null nor <span
+      title="equivalent values">equivalent</span> to <var>propagated
+      value</var>, continue with the next <var>child</var>.
       <!--
       TODO: This will be incorrect for relative font sizes.  If the font size
       on the parent was removed and the font size on the child is in ems or
@@ -1943,24 +1971,25 @@
 
     <li><span>Wrap</span> the one-[[node]] list consisting of <var>node</var>,
     with <span>sibling criteria</span> matching a <span>simple modifiable
-    element</span> whose <span>specified command value</span> and
-    <span>effective command value</span> for <var>command</var> are both
-    <var>new value</var>, and with <span>new parent instructions</span>
-    returning null.
+    element</span> whose <span>specified command value</span> is [[equivalent]]
+    to <var>new value</var> and whose <span>effective command value</span> is
+    [[looselyequivalent]] to <var>new value</var>, and with <span>new parent
+    instructions</span> returning null.
     <!-- The new parent instructions are too complicated to reasonably feed
     into the wrap algorithm. -->
   </ol>
 
   <li>If the <span>effective command value</span> of <var>command</var> is
-  <var>new value</var> on <var>node</var>, abort this algorithm.
+  [[looselyequivalent]] to <var>new value</var> on <var>node</var>, abort this
+  algorithm.
 
   <li>If <var>node</var> is not an <span>allowed child</span> of "span":
 
   <ol>
     <li>Let <var>children</var> be all [[children]] of <var>node</var>,
     omitting any that are [[element]]s whose <span>specified command
-    value</span> for <var>command</var> is neither null nor equal to <var>new
-    value</var>.
+    value</span> for <var>command</var> is neither null nor <span
+    title="equivalent values">equivalent</span> to <var>new value</var>.
 
     <li><span>Force the value</span> of each [[node]] in <var>children</var>,
     with <var>command</var> and <var>new value</var> as in this invocation of
@@ -1975,7 +2004,8 @@
   </ol>
 
   <li>If the <span>effective command value</span> of <var>command</var> is
-  <var>new value</var> on <var>node</var>, abort this algorithm.
+  [[looselyequivalent]] to <var>new value</var> on <var>node</var>, abort this
+  algorithm.
 
   <li>Let <var>new parent</var> be null.
 
@@ -2124,10 +2154,13 @@
   -->
 
   <li>If the <span>effective command value</span> of <var>command</var> for
-  <var>new parent</var> is not <var>new value</var>, and the <span>relevant CSS
-  property</span> for <var>command</var> is not null, set that CSS property of
-  <var>new parent</var> to <var>new value</var> (if the new value would be
-  valid).
+  <var>new parent</var> is not [[looselyequivalent]] to <var>new value</var>,
+  and the <span>relevant CSS property</span> for <var>command</var> is not
+  null, set that CSS property of <var>new parent</var> to <var>new value</var>
+  (if the new value would be valid).
+
+  <p class=XXX>Need to be explicit.  I think "if the new value would be valid"
+  means "if the new value isn't xxx-large for font-size", need to double-check.
 
   <li>If <var>command</var> is "strikethrough", and <var>new value</var> is
   "line-through", and the <span>effective command value</span> of
@@ -2143,8 +2176,8 @@
   <span>preserving ranges</span>.
 
   <li>If <var>node</var> is an [[element]] and the <span>effective command
-  value</span> of <var>command</var> for <var>node</var> is not <var>new
-  value</var>:
+  value</span> of <var>command</var> for <var>node</var> is not
+  [[looselyequivalent]] to <var>new value</var>:
 
   <ol>
     <li>Insert <var>node</var> into the [[parent]] of <var>new parent</var>
@@ -2154,8 +2187,8 @@
 
     <li>Let <var>children</var> be all [[children]] of <var>node</var>,
     omitting any that are [[element]]s whose <span>specified command
-    value</span> for <var>command</var> is neither null nor equal to <var>new
-    value</var>.
+    value</span> for <var>command</var> is neither null nor <span
+    title="equivalent values">equivalent</span> to <var>new value</var>.
 
     <li><span>Force the value</span> of each [[node]] in <var>children</var>,
     with <var>command</var> and <var>new value</var> as in this invocation of
@@ -2260,10 +2293,6 @@
     <li>If <var>new value</var> is null, unset the <span>value override</span>
     (if any).
 
-    <li>Otherwise, if <var>command</var> is "fontSize", set the <span>value
-    override</span> to the <span>legacy font size for</span> the result of
-    converting <var>new value</var> to pixels.
-
     <li>Otherwise, if <var>command</var> has a <span>value</span> specified,
     set the <span>value override</span> to <var>new value</var>.
 
@@ -2399,6 +2428,10 @@
 
 <p><span>Relevant CSS property</span>: "background-color"
 
+<p><span>Equivalent values</span>: Either both strings are valid CSS colors and
+have the same red, green, blue, and alpha components, or neither string is a
+valid CSS color.
+
 <!-- @} -->
 <h3><dfn>The <code title>bold</code> command</dfn></h3>
 <!-- @{ -->
@@ -2434,6 +2467,9 @@
 
 <p><span>Relevant CSS property</span>: "font-weight"
 
+<p><span>Equivalent values</span>: Either the two strings are equal, or one is
+"bold" and the other is "700", or one is "normal" and the other is "400".
+
 <!-- @} -->
 <h3><dfn>The <code title>createLink</code> command</dfn></h3>
 <!-- @{ -->
@@ -2811,6 +2847,10 @@
 
 <p><span>Relevant CSS property</span>: "color"
 
+<p><span>Equivalent values</span>: Either both strings are valid CSS colors and
+have the same red, green, blue, and alpha components, or neither string is a
+valid CSS color.
+
 <!-- @} -->
 <h3><dfn>The <code title>hiliteColor</code> command</dfn></h3>
 <!-- @{ -->
@@ -2854,6 +2894,10 @@
 
 <p><span>Relevant CSS property</span>: "background-color"
 
+<p><span>Equivalent values</span>: Either both strings are valid CSS colors and
+have the same red, green, blue, and alpha components, or neither string is a
+valid CSS color.
+
 <!-- @} -->
 <h3><dfn>The <code title>italic</code> command</dfn></h3>
 <!-- @{ -->
@@ -3628,9 +3672,13 @@
   the contents".
   -->
 
-  <li>For each <var>command</var> in the list "fontName", "fontSize",
-  "foreColor", "hiliteColor", in order: add (<var>command</var>,
-  <var>command</var>'s <span>value</span>) to <var>overrides</var>.
+  <li>For each <var>command</var> in the list "fontName", "foreColor",
+  "hiliteColor", in order: add (<var>command</var>, <var>command</var>'s
+  <span>value</span>) to <var>overrides</var>.
+
+  <!-- Special case for fontSize, because its values are weird. -->
+  <li>Add ("fontSize", <var>node</var>'s <span>effective command value</span>
+  for "fontSize") to <var>overrides</var>.
 
   <li>Return <var>overrides</var>.
 </ol>
@@ -3640,10 +3688,12 @@
 <span>record current states and values</span> algorithm:
 
 <ol>
-  <li>If there is some <span>editable</span> [[text]] node <span>effectively
-  contained</span> in the <span>active range</span>, then for each
-  (<var>command</var>, <var>override</var>) pair in <var>overrides</var>, in
-  order:
+  <li>Let <var>node</var> be the first <span>editable</span> [[text]] node
+  <span>effectively contained</span> in the <span>active range</span>, or null
+  if there is none.
+
+  <li>If <var>node</var> is not null, then for each (<var>command</var>,
+  <var>override</var>) pair in <var>overrides</var>, in order:
 
   <ol>
     <li>If <var>override</var> is a boolean, and <code
@@ -3651,11 +3701,21 @@
     returns something different from <var>override</var>, call <code
     title=execCommand()>execCommand(<var>command</var>)</code>.
 
-    <li>If <var>override</var> is a string, and <code
+    <li>If <var>override</var> is a string, and <var>command</var> is not
+    "fontSize", and <code
     title=queryCommandValue()>queryCommandValue(<var>command</var>)</code>
-    returns something different from <var>override</var>, call <code
+    returns something not <span title="equivalent values">equivalent</span> to
+    <var>override</var>, call <code
     title=execCommand()>execCommand(<var>command</var>, false,
     <var>override</var>)</code>.
+
+    <li>If <var>override</var> is a string; and <var>command</var> is
+    "fontSize"; and either there is a <span>value override</span> for
+    "fontSize" that is not equal to <var>override</var>, or there is no
+    <span>value override</span> for "fontSize" and <var>node</var>'s
+    <span>effective command value</span> for "fontSize" is not
+    [[looselyequivalent]] to <var>override</var>: call
+    [[execcommand|"fontSize", false, <var>override</var>]].
   </ol>
 
   <li>Otherwise, for each (<var>command</var>, <var>override</var>) pair in
--- a/tests.js	Thu Jul 28 15:24:15 2011 -0600
+++ b/tests.js	Fri Jul 29 14:43:56 2011 -0600
@@ -446,6 +446,8 @@
 		// Styled stuff with collapsed selection
 		'<p style=color:blue>foo<p>[]bar',
 		'<p style=color:blue>foo<p style=color:brown>[]bar',
+		'<p style=color:blue>foo<p style=color:rgba(0,0,255,1)>[]bar',
+		'<p style=color:transparent>foo<p style=color:rgba(0,0,0,0)>[]bar',
 		'<p>foo<p style=color:brown>[]bar',
 		'<p><font color=blue>foo</font><p>[]bar',
 		'<p><font color=blue>foo</font><p><font color=brown>[]bar</font>',
@@ -3755,16 +3757,7 @@
 			value = "#" + value;
 		}
 	} else if (command == "fontsize") {
-		value = normalizeFontSize(value);
-		var font = document.createElement("font");
-		document.body.appendChild(font);
-		if (value == "xxx-large") {
-			font.size = 7;
-		} else {
-			font.style.fontSize = value;
-		}
-		value = getLegacyFontSize(parseInt(getComputedStyle(font).fontSize));
-		document.body.removeChild(font);
+		afterValue = legacySizeToCss(afterValue);
 	} else if (command == "formatblock") {
 		value = value.replace(/^<(.*)>$/, "$1").toLowerCase();
 	} else if (command == "unlink") {
@@ -3795,7 +3788,7 @@
 		// And in all other cases, the value afterwards has to be the one we
 		// set.
 		afterDiv.lastChild.className =
-			valuesEqual(command, afterValue, value)
+			areEquivalentValues(command, afterValue, value)
 				? "good-result"
 				: "bad-result";
 	}