Make whitespace canonicalization robust
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Tue, 05 Jul 2011 14:00:32 -0600
changeset 363 02da8d6c63db
parent 362 e3392e61900d
child 364 b15222941c6e
Make whitespace canonicalization robust

This now correctly handles things like inserting a space at
<b>foo[]</b>bar (space instead of nbsp), or backspacing at
<b>foo </b> []bar (deletes both spaces). It most likely still fails in
some cases, but hopefully not in extremely common ones.
editcommands.html
implementation.js
source.html
tests.js
--- a/editcommands.html	Tue Jul 05 12:06:30 2011 -0600
+++ b/editcommands.html	Tue Jul 05 14:00:32 2011 -0600
@@ -3410,72 +3410,131 @@
 <p>To <dfn id=canonicalize-whitespace>canonicalize whitespace</dfn> at (<var title="">node</var>,
 <var title="">offset</var>):
 
-<p class=XXX>This algorithm fails in all kinds of common cases, like any
-non-text node, or whitespace that spans multiple text nodes.  Needs lots of
-fixing.
-
 <ol>
-  <li>If <var title="">node</var> is not 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, or is not
-  <a href=#editable>editable</a>, or 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>'s <a href=http://www.w3.org/TR/CSS21/cascade.html#computed-value>computed value</a> for "white-space" is
-  "pre" or "pre-wrap", abort these steps.
+  <li>If <var title="">node</var> is neither <a href=#editable>editable</a> nor an <a href=#editing-host>editing
+  host</a>, abort these steps.
+
+  <li>Let <var title="">start node</var> equal <var title="">node</var> and let <var title="">start
+  offset</var> equal <var title="">offset</var>.
 
   <!-- First go to the beginning of the current whitespace run. -->
-  <li>Let <var title="">start offset</var> equal <var title="">offset</var>.
-
-  <li>While <var title="">start offset</var> is positive and the (<var title="">start
-  offset</var> &minus; 1)st <a href=http://es5.github.com/#x8.4>element</a> of <var title="">node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is a
-  space (0x0020) or non-breaking space (0x00A0), subtract one from <var title="">start
-  offset</var>.
+  <li>Repeat the following steps:
+
+  <ol>
+    <li>If <var title="">start node</var> has a <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=#in-the-same-editing-host>in the same editing
+    host</a> with <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-indexof title=concept-indexof>index</a> <var title="">start offset</var> minus one, set
+    <var title="">start node</var> to 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>, then set <var title="">start offset</var>
+    to <var title="">start node</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a>.
+
+    <li>Otherwise, if <var title="">start offset</var> is zero and <var title="">start node</var>
+    does not <a href=#follows-a-line-break title="follows a line break">follow a line break</a> and
+    <var title="">start 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=#in-the-same-editing-host>in the same editing
+    host</a>, set <var title="">start offset</var> to <var title="">start node</var>'s
+    <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-indexof title=concept-indexof>index</a>, then set <var title="">start 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>.
+
+    <p class=XXX>Following a line break is unlikely to be the right criterion.
+
+    <li>Otherwise, if <var title="">start node</var> is 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 and 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>'s <a href=http://www.w3.org/TR/CSS21/cascade.html#computed-value>computed value</a> for "white-space" is neither "pre" nor "pre-wrap"
+    and <var title="">start offset</var> is not zero and the (<var title="">start offset</var>
+    &minus; 1)st <a href=http://es5.github.com/#x8.4>element</a> of <var title="">start node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is a space
+    (0x0020) or non-breaking space (0x00A0), subtract one from <var title="">start
+    offset</var>.
+
+    <li>Otherwise, break from this loop.
+  </ol>
 
   <!-- Now collapse any consecutive spaces. -->
-  <li>Let <var title="">end offset</var> equal <var title="">start offset</var>.
-
-  <li>While <var title="">end offset</var> is less than <var title="">node</var>'s <a href=http://es5.github.com/#x15.5.5.1>length</a>,
-  and the <var title="">end offset</var>th <a href=http://es5.github.com/#x8.4>element</a> of <var title="">node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is
-  0x0020 or 0x00A0:
+  <li>Let <var title="">end node</var> equal <var title="">start node</var> and <var title="">end
+  offset</var> equal <var title="">start offset</var>.
+
+  <li>Let <var title="">length</var> equal zero.
+
+  <li>Let <var title="">follows space</var> be false.
+
+  <li>Repeat the following steps:
 
   <ol>
-    <li>Let <var title="">length</var> equal zero.
-
-    <li>While <var title="">end offset</var> plus <var title="">length</var> is less than
-    <var title="">node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-length><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-length>length</a></code>, and the (<var title="">end offset</var> +
-    <var title="">length</var>)th <a href=http://es5.github.com/#x8.4>element</a> of <var title="">node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is 0x0020,
-    add one to <var title="">length</var>.
-
-    <li>If <var title="">length</var> is greater than one, call <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData(<var title="">end
-    offset</var> + 1, <var title="">length</var> &minus; 1)</a></code> on <var title="">node</var>.
-
-    <li>Add one to <var title="">end offset</var>.
+    <li>If <var title="">end node</var> has a <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=#in-the-same-editing-host>in the same editing
+    host</a> with <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-indexof title=concept-indexof>index</a> <var title="">end offset</var>, set <var title="">end node</var>
+    to 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>, then set <var title="">end offset</var> to zero.
+
+    <li>Otherwise, if <var title="">end offset</var> is <var title="">end node</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a>
+    and <var title="">end node</var> does not <a href=#precedes-a-line-break title="precedes a line
+    break">precede a line break</a> and <var title="">end 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=#in-the-same-editing-host>in the same editing host</a>, set <var title="">end offset</var> to one
+    plus <var title="">end node</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-indexof title=concept-indexof>index</a>, then set <var title="">end 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>.
+
+    <p class=XXX>Preceding a line break is unlikely to be the right criterion.
+
+    <li>Otherwise, if <var title="">end node</var> is 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 and 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>'s <a href=http://www.w3.org/TR/CSS21/cascade.html#computed-value>computed value</a> for "white-space" is neither "pre" nor "pre-wrap"
+    and <var title="">end offset</var> is not <var title="">end node</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a> and the
+    <var title="">end offset</var>th <a href=http://es5.github.com/#x8.4>element</a> of <var title="">end node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is a
+    space (0x0020) or non-breaking space (0x00A0):
+
+    <ol>
+      <li>If <var title="">follows space</var> is true and the <var title="">end offset</var>th
+      <a href=http://es5.github.com/#x8.4>element</a> of <var title="">end node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is a space (0x0020), call
+      <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData(<var title="">end offset</var>, 1)</a></code> on <var title="">end node</var>, then
+      continue this loop from the beginning.
+
+      <li>Set <var title="">follows space</var> to true if the <var title="">end offset</var>th
+      <a href=http://es5.github.com/#x8.4>element</a> of <var title="">end node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code> is a space (0x0020), false
+      otherwise.
+
+      <li>Add one to <var title="">end offset</var>.
+
+      <li>Add one to <var title="">length</var>.
+    </ol>
+
+    <li>Otherwise, break from this loop.
   </ol>
 
   <!-- Now replace with the canonical sequence. -->
   <li>Let <var title="">replacement whitespace</var> be the <a href=#canonical-space-sequence>canonical space
-  sequence</a> of length <var title="">end offset</var> minus <var title="">start
-  offset</var>.  <var title="">non-breaking start</var> is true if <var title="">start
-  offset</var> is zero and false otherwise, and <var title="">non-breaking end</var> is
-  true if <var title="">end offset</var> is <var title="">node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-length><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-length>length</a></code> and false
-  otherwise.
-
-  <li>While <var title="">start offset</var> is less than <var title="">end offset</var>:
+  sequence</a> of length <var title="">length</var>.  <var title="">non-breaking start</var>
+  is true if <var title="">start offset</var> is zero and <var title="">start node</var>
+  <a href=#follows-a-line-break>follows a line break</a>, and false otherwise.  <var title="">non-breaking
+  end</var> is true if <var title="">end offset</var> is <var title="">end node</var>'s
+  <code class=external data-anolis-spec=domcore title=dom-CharacterData-length><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-length>length</a></code> and <var title="">end node</var> <a href=#precedes-a-line-break>precedes a line break</a>, and
+  false otherwise.
+
+  <li>While (<var title="">start node</var>, <var title="">start offset</var>) is <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-bp-before title=concept-bp-before>before</a>
+  (<var title="">end node</var>, <var title="">end offset</var>):
 
   <ol>
-    <li>Remove the first <a href=http://es5.github.com/#x8.4>element</a> from <var title="">replacement whitespace</var>, and
-    let <var title="">element</var> be that <a href=http://es5.github.com/#x8.4>element</a>.
-
-    <li>If <var title="">element</var> is not the same as the <var title="">start offset</var>th
-    <a href=http://es5.github.com/#x8.4>element</a> of <var title="">node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code>:
+    <li>If <var title="">start node</var> has a <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> with <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-indexof title=concept-indexof>index</a> <var title="">start
+    offset</var>, set <var title="">start node</var> to 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>, then set
+    <var title="">start offset</var> to zero.
+
+    <li>Otherwise, if <var title="">start node</var> is not 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 or if
+    <var title="">start offset</var> is <var title="">start node</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a>, set
+    <var title="">start offset</var> to one plus <var title="">start node</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-indexof title=concept-indexof>index</a>, then
+    set <var title="">start 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>Otherwise:
 
     <ol>
-      <!-- We need to insert then delete, so that we don't change range
-      boundary points. -->
-      <li>Call <code class=external data-anolis-spec=domcore title=dom-CharacterData-insertData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-insertdata>insertData(<var title="">start offset</var>, <var title="">element</var>)</a></code> on
-      <var title="">node</var>.
-
-      <li>Call <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData(<var title="">start offset</var> + 1, 1)</a></code> on
-      <var title="">node</var>.
+      <li>Remove the first <a href=http://es5.github.com/#x8.4>element</a> from <var title="">replacement whitespace</var>,
+      and let <var title="">element</var> be that <a href=http://es5.github.com/#x8.4>element</a>.
+
+      <li>If <var title="">element</var> is not the same as the <var title="">start
+      offset</var>th <a href=http://es5.github.com/#x8.4>element</a> of <var title="">start node</var>'s <code class=external data-anolis-spec=domcore title=dom-CharacterData-data><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-data>data</a></code>:
+
+      <ol>
+        <!-- We need to insert then delete, so that we don't change range
+        boundary points. -->
+        <li>Call <code class=external data-anolis-spec=domcore title=dom-CharacterData-insertData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-insertdata>insertData(<var title="">start offset</var>, <var title="">element</var>)</a></code> on
+        <var title="">start node</var>.
+
+        <li>Call <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData(<var title="">start offset</var> + 1, 1)</a></code> on
+        <var title="">start node</var>.
+      </ol>
+
+      <li>Add one to <var title="">start offset</var>.
     </ol>
-
-    <li>Add one to <var title="">start offset</var>.
   </ol>
 </ol>
 
@@ -3908,58 +3967,61 @@
 
     <li><a href=#canonicalize-whitespace>Canonicalize whitespace</a> at (<var title="">start node</var>,
     <var title="">start offset</var>).
+
+    <li>Set <var title="">range</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range-end title=concept-range-end>end</a> to its <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range-start title=concept-range-start>start</a>.
+
+    <li>Abort these steps.
   </ol>
 
-  <li>Otherwise:
+  <li>If <var title="">start node</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#text>Text</a></code> node, call
+  <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData()</a></code> on it, with <var title="">start offset</var> as the first argument and
+  (<a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a> of <var title="">start node</var> &minus; <var title="">start offset</var>) as
+  the second argument.
+
+  <li>Let <var title="">node list</var> be a list of <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>nodes</a>, initially empty.
+
+  <li>For each <var title="">node</var> <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#contained>contained</a> in <var title="">range</var>, append
+  <var title="">node</var> to <var title="">node list</var> if the last member of <var title="">node
+  list</var> (if any) is not an <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 <var title="">node</var>;
+  <var title="">node</var> is <a href=#editable>editable</a>; and <var title="">node</var> is not a
+  <code class=external data-anolis-spec=html title="the thead element"><a href=http://www.whatwg.org/html/#the-thead-element>thead</a></code>, <code class=external data-anolis-spec=html title="the tbody element"><a href=http://www.whatwg.org/html/#the-tbody-element>tbody</a></code>, <code class=external data-anolis-spec=html title="the tfoot element"><a href=http://www.whatwg.org/html/#the-tfoot-element>tfoot</a></code>, <code class=external data-anolis-spec=html title="the tr element"><a href=http://www.whatwg.org/html/#the-tr-element>tr</a></code>, <code class=external data-anolis-spec=html title="the th element"><a href=http://www.whatwg.org/html/#the-th-element>th</a></code>, or <code class=external data-anolis-spec=html title="the td element"><a href=http://www.whatwg.org/html/#the-td-element>td</a></code>.
+  <!--
+  IE9 doesn't seem to let you do any intercell deletions: the delete key does
+  nothing if you select across multiple cells.  Firefox 5.0a2 and Opera 11.11
+  behave as the spec says, not removing any table things.  Chrome 13 dev will
+  remove entire rows if selected.  Note that IE, Firefox, Word 2007, and
+  OpenOffice.org 3.2.1 Ubuntu all switch to a magic cell-selection mode when
+  you try to select between cells, at least in some cases, instead of selecting
+  letter-by-letter.
+  -->
+
+  <li>For each <var title="">node</var> in <var title="">node list</var>:
 
   <ol>
-    <li>If <var title="">start node</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#text>Text</a></code> node,
-    call <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData()</a></code> on it, with <var title="">start offset</var> as the first
-    argument and (<a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a> of <var title="">start node</var> &minus; <var title="">start
-    offset</var>) as the second argument.  Then <a href=#canonicalize-whitespace>canonicalize
-    whitespace</a> at (<var title="">start node</var>, <var title="">start offset</var>).
-
-    <li>Let <var title="">node list</var> be a list of <a class=external data-anolis-spec=domcore href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>nodes</a>, initially empty.
-
-    <li>For each <var title="">node</var> <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#contained>contained</a> in <var title="">range</var>, append
-    <var title="">node</var> to <var title="">node list</var> if the last member of <var title="">node
-    list</var> (if any) is not an <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 <var title="">node</var>;
-    <var title="">node</var> is <a href=#editable>editable</a>; and <var title="">node</var> is not a
-    <code class=external data-anolis-spec=html title="the thead element"><a href=http://www.whatwg.org/html/#the-thead-element>thead</a></code>, <code class=external data-anolis-spec=html title="the tbody element"><a href=http://www.whatwg.org/html/#the-tbody-element>tbody</a></code>, <code class=external data-anolis-spec=html title="the tfoot element"><a href=http://www.whatwg.org/html/#the-tfoot-element>tfoot</a></code>, <code class=external data-anolis-spec=html title="the tr element"><a href=http://www.whatwg.org/html/#the-tr-element>tr</a></code>, <code class=external data-anolis-spec=html title="the th element"><a href=http://www.whatwg.org/html/#the-th-element>th</a></code>, or <code class=external data-anolis-spec=html title="the td element"><a href=http://www.whatwg.org/html/#the-td-element>td</a></code>.
-    <!--
-    IE9 doesn't seem to let you do any intercell deletions: the delete key does
-    nothing if you select across multiple cells.  Firefox 5.0a2 and Opera 11.11
-    behave as the spec says, not removing any table things.  Chrome 13 dev will
-    remove entire rows if selected.  Note that IE, Firefox, Word 2007, and
-    OpenOffice.org 3.2.1 Ubuntu all switch to a magic cell-selection mode when
-    you try to select between cells, at least in some cases, instead of
-    selecting letter-by-letter.
-    -->
-
-    <li>For each <var title="">node</var> in <var title="">node list</var>:
-
-    <ol>
-      <li>Let <var title="">parent</var> be 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="">node</var>.
-
-      <li>Remove <var title="">node</var> from <var title="">parent</var>.
-
-      <li>While <var title="">parent</var> is an <a href=#editable>editable</a> <a href=#inline-node>inline
-      node</a> with <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a> 0, let <var title="">grandparent</var> be 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="">parent</var>, then remove <var title="">parent</var> from
-      <var title="">grandparent</var>, then set <var title="">parent</var> to
-      <var title="">grandparent</var>.
-
-      <li>If <var title="">parent</var> is <a href=#editable>editable</a> or an <a href=#editing-host>editing
-      host</a>, is not an <a href=#inline-node>inline node</a>, and has no <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>,
-      call <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("br")</a></code> on the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#context-object>context object</a> and append the
-      result as the last <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="">parent</var>.
-    </ol>
-
-    <li>If <var title="">end node</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#text>Text</a></code> node, call
-    <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData(0, <var title="">end offset</var>)</a></code> on it, then <a href=#canonicalize-whitespace>canonicalize
-    whitespace</a> at (<var title="">end node</var>, 0).
+    <li>Let <var title="">parent</var> be 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="">node</var>.
+
+    <li>Remove <var title="">node</var> from <var title="">parent</var>.
+
+    <li>While <var title="">parent</var> is an <a href=#editable>editable</a> <a href=#inline-node>inline
+    node</a> with <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a> 0, let <var title="">grandparent</var> be 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="">parent</var>, then remove <var title="">parent</var> from
+    <var title="">grandparent</var>, then set <var title="">parent</var> to
+    <var title="">grandparent</var>.
+
+    <li>If <var title="">parent</var> is <a href=#editable>editable</a> or an <a href=#editing-host>editing
+    host</a>, is not an <a href=#inline-node>inline node</a>, and has no <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>,
+    call <code class=external data-anolis-spec=domcore title=dom-Document-createElement><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-createelement>createElement("br")</a></code> on the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#context-object>context object</a> and append the result
+    as the last <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="">parent</var>.
   </ol>
 
+  <li>If <var title="">end node</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#text>Text</a></code> node, call
+  <code class=external data-anolis-spec=domcore title=dom-CharacterData-deleteData><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-characterdata-deletedata>deleteData(0, <var title="">end offset</var>)</a></code> on it.
+
+  <li><a href=#canonicalize-whitespace>Canonicalize whitespace</a> at <var title="">range</var>'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>.
+
+  <li><a href=#canonicalize-whitespace>Canonicalize whitespace</a> at <var title="">range</var>'s <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-range-end title=concept-range-end>end</a>.
+
   <!--
   Now we need to merge blocks.  The simplest case is something like
 
@@ -6456,9 +6518,8 @@
 
   <li>Call <code class=external data-anolis-spec=domrange title=dom-Range-insertNode><a href=http://html5.org/specs/dom-range.html#dom-range-insertnode>insertNode(<var title="">text</var>)</a></code> on the <a href=#active-range>active range</a>.
 
-  <li>Call <code class=external data-anolis-spec=domrange title=dom-Selection-collapse><a href=http://html5.org/specs/dom-range.html#dom-selection-collapse>collapse(<var title="">text</var>, <var title="">length</var>)</a></code> on the
-  <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#context-object>context object</a>'s <code class=external data-anolis-spec=domrange><a href=http://html5.org/specs/dom-range.html#selection>Selection</a></code>, where <var title="">length</var> is the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#concept-node-length title=concept-node-length>length</a>
-  of <var title="">text</var>.
+  <li>Call <code class=external data-anolis-spec=domrange title=dom-Selection-collapse><a href=http://html5.org/specs/dom-range.html#dom-selection-collapse>collapse(<var title="">text</var>, 1)</a></code> on the <a class=external data-anolis-spec=domrange href=http://html5.org/specs/dom-range.html#context-object>context object</a>'s
+  <code class=external data-anolis-spec=domrange><a href=http://html5.org/specs/dom-range.html#selection>Selection</a></code>.
 </ol>
 
 
--- a/implementation.js	Tue Jul 05 12:06:30 2011 -0600
+++ b/implementation.js	Tue Jul 05 14:00:32 2011 -0600
@@ -3701,81 +3701,159 @@
 }
 
 function canonicalizeWhitespace(node, offset) {
-	// "If node is not a Text node, or is not editable, or its parent's
-	// computed value for "white-space" is "pre" or "pre-wrap", abort these
-	// steps."
-	if (node.nodeType != Node.TEXT_NODE
-	|| !isEditable(node)
-	|| ["pre", "pre-wrap"].indexOf(getComputedStyle(node.parentNode).whiteSpace) != -1) {
+	// "If node is neither editable nor an editing host, abort these steps."
+	if (!isEditable(node) && !isEditingHost(node)) {
 		return;
 	}
 
-	// "Let start offset equal offset."
+	// "Let start node equal node and let start offset equal offset."
+	var startNode = node;
 	var startOffset = offset;
 
-	// "While start offset is positive and the (start offset − 1)st element of
-	// node's data is a space (0x0020) or non-breaking space (0x00A0), subtract
-	// one from start offset."
-	while (startOffset > 0
-	&& /[ \xa0]/.test(node.data[startOffset - 1])) {
-		startOffset--;
-	}
-
-	// "Let end offset equal start offset."
+	// "Repeat the following steps:"
+	while (true) {
+		// "If start node has a child in the same editing host with index start
+		// offset minus one, set start node to that child, then set start
+		// offset to start node's length."
+		if (0 <= startOffset - 1
+		&& inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) {
+			startNode = startNode.childNodes[startOffset - 1];
+			startOffset = getNodeLength(startNode);
+
+		// "Otherwise, if start offset is zero and start node does not follow a
+		// line break and start node's parent is in the same editing host, set
+		// start offset to start node's index, then set start node to its
+		// parent."
+		} else if (startOffset == 0
+		&& !followsLineBreak(startNode)
+		&& inSameEditingHost(startNode, startNode.parentNode)) {
+			startOffset = getNodeIndex(startNode);
+			startNode = startNode.parentNode;
+
+		// "Otherwise, if start node is a Text node and its parent's computed
+		// value for "white-space" is neither "pre" nor "pre-wrap" and start
+		// offset is not zero and the (start offset − 1)st element of start
+		// node's data is a space (0x0020) or non-breaking space (0x00A0),
+		// subtract one from start offset."
+		} else if (startNode.nodeType == Node.TEXT_NODE
+		&& ["pre", "pre-wrap"].indexOf(getComputedStyle(startNode.parentNode).whiteSpace) == -1
+		&& startOffset != 0
+		&& /[ \xa0]/.test(startNode.data[startOffset - 1])) {
+			startOffset--;
+
+		// "Otherwise, break from this loop."
+		} else {
+			break;
+		}
+	}
+
+	// "Let end node equal start node and end offset equal start offset."
+	var endNode = startNode;
 	var endOffset = startOffset;
 
-	// "While end offset is less than node's length, and the end offsetth
-	// element of node's data is 0x0020 or 0x00A0:"
-	while (endOffset < node.length
-	&& /[ \xa0]/.test(node.data[endOffset])) {
-		// "Let length equal zero."
-		var length = 0;
-
-		// "While end offset plus length is less than node's length, and the
-		// (end offset + length)th element of node's data is 0x0020, add one to
-		// length."
-		while (endOffset + length < node.length
-		&& node.data[endOffset + length] == " ") {
+	// "Let length equal zero."
+	var length = 0;
+
+	// "Let follows space be false."
+	var followsSpace = false;
+
+	// "Repeat the following steps:"
+	while (true) {
+		// "If end node has a child in the same editing host with index end
+		// offset, set end node to that child, then set end offset to zero."
+		if (endOffset < endNode.childNodes.length
+		&& inSameEditingHost(endNode, endNode.childNodes[endOffset])) {
+			endNode = endNode.childNodes[endOffset];
+			endOffset = 0;
+
+		// "Otherwise, if end offset is end node's length and end node does not
+		// precede a line break and end node's parent is in the same editing
+		// host, set end offset to one plus end node's index, then set end node
+		// to its parent."
+		} else if (endOffset == getNodeLength(endNode)
+		&& !precedesLineBreak(endNode)
+		&& inSameEditingHost(endNode, endNode.parentNode)) {
+			endOffset = 1 + getNodeIndex(endNode);
+			endNode = endNode.parentNode;
+
+		// "Otherwise, if end node is a Text node and its parent's computed
+		// value for "white-space" is neither "pre" nor "pre-wrap" and end
+		// offset is not end node's length and the end offsetth element of
+		// end node's data is a space (0x0020) or non-breaking space (0x00A0):"
+		} else if (endNode.nodeType == Node.TEXT_NODE
+		&& ["pre", "pre-wrap"].indexOf(getComputedStyle(endNode.parentNode).whiteSpace) == -1
+		&& endOffset != getNodeLength(endNode)
+		&& /[ \xa0]/.test(endNode.data[endOffset])) {
+			// "If follows space is true and the end offsetth element of end
+			// node's data is a space (0x0020), call deleteData(end offset, 1)
+			// on end node, then continue this loop from the beginning."
+			if (followsSpace
+			&& " " == endNode.data[endOffset]) {
+				endNode.deleteData(endOffset, 1);
+				continue;
+			}
+
+			// "Set follows space to true if the end offsetth element of end
+			// node's data is a space (0x0020), false otherwise."
+			followsSpace = " " == endNode.data[endOffset];
+
+			// "Add one to end offset."
+			endOffset++;
+
+			// "Add one to length."
 			length++;
-		}
-
-		// "If length is greater than one, call deleteData(end offset + 1,
-		// length − 1) on node."
-		if (length > 1) {
-			node.deleteData(endOffset + 1, length - 1);
-		}
-
-		// "Add one to end offset."
-		endOffset++;
+
+		// "Otherwise, break from this loop."
+		} else {
+			break;
+		}
 	}
 
 	// "Let replacement whitespace be the canonical space sequence of length
-	// end offset minus start offset. non-breaking start is true if start
-	// offset is zero and false otherwise, and non-breaking end is true if end
-	// offset is node's length and false otherwise."
-	var replacementWhitespace = canonicalSpaceSequence(endOffset - startOffset,
-		startOffset == 0,
-		endOffset == node.length);
-
-	// "While start offset is less than end offset:"
-	while (startOffset < endOffset) {
-		// "Remove the first element from replacement whitespace, and let
-		// element be that element."
-		var element = replacementWhitespace[0];
-		replacementWhitespace = replacementWhitespace.slice(1);
-
-		// "If element is not the same as the start offsetth element of node's
-		// data:"
-		if (element != node.data[startOffset]) {
-			// "Call insertData(start offset, element) on node."
-			node.insertData(startOffset, element);
-
-			// "Call deleteData(start offset + 1, 1) on node."
-			node.deleteData(startOffset + 1, 1);
-		}
-
-		// "Add one to start offset."
-		startOffset++;
+	// length. non-breaking start is true if start offset is zero and start
+	// node follows a line break, and false otherwise. non-breaking end is true
+	// if end offset is end node's length and end node precedes a line break,
+	// and false otherwise."
+	var replacementWhitespace = canonicalSpaceSequence(length,
+		startOffset == 0 && followsLineBreak(startNode),
+		endOffset == getNodeLength(endNode) && precedesLineBreak(endNode));
+
+	// "While (start node, start offset) is before (end node, end offset):"
+	while (getPosition(startNode, startOffset, endNode, endOffset) == "before") {
+		// "If start node has a child with index start offset, set start node
+		// to that child, then set start offset to zero."
+		if (startOffset < startNode.childNodes.length) {
+			startNode = startNode.childNodes[startOffset];
+			startOffset = 0;
+
+		// "Otherwise, if start node is not a Text node or if start offset is
+		// start node's length, set start offset to one plus start node's
+		// index, then set start node to its parent."
+		} else if (startNode.nodeType != Node.TEXT_NODE
+		|| startOffset == getNodeLength(startNode)) {
+			startOffset = 1 + getNodeIndex(startNode);
+			startNode = startNode.parentNode;
+
+		// "Otherwise:"
+		} else {
+			// "Remove the first element from replacement whitespace, and let
+			// element be that element."
+			var element = replacementWhitespace[0];
+			replacementWhitespace = replacementWhitespace.slice(1);
+
+			// "If element is not the same as the start offsetth element of
+			// start node's data:"
+			if (element != startNode.data[startOffset]) {
+				// "Call insertData(start offset, element) on start node."
+				startNode.insertData(startOffset, element);
+
+				// "Call deleteData(start offset + 1, 1) on start node."
+				startNode.deleteData(startOffset + 1, 1);
+			}
+
+			// "Add one to start offset."
+			startOffset++;
+		}
 	}
 }
 
@@ -4279,70 +4357,76 @@
 		// "Canonicalize whitespace at (start node, start offset)."
 		canonicalizeWhitespace(startNode, startOffset);
 
-	// "Otherwise:"
-	} else {
-		// "If start node is an editable Text node, call deleteData() on it,
-		// with start offset as the first argument and (length of start node −
-		// start offset) as the second argument.  Then canonicalize whitespace
-		// at (start node, start offset)."
-		if (isEditable(startNode)
-		&& startNode.nodeType == Node.TEXT_NODE) {
-			startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
-			canonicalizeWhitespace(startNode, startOffset);
-		}
-
-		// "Let node list be a list of nodes, initially empty."
-		//
-		// "For each node contained in range, append node to node list if the
-		// last member of node list (if any) is not an ancestor of node; node
-		// is editable; and node is not a thead, tbody, tfoot, tr, th, or td."
-		var nodeList = getContainedNodes(range,
-			function(node) {
-				return isEditable(node)
-					&& !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
-			}
-		);
-
-		// "For each node in node list:"
-		for (var i = 0; i < nodeList.length; i++) {
-			var node = nodeList[i];
-
-			// "Let parent be the parent of node."
-			var parent_ = node.parentNode;
-
-			// "Remove node from parent."
-			parent_.removeChild(node);
-
-			// "While parent is an editable inline node with length 0, let
-			// grandparent be the parent of parent, then remove parent from
-			// grandparent, then set parent to grandparent."
-			while (isEditable(parent_)
-			&& isInlineNode(parent_)
-			&& getNodeLength(parent_) == 0) {
-				var grandparent = parent_.parentNode;
-				grandparent.removeChild(parent_);
-				parent_ = grandparent;
-			}
-
-			// "If parent is editable or an editing host, is not an inline
-			// node, and has no children, call createElement("br") on the
-			// context object and append the result as the last child of
-			// parent."
-			if ((isEditable(parent_) || isEditingHost(parent_))
-			&& !isInlineNode(parent_)
-			&& !parent_.hasChildNodes()) {
-				parent_.appendChild(document.createElement("br"));
-			}
-		}
-
-		// "If end node is an editable Text node, call deleteData(0, end
-		// offset) on it, then canonicalize whitespace at (end node, 0)."
-		if (isEditable(endNode)
-		&& endNode.nodeType == Node.TEXT_NODE) {
-			endNode.deleteData(0, endOffset);
-			canonicalizeWhitespace(endNode, 0);
-		}
-	}
+		// "Set range's end to its start."
+		range.setEnd(range.startContainer, range.startOffset);
+
+		// "Abort these steps."
+		return;
+	}
+
+	// "If start node is an editable Text node, call deleteData() on it, with
+	// start offset as the first argument and (length of start node − start
+	// offset) as the second argument."
+	if (isEditable(startNode)
+	&& startNode.nodeType == Node.TEXT_NODE) {
+		startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset);
+	}
+
+	// "Let node list be a list of nodes, initially empty."
+	//
+	// "For each node contained in range, append node to node list if the last
+	// member of node list (if any) is not an ancestor of node; node is
+	// editable; and node is not a thead, tbody, tfoot, tr, th, or td."
+	var nodeList = getContainedNodes(range,
+		function(node) {
+			return isEditable(node)
+				&& !isHtmlElement(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]);
+		}
+	);
+
+	// "For each node in node list:"
+	for (var i = 0; i < nodeList.length; i++) {
+		var node = nodeList[i];
+
+		// "Let parent be the parent of node."
+		var parent_ = node.parentNode;
+
+		// "Remove node from parent."
+		parent_.removeChild(node);
+
+		// "While parent is an editable inline node with length 0, let
+		// grandparent be the parent of parent, then remove parent from
+		// grandparent, then set parent to grandparent."
+		while (isEditable(parent_)
+		&& isInlineNode(parent_)
+		&& getNodeLength(parent_) == 0) {
+			var grandparent = parent_.parentNode;
+			grandparent.removeChild(parent_);
+			parent_ = grandparent;
+		}
+
+		// "If parent is editable or an editing host, is not an inline node,
+		// and has no children, call createElement("br") on the context object
+		// and append the result as the last child of parent."
+		if ((isEditable(parent_) || isEditingHost(parent_))
+		&& !isInlineNode(parent_)
+		&& !parent_.hasChildNodes()) {
+			parent_.appendChild(document.createElement("br"));
+		}
+	}
+
+	// "If end node is an editable Text node, call deleteData(0, end offset) on
+	// it."
+	if (isEditable(endNode)
+	&& endNode.nodeType == Node.TEXT_NODE) {
+		endNode.deleteData(0, endOffset);
+	}
+
+	// "Canonicalize whitespace at range's start."
+	canonicalizeWhitespace(range.startContainer, range.startOffset);
+
+	// "Canonicalize whitespace at range's end."
+	canonicalizeWhitespace(range.endContainer, range.endOffset);
 
 	// "If start block or end block is null, or start block is not in the same
 	// editing host as end block, or start block and end block are the same,
--- a/source.html	Tue Jul 05 12:06:30 2011 -0600
+++ b/source.html	Tue Jul 05 14:00:32 2011 -0600
@@ -3397,72 +3397,131 @@
 <p>To <dfn>canonicalize whitespace</dfn> at (<var>node</var>,
 <var>offset</var>):
 
-<p class=XXX>This algorithm fails in all kinds of common cases, like any
-non-text node, or whitespace that spans multiple text nodes.  Needs lots of
-fixing.
-
 <ol>
-  <li>If <var>node</var> is not a [[text]] node, or is not
-  <span>editable</span>, or its [[parent]]'s [[compval]] for "white-space" is
-  "pre" or "pre-wrap", abort these steps.
+  <li>If <var>node</var> is neither <span>editable</span> nor an <span>editing
+  host</span>, abort these steps.
+
+  <li>Let <var>start node</var> equal <var>node</var> and let <var>start
+  offset</var> equal <var>offset</var>.
 
   <!-- First go to the beginning of the current whitespace run. -->
-  <li>Let <var>start offset</var> equal <var>offset</var>.
-
-  <li>While <var>start offset</var> is positive and the (<var>start
-  offset</var> &minus; 1)st [[strel]] of <var>node</var>'s [[cddata]] is a
-  space (0x0020) or non-breaking space (0x00A0), subtract one from <var>start
-  offset</var>.
+  <li>Repeat the following steps:
+
+  <ol>
+    <li>If <var>start node</var> has a [[child]] <span>in the same editing
+    host</span> with [[index]] <var>start offset</var> minus one, set
+    <var>start node</var> to that [[child]], then set <var>start offset</var>
+    to <var>start node</var>'s [[length]].
+
+    <li>Otherwise, if <var>start offset</var> is zero and <var>start node</var>
+    does not <span title="follows a line break">follow a line break</span> and
+    <var>start node</var>'s [[parent]] is <span>in the same editing
+    host</span>, set <var>start offset</var> to <var>start node</var>'s
+    [[index]], then set <var>start node</var> to its [[parent]].
+
+    <p class=XXX>Following a line break is unlikely to be the right criterion.
+
+    <li>Otherwise, if <var>start node</var> is a [[text]] node and its
+    [[parent]]'s [[compval]] for "white-space" is neither "pre" nor "pre-wrap"
+    and <var>start offset</var> is not zero and the (<var>start offset</var>
+    &minus; 1)st [[strel]] of <var>start node</var>'s [[cddata]] is a space
+    (0x0020) or non-breaking space (0x00A0), subtract one from <var>start
+    offset</var>.
+
+    <li>Otherwise, break from this loop.
+  </ol>
 
   <!-- Now collapse any consecutive spaces. -->
-  <li>Let <var>end offset</var> equal <var>start offset</var>.
-
-  <li>While <var>end offset</var> is less than <var>node</var>'s [[strlen]],
-  and the <var>end offset</var>th [[strel]] of <var>node</var>'s [[cddata]] is
-  0x0020 or 0x00A0:
+  <li>Let <var>end node</var> equal <var>start node</var> and <var>end
+  offset</var> equal <var>start offset</var>.
+
+  <li>Let <var>length</var> equal zero.
+
+  <li>Let <var>follows space</var> be false.
+
+  <li>Repeat the following steps:
 
   <ol>
-    <li>Let <var>length</var> equal zero.
-
-    <li>While <var>end offset</var> plus <var>length</var> is less than
-    <var>node</var>'s [[cdlength]], and the (<var>end offset</var> +
-    <var>length</var>)th [[strel]] of <var>node</var>'s [[cddata]] is 0x0020,
-    add one to <var>length</var>.
-
-    <li>If <var>length</var> is greater than one, call [[deletedata|<var>end
-    offset</var> + 1, <var>length</var> &minus; 1]] on <var>node</var>.
-
-    <li>Add one to <var>end offset</var>.
+    <li>If <var>end node</var> has a [[child]] <span>in the same editing
+    host</span> with [[index]] <var>end offset</var>, set <var>end node</var>
+    to that [[child]], then set <var>end offset</var> to zero.
+
+    <li>Otherwise, if <var>end offset</var> is <var>end node</var>'s [[length]]
+    and <var>end node</var> does not <span title="precedes a line
+    break">precede a line break</span> and <var>end node</var>'s [[parent]] is
+    <span>in the same editing host</span>, set <var>end offset</var> to one
+    plus <var>end node</var>'s [[index]], then set <var>end node</var> to its
+    [[parent]].
+
+    <p class=XXX>Preceding a line break is unlikely to be the right criterion.
+
+    <li>Otherwise, if <var>end node</var> is a [[text]] node and its
+    [[parent]]'s [[compval]] for "white-space" is neither "pre" nor "pre-wrap"
+    and <var>end offset</var> is not <var>end node</var>'s [[length]] and the
+    <var>end offset</var>th [[strel]] of <var>end node</var>'s [[cddata]] is a
+    space (0x0020) or non-breaking space (0x00A0):
+
+    <ol>
+      <li>If <var>follows space</var> is true and the <var>end offset</var>th
+      [[strel]] of <var>end node</var>'s [[cddata]] is a space (0x0020), call
+      [[deletedata|<var>end offset</var>, 1]] on <var>end node</var>, then
+      continue this loop from the beginning.
+
+      <li>Set <var>follows space</var> to true if the <var>end offset</var>th
+      [[strel]] of <var>end node</var>'s [[cddata]] is a space (0x0020), false
+      otherwise.
+
+      <li>Add one to <var>end offset</var>.
+
+      <li>Add one to <var>length</var>.
+    </ol>
+
+    <li>Otherwise, break from this loop.
   </ol>
 
   <!-- Now replace with the canonical sequence. -->
   <li>Let <var>replacement whitespace</var> be the <span>canonical space
-  sequence</span> of length <var>end offset</var> minus <var>start
-  offset</var>.  <var>non-breaking start</var> is true if <var>start
-  offset</var> is zero and false otherwise, and <var>non-breaking end</var> is
-  true if <var>end offset</var> is <var>node</var>'s [[cdlength]] and false
-  otherwise.
-
-  <li>While <var>start offset</var> is less than <var>end offset</var>:
+  sequence</span> of length <var>length</var>.  <var>non-breaking start</var>
+  is true if <var>start offset</var> is zero and <var>start node</var>
+  <span>follows a line break</span>, and false otherwise.  <var>non-breaking
+  end</var> is true if <var>end offset</var> is <var>end node</var>'s
+  [[cdlength]] and <var>end node</var> <span>precedes a line break</span>, and
+  false otherwise.
+
+  <li>While (<var>start node</var>, <var>start offset</var>) is [[bpbefore]]
+  (<var>end node</var>, <var>end offset</var>):
 
   <ol>
-    <li>Remove the first [[strel]] from <var>replacement whitespace</var>, and
-    let <var>element</var> be that [[strel]].
-
-    <li>If <var>element</var> is not the same as the <var>start offset</var>th
-    [[strel]] of <var>node</var>'s [[cddata]]:
+    <li>If <var>start node</var> has a [[child]] with [[index]] <var>start
+    offset</var>, set <var>start node</var> to that [[child]], then set
+    <var>start offset</var> to zero.
+
+    <li>Otherwise, if <var>start node</var> is not a [[text]] node or if
+    <var>start offset</var> is <var>start node</var>'s [[length]], set
+    <var>start offset</var> to one plus <var>start node</var>'s [[index]], then
+    set <var>start node</var> to its [[parent]].
+
+    <li>Otherwise:
 
     <ol>
-      <!-- We need to insert then delete, so that we don't change range
-      boundary points. -->
-      <li>Call [[insertdata|<var>start offset</var>, <var>element</var>]] on
-      <var>node</var>.
-
-      <li>Call [[deletedata|<var>start offset</var> + 1, 1]] on
-      <var>node</var>.
+      <li>Remove the first [[strel]] from <var>replacement whitespace</var>,
+      and let <var>element</var> be that [[strel]].
+
+      <li>If <var>element</var> is not the same as the <var>start
+      offset</var>th [[strel]] of <var>start node</var>'s [[cddata]]:
+
+      <ol>
+        <!-- We need to insert then delete, so that we don't change range
+        boundary points. -->
+        <li>Call [[insertdata|<var>start offset</var>, <var>element</var>]] on
+        <var>start node</var>.
+
+        <li>Call [[deletedata|<var>start offset</var> + 1, 1]] on
+        <var>start node</var>.
+      </ol>
+
+      <li>Add one to <var>start offset</var>.
     </ol>
-
-    <li>Add one to <var>start offset</var>.
   </ol>
 </ol>
 
@@ -3896,58 +3955,61 @@
 
     <li><span>Canonicalize whitespace</span> at (<var>start node</var>,
     <var>start offset</var>).
+
+    <li>Set <var>range</var>'s [[rangeend]] to its [[rangestart]].
+
+    <li>Abort these steps.
   </ol>
 
-  <li>Otherwise:
+  <li>If <var>start node</var> is an <span>editable</span> [[text]] node, call
+  [[deletedata|]] on it, with <var>start offset</var> as the first argument and
+  ([[nodelength]] of <var>start node</var> &minus; <var>start offset</var>) as
+  the second argument.
+
+  <li>Let <var>node list</var> be a list of [[nodes]], initially empty.
+
+  <li>For each <var>node</var> [[contained]] in <var>range</var>, append
+  <var>node</var> to <var>node list</var> if the last member of <var>node
+  list</var> (if any) is not an [[ancestor]] of <var>node</var>;
+  <var>node</var> is <span>editable</span>; and <var>node</var> is not a
+  [[thead]], [[tbody]], [[tfoot]], [[tr]], [[th]], or [[td]].
+  <!--
+  IE9 doesn't seem to let you do any intercell deletions: the delete key does
+  nothing if you select across multiple cells.  Firefox 5.0a2 and Opera 11.11
+  behave as the spec says, not removing any table things.  Chrome 13 dev will
+  remove entire rows if selected.  Note that IE, Firefox, Word 2007, and
+  OpenOffice.org 3.2.1 Ubuntu all switch to a magic cell-selection mode when
+  you try to select between cells, at least in some cases, instead of selecting
+  letter-by-letter.
+  -->
+
+  <li>For each <var>node</var> in <var>node list</var>:
 
   <ol>
-    <li>If <var>start node</var> is an <span>editable</span> [[text]] node,
-    call [[deletedata|]] on it, with <var>start offset</var> as the first
-    argument and ([[nodelength]] of <var>start node</var> &minus; <var>start
-    offset</var>) as the second argument.  Then <span>canonicalize
-    whitespace</span> at (<var>start node</var>, <var>start offset</var>).
-
-    <li>Let <var>node list</var> be a list of [[nodes]], initially empty.
-
-    <li>For each <var>node</var> [[contained]] in <var>range</var>, append
-    <var>node</var> to <var>node list</var> if the last member of <var>node
-    list</var> (if any) is not an [[ancestor]] of <var>node</var>;
-    <var>node</var> is <span>editable</span>; and <var>node</var> is not a
-    [[thead]], [[tbody]], [[tfoot]], [[tr]], [[th]], or [[td]].
-    <!--
-    IE9 doesn't seem to let you do any intercell deletions: the delete key does
-    nothing if you select across multiple cells.  Firefox 5.0a2 and Opera 11.11
-    behave as the spec says, not removing any table things.  Chrome 13 dev will
-    remove entire rows if selected.  Note that IE, Firefox, Word 2007, and
-    OpenOffice.org 3.2.1 Ubuntu all switch to a magic cell-selection mode when
-    you try to select between cells, at least in some cases, instead of
-    selecting letter-by-letter.
-    -->
-
-    <li>For each <var>node</var> in <var>node list</var>:
-
-    <ol>
-      <li>Let <var>parent</var> be the [[parent]] of <var>node</var>.
-
-      <li>Remove <var>node</var> from <var>parent</var>.
-
-      <li>While <var>parent</var> is an <span>editable</span> <span>inline
-      node</span> with [[nodelength]] 0, let <var>grandparent</var> be the
-      [[parent]] of <var>parent</var>, then remove <var>parent</var> from
-      <var>grandparent</var>, then set <var>parent</var> to
-      <var>grandparent</var>.
-
-      <li>If <var>parent</var> is <span>editable</span> or an <span>editing
-      host</span>, is not an <span>inline node</span>, and has no [[children]],
-      call [[createelement|"br"]] on the [[contextobject]] and append the
-      result as the last [[child]] of <var>parent</var>.
-    </ol>
-
-    <li>If <var>end node</var> is an <span>editable</span> [[text]] node, call
-    [[deletedata|0, <var>end offset</var>]] on it, then <span>canonicalize
-    whitespace</span> at (<var>end node</var>, 0).
+    <li>Let <var>parent</var> be the [[parent]] of <var>node</var>.
+
+    <li>Remove <var>node</var> from <var>parent</var>.
+
+    <li>While <var>parent</var> is an <span>editable</span> <span>inline
+    node</span> with [[nodelength]] 0, let <var>grandparent</var> be the
+    [[parent]] of <var>parent</var>, then remove <var>parent</var> from
+    <var>grandparent</var>, then set <var>parent</var> to
+    <var>grandparent</var>.
+
+    <li>If <var>parent</var> is <span>editable</span> or an <span>editing
+    host</span>, is not an <span>inline node</span>, and has no [[children]],
+    call [[createelement|"br"]] on the [[contextobject]] and append the result
+    as the last [[child]] of <var>parent</var>.
   </ol>
 
+  <li>If <var>end node</var> is an <span>editable</span> [[text]] node, call
+  [[deletedata|0, <var>end offset</var>]] on it.
+
+  <li><span>Canonicalize whitespace</span> at <var>range</var>'s
+  [[rangestart]].
+
+  <li><span>Canonicalize whitespace</span> at <var>range</var>'s [[rangeend]].
+
   <!--
   Now we need to merge blocks.  The simplest case is something like
 
@@ -6479,9 +6541,8 @@
 
   <li>Call [[insertnode|<var>text</var>]] on the <span>active range</span>.
 
-  <li>Call [[selcollapse|<var>text</var>, <var>length</var>]] on the
-  [[contextobject]]'s [[selection]], where <var>length</var> is the [[length]]
-  of <var>text</var>.
+  <li>Call [[selcollapse|<var>text</var>, 1]] on the [[contextobject]]'s
+  [[selection]].
 </ol>
 <!-- @} -->
 
--- a/tests.js	Tue Jul 05 12:06:30 2011 -0600
+++ b/tests.js	Tue Jul 05 14:00:32 2011 -0600
@@ -2019,6 +2019,10 @@
 		[' ', '{}<br>'],
 		[' ', '<p>{}<br>'],
 
+		[' ', '<p>foo[]<p>bar'],
+		[' ', '<p>foo&nbsp;[]<p>bar'],
+		[' ', '<p>foo[]<p>&nbsp;bar'],
+
 		['   ', 'foo[]'],
 
 		'foo[]bar',