Begin making delete algorithm less obscure
authorAryeh Gregor <AryehGregor+gitcommit@gmail.com>
Mon, 07 Nov 2011 16:01:08 -0700
changeset 662 420aa6a5998b
parent 661 ecbd0e911073
child 663 9dfe25d9106a
Begin making delete algorithm less obscure

This is refactoring that should have no significant practical effect
(see data.js changes). It's a start to fixing
http://www.w3.org/Bugs/Public/show_bug.cgi?id=13973. I took out some
relatively opaque algorithms and redefined them in terms of more
comprehensible subalgorithms. This makes things slightly longer, but I
should also be able to reuse the new definitions elsewhere.
conformancetest/data.js
editing.html
implementation.js
preprocess
source.html
--- a/conformancetest/data.js	Thu Oct 27 15:16:47 2011 -0600
+++ b/conformancetest/data.js	Mon Nov 07 16:01:08 2011 -0700
@@ -3681,11 +3681,11 @@
 	{"stylewithcss":[false,false,"",false,true,""],"delete":[false,false,"",false,false,""]}],
 ["foo[<br>]bar",
 	[["stylewithcss","false"],["delete",""]],
-	"foo[]bar",
+	"foo{}bar",
 	{"stylewithcss":[false,true,"",false,false,""],"delete":[false,false,"",false,false,""]}],
 ["foo[<br>]bar",
 	[["stylewithcss","true"],["delete",""]],
-	"foo[]bar",
+	"foo{}bar",
 	{"stylewithcss":[false,false,"",false,true,""],"delete":[false,false,"",false,false,""]}],
 ["<p>foo[</p><p>]bar</p>",
 	[["stylewithcss","false"],["delete",""]],
@@ -9913,11 +9913,11 @@
 	{"stylewithcss":[false,false,"",false,true,""],"forwarddelete":[false,false,"",false,false,""]}],
 ["foo[<br>]bar",
 	[["stylewithcss","false"],["forwarddelete",""]],
-	"foo[]bar",
+	"foo{}bar",
 	{"stylewithcss":[false,true,"",false,false,""],"forwarddelete":[false,false,"",false,false,""]}],
 ["foo[<br>]bar",
 	[["stylewithcss","true"],["forwarddelete",""]],
-	"foo[]bar",
+	"foo{}bar",
 	{"stylewithcss":[false,false,"",false,true,""],"forwarddelete":[false,false,"",false,false,""]}],
 ["<p>foo[</p><p>]bar</p>",
 	[["stylewithcss","false"],["forwarddelete",""]],
--- a/editing.html	Thu Oct 27 15:16:47 2011 -0600
+++ b/editing.html	Mon Nov 07 16:01:08 2011 -0700
@@ -67,7 +67,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-27-october-2011>Work in Progress &mdash; Last Update 27 October 2011</h2>
+<h2 class="no-num no-toc" id=work-in-progress-&mdash;-last-update-7-november-2011>Work in Progress &mdash; Last Update 7 November 2011</h2>
 <dl>
  <dt>Editor
  <dd>Aryeh Gregor &lt;<a href=mailto:ayg@aryeh.name>ayg@aryeh.name</a>&gt;
@@ -336,7 +336,7 @@
   bad, <code title="">&lt;b&gt;{}&lt;/b&gt;</code> is correct.
 
   <li>Curly braces mark a selection inside an element, like
-  <code title="">&lt;b&gt;{foobarbaz&lt;/b&gt;} </code> for a selection whose start node is the element
+  <code title="">&lt;b&gt;{foobarbaz&lt;/b&gt;}</code> for a selection whose start node is the element
   <code title="">&lt;b&gt;</code>, whose start offset is 0, whose end node is the root of the
   editable region, and whose end offset is 1.  Do not use curly braces in the
   middle of a text node: <code title="">foo{bar}baz</code> is bad, <code title="">foo[bar]baz</code> is correct.
@@ -5160,6 +5160,104 @@
 <!--@}-->
 <h3 id=deleting-the-selection>Deleting the selection</h3>
 
+<p class=note>Sometimes one location corresponds to multiple distinct boundary
+points.  For instance, in the DOM <code title="">&lt;p&gt;Hello&lt;/p&gt;</code>, a boundary point might
+lie at the beginning of the text node or the beginning of the element node, but
+these don't logically differ much and will appear the same to the user, so we
+often want to treat them the same.  The algorithms here (currently used only
+for the delete algorithm) allow navigating through such equivalent boundary
+points, for when we want to make the selection as inclusive or exclusive as
+possible.  For deletion, we want to delete as few nodes as possible, so we move
+the start node forward and the end node backward.  In other cases we might do
+the reverse, expanding the selection.
+
+<p>Given a <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp title=concept-range-bp>boundary point</a> (<var title="">node</var>, <var title="">offset</var>), the
+<dfn id=next-equivalent-point>next equivalent point</dfn> is either a <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp title=concept-range-bp>boundary point</a> or null, as
+returned by the following algorithm:
+
+<ol>
+  <li>If <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a> is zero, return null.
+
+  <li class=note>We don't want to move into or out of zero-length nodes,
+  because that would move us straight through them.  For instance, if
+  <code title="">{}&lt;span&gt;&lt;/span&gt;</code> were equivalent to <code title="">&lt;span&gt;{}&lt;/span&gt;</code>, it
+  would also be equivalent to <code title="">&lt;span&gt;&lt;/span&gt;{}</code>.  This produces very
+  unexpected results for nodes like <code title="">&lt;br&gt;</code>.
+
+  <li>If <var title="">offset</var> is <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a>, and
+  <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> is not null, return (<var title="">node</var>'s
+  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>, 1 + <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a>).
+
+  <li class=note>For instance, <code title="">&lt;span&gt;foo[]&lt;/span&gt;</code> is equivalent to
+  <code title="">&lt;span&gt;foo{}&lt;/span&gt;</code>, which is equivalent to <code title="">&lt;span&gt;foo&lt;/span&gt;{}</code>.
+
+  <li>If <var title="">node</var> has a <a class=external data-anolis-spec=dom 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=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a> <var title="">offset</var>,
+  and that <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a> is not zero, return (that <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>, 0).
+
+  <li class=note>For instance, <code title="">{}&lt;span&gt;foo&lt;/span&gt;</code> is equivalent to
+  <code title="">&lt;span&gt;{}foo&lt;/span&gt;</code>, which is equivalent to
+  <code title="">&lt;span&gt;[]foo&lt;/span&gt;</code>.  As noted before, though, we don't descend into
+  empty nodes.
+
+  <li>Return null.
+</ol>
+
+<p>Given a <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp title=concept-range-bp>boundary point</a> (<var title="">node</var>, <var title="">offset</var>), the
+<dfn id=previous-equivalent-point>previous equivalent point</dfn> is either a <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp title=concept-range-bp>boundary point</a> or null, as
+returned by the following algorithm:
+
+<ol>
+  <li>If <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a> is zero, return null.
+
+  <li>If <var title="">offset</var> is 0, and <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> is not null,
+  return (<var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>, <var title="">node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a>).
+
+  <li>If <var title="">node</var> has a <a class=external data-anolis-spec=dom 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=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a> <var title="">offset</var>
+  &minus; 1, and that <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a> is not zero, return (that
+  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>, that <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a>).
+
+  <li>Return null.
+</ol>
+
+<!-- Turned out not to be necessary here, but I've left it in case it turns out
+to be handy elsewhere. -->
+<!--
+<p>Two <span data-anolis-spec=dom title=concept-range-bp>boundary points</span> <var title>A</var> and <var title>B</var> are <dfn>equivalent
+boundary points</dfn> if the following algorithm returns true:
+
+<p class=note>This is indeed an <a
+href=http://en.wikipedia.org/wiki/Equivalence_relation>equivalence
+relation</a>.  Reflexivity, symmetry, and transitivity are all fairly obvious
+from the definition.  It also isn't hard to check that using <span>next
+equivalent point</span> instead of <span>previous equivalent point</span> would
+yield the same definition, because the two algorithms are inverses if neither
+output is null.
+
+<ol>
+  <li>While <var title>A</var>'s <span>previous equivalent point</span> is not null,
+  set <var title>A</var> to its <span>previous equivalent point</span>.
+
+  <li>While <var title>B</var>'s <span>previous equivalent point</span> is not null,
+  set <var title>B</var> to its <span>previous equivalent point</span>.
+
+  <li>Return true if <var title>A</var> is the same as <var title>B</var>, otherwise false.
+</ol>
+-->
+
+<p>The <dfn id=block-node-of>block node of</dfn> a <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>node</a> <var title="">node</var> is either a
+<a href=#block-node>block node</a> or null, as returned by the following algorithm:
+
+<ol>
+  <li>While <var title="">node</var> is an <a href=#inline-node>inline node</a>, set <var title="">node</var>
+  to its <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a>.
+
+  <li>Return <var title="">node</var>.
+</ol>
+
+<p>Two <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>nodes</a> are <dfn id=in-the-same-block>in the same block</dfn> if the <a href=#block-node-of>block node
+of</a> the first is non-null and the same as the <a href=#block-node-of>block node of</a>
+the second.
+
 <p class=comments>TODO: Consider what should happen for block merging in corner
 cases like display: inline-table.
 
@@ -5175,7 +5273,7 @@
 selection will be collapsed.  By way of contrast, <a href=#effectively-contained>effectively
 contained</a> tries to expand the range to include as much as possible, so
 <code title="">&lt;p&gt;[foo]&lt;/p&gt;</code> contains the <code title="">&lt;p&gt;</code>.  What we do here is contract
-the range to include as little as possible, so <code title="">{&lt;p&gt;foo&lt;/p&gt;} </code> contains
+the range to include as little as possible, so <code title="">{&lt;p&gt;foo&lt;/p&gt;}</code> contains
 only <code title="">foo</code> and doesn't delete the paragraph.
 
 <p>After that, if the selection originally started and ended in different
@@ -5219,104 +5317,21 @@
   and <var title="">end offset</var> be the <a href=#active-range>active range</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-start title=concept-range-start>start</a>
   and <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-end title=concept-range-end>end</a> <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>nodes</a> and <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-offset title=concept-range-bp-offset>offsets</a>.
 
-  <li class=note>
-  <p>First we adjust the selection so that we don't delete more than we should.
-
-  <li>
-  <div class=comments>
-  <p>We don't want to keep going when we hit an element with no children,
-  because then we'd do something like
-
-<pre>foo{&lt;br /&gt;bar]
--&gt; foo&lt;br&gt;{&lt;/br&gt;bar]
--&gt; foo&lt;br /&gt;{bar]
--&gt; foo&lt;br /&gt;[bar]</pre>
-
-  <p>and we deselected the <code title="">&lt;br&gt;</code>.
-  </div>
-
-  <p>While <var title="">start node</var> has at least one <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>:
-
-  <ol>
-    <li>
-    <div class=comments>
-    <p>For instance:
-
-<pre>&lt;b&gt;foo[&lt;/b&gt;&lt;i&gt;bar]&lt;/i&gt;
--&gt; &lt;b&gt;foo{&lt;/b&gt;&lt;i&gt;bar]&lt;/i&gt;
--&gt; &lt;b&gt;foo&lt;/b&gt;{&lt;i&gt;bar]&lt;/i&gt;</pre>
-
-    <p>Then the next step will make it <code title="">&lt;b&gt;foo&lt;/b&gt;&lt;i&gt;[bar]&lt;/i&gt;</code>.
-
-    <p>We don't want to do this for block nodes, because that would lead to
-    something like
-
-      <pre>&lt;p&gt;foo[&lt;/p&gt;&lt;p&gt;]bar&lt;p&gt;</pre>
-
-    <p>ultimately collapsing, which is wrong.  Once we do the deletion, it
-    needs to wind up <code title="">&lt;p&gt;foo[]bar&lt;/p&gt;</code>, whereas an actually collapsed
-    selection should do nothing.
-    </div>
-
-    <p>If <var title="">start offset</var> is <var title="">start node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a>, and
-    <var title="">start node</var>'s <a class=external data-anolis-spec=dom 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>, and <var title="">start node</var> is an <a href=#inline-node>inline node</a>, set
-    <var title="">start offset</var> to one plus the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a> of <var title="">start node</var>,
-    then set <var title="">start node</var> to its <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> and continue this loop
-    from the beginning.
-
-    <li>
-    <p class=comments>This happens if the first step brought us all the way up
-    to the root.  The step immediately after this loop will bring us back down
-    again.
-
-    <p>If <var title="">start offset</var> is <var title="">start node</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a>,
-    break from this loop.
-
-    <li>Let <var title="">reference node</var> be the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a> of <var title="">start node</var>
-    with <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a> equal to <var title="">start offset</var>.
-
-    <li>
-    <div class=comments>
-    <p>Don't descend into an element with no children, since then it won't get
-    deleted even if it's selected.  Don't descend into a block node, because
-    then we might wind up not merging blocks when we should, e.g.
-
-<pre>foo{&lt;p&gt;}bar&lt;/p&gt;
--&gt; foo&lt;p&gt;{}bar&lt;/p&gt;</pre>
-
-    <p>and nothing gets changed.
-    </div>
-
-    <p>If <var title="">reference node</var> is a <a href=#block-node>block node</a> or an
-    <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> with no <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a>, or is neither an <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> nor a
-    <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#text>Text</a></code> node, break from this loop.
-
-    <li>Set <var title="">start node</var> to <var title="">reference node</var> and <var title="">start
-    offset</var> to 0.
-  </ol>
-
-  <li>While <var title="">end node</var> has at least one <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a>:
-
-  <ol>
-    <li>If <var title="">end offset</var> is 0, and <var title="">end node</var>'s
-    <a class=external data-anolis-spec=dom 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>, and <var title="">end
-    node</var> is an <a href=#inline-node>inline node</a>, set <var title="">end offset</var> to the
-    <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a> of <var title="">end node</var>, then set <var title="">end node</var> to its
-    <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-parent title=concept-tree-parent>parent</a> and continue this loop from the beginning.
-
-    <li>If <var title="">end offset</var> is 0, break from this loop.
-
-    <li>Let <var title="">reference node</var> be the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>child</a> of <var title="">end node</var>
-    with <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-index title=concept-tree-index>index</a> equal to <var title="">end offset</var> minus one.
-
-    <li>If <var title="">reference node</var> is a <a href=#block-node>block node</a> or an
-    <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> with no <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-child title=concept-tree-child>children</a>, or is neither an <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#element>Element</a></code> nor a
-    <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#text>Text</a></code> node, break from this loop.
-
-    <li>Set <var title="">end node</var> to <var title="">reference node</var> and <var title="">end
-    offset</var> to the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length title=concept-node-length>length</a> of <var title="">reference node</var>.
-  </ol>
+  <li>While the <a href=#next-equivalent-point>next equivalent point</a> for (<var title="">start node</var>,
+  <var title="">start offset</var>) is not null, and that <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp title=concept-range-bp>boundary point</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>node</a>
+  is <a href=#in-the-same-editing-host>in the same editing host</a> and <a href=#in-the-same-block>in the same block</a>
+  as <var title="">start node</var>, set (<var title="">start node</var>, <var title="">start
+  offset</var>) to its <a href=#next-equivalent-point>next equivalent point</a>.
+
+  <li class=note>We don't want to leave the current block because otherwise,
+  for instance, a selection of <code title="">&lt;p&gt;foo[&lt;/p&gt;&lt;p&gt;]bar&lt;/p&gt;</code> would be treated
+  as empty and nothing would be deleted, when we want it to merge the blocks.
+
+  <li>While the <a href=#previous-equivalent-point>previous equivalent point</a> for (<var title="">end
+  node</var>, <var title="">end offset</var>) is not null, and that <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp title=concept-range-bp>boundary point</a>'s
+  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node title=concept-node>node</a> is <a href=#in-the-same-editing-host>in the same editing host</a> and <a href=#in-the-same-block>in the same
+  block</a> as <var title="">end node</var>, set (<var title="">end node</var>, <var title="">end
+  offset</var>) to its <a href=#previous-equivalent-point>previous equivalent point</a>.
 
   <li>If (<var title="">end node</var>, <var title="">end offset</var>) is not <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-after title=concept-range-bp-after>after</a>
   (<var title="">start node</var>, <var title="">start offset</var>):
--- a/implementation.js	Thu Oct 27 15:16:47 2011 -0600
+++ b/implementation.js	Mon Nov 07 16:01:08 2011 -0700
@@ -4372,6 +4372,71 @@
 ///// Deleting the selection /////
 //@{
 
+function getNextEquivalentPoint(node, offset) {
+	// "If node's length is zero, return null."
+	if (getNodeLength(node) == 0) {
+		return null;
+	}
+
+	// "If offset is node's length, and node's parent is not null, return
+	// (node's parent, 1 + node's index)."
+	if (offset == getNodeLength(node) && node.parentNode) {
+		return [node.parentNode, 1 + getNodeIndex(node)];
+	}
+
+	// "If node has a child with index offset, and that child's length is not
+	// zero, return (that child, 0)."
+	if (0 <= node.childNodes.length
+	&& offset < node.childNodes.length
+	&& getNodeLength(node.childNodes[offset]) != 0) {
+		return [node.childNodes[offset], 0];
+	}
+
+	// "Return null."
+	return null;
+}
+
+function getPreviousEquivalentPoint(node, offset) {
+	// "If node's length is zero, return null."
+	if (getNodeLength(node) == 0) {
+		return null;
+	}
+
+	// "If offset is 0, and node's parent is not null, return (node's parent,
+	// node's index)."
+	if (offset == 0 && node.parentNode) {
+		return [node.parentNode, getNodeIndex(node)];
+	}
+
+	// "If node has a child with index offset − 1, and that child's length is
+	// not zero, return (that child, that child's length)."
+	if (0 <= node.childNodes.length
+	&& offset - 1 < node.childNodes.length
+	&& getNodeLength(node.childNodes[offset - 1]) != 0) {
+		return [node.childNodes[offset - 1], getNodeLength(node.childNodes[offset - 1])];
+	}
+
+	// "Return null."
+	return null;
+}
+
+function getBlockNodeOf(node) {
+	// "While node is an inline node, set node to its parent."
+	while (isInlineNode(node)) {
+		node = node.parentNode;
+	}
+
+	// "Return node."
+	return node;
+}
+
+// "Two nodes are in the same block if the block node of the first is non-null
+// and the same as the block node of the second."
+function inSameBlock(node1, node2) {
+	return getBlockNodeOf(node1)
+		&& getBlockNodeOf(node1) === getBlockNodeOf(node2);
+}
+
 // The flags argument is a dictionary that can have blockMerging,
 // stripWrappers, and/or direction as keys.
 function deleteSelection(flags) {
@@ -4401,81 +4466,28 @@
 	var endNode = getActiveRange().endContainer;
 	var endOffset = getActiveRange().endOffset;
 
-	// "While start node has at least one child:"
-	while (startNode.hasChildNodes()) {
-		// "If start offset is start node's length, and start node's parent is
-		// in the same editing host, and start node is an inline node, set
-		// start offset to one plus the index of start node, then set start
-		// node to its parent and continue this loop from the beginning."
-		if (startOffset == getNodeLength(startNode)
-		&& inSameEditingHost(startNode, startNode.parentNode)
-		&& isInlineNode(startNode)) {
-			startOffset = 1 + getNodeIndex(startNode);
-			startNode = startNode.parentNode;
-			continue;
-		}
-
-		// "If start offset is start node's length, break from this loop."
-		if (startOffset == getNodeLength(startNode)) {
-			break;
-		}
-
-		// "Let reference node be the child of start node with index equal to
-		// start offset."
-		var referenceNode = startNode.childNodes[startOffset];
-
-		// "If reference node is a block node or an Element with no children,
-		// or is neither an Element nor a Text node, break from this loop."
-		if (isBlockNode(referenceNode)
-		|| (referenceNode.nodeType == Node.ELEMENT_NODE
-		&& !referenceNode.hasChildNodes())
-		|| (referenceNode.nodeType != Node.ELEMENT_NODE
-		&& referenceNode.nodeType != Node.TEXT_NODE)) {
-			break;
-		}
-
-		// "Set start node to reference node and start offset to 0."
-		startNode = referenceNode;
-		startOffset = 0;
-	}
-
-	// "While end node has at least one child:"
-	while (endNode.hasChildNodes()) {
-		// "If end offset is 0, and end node's parent is in the same editing
-		// host, and end node is an inline node, set end offset to the index of
-		// end node, then set end node to its parent and continue this loop
-		// from the beginning."
-		if (endOffset == 0
-		&& inSameEditingHost(endNode, endNode.parentNode)
-		&& isInlineNode(endNode)) {
-			endOffset = getNodeIndex(endNode);
-			endNode = endNode.parentNode;
-			continue;
-		}
-
-		// "If end offset is 0, break from this loop."
-		if (endOffset == 0) {
-			break;
-		}
-
-		// "Let reference node be the child of end node with index equal to end
-		// offset minus one."
-		var referenceNode = endNode.childNodes[endOffset - 1];
-
-		// "If reference node is a block node or an Element with no children,
-		// or is neither an Element nor a Text node, break from this loop."
-		if (isBlockNode(referenceNode)
-		|| (referenceNode.nodeType == Node.ELEMENT_NODE
-		&& !referenceNode.hasChildNodes())
-		|| (referenceNode.nodeType != Node.ELEMENT_NODE
-		&& referenceNode.nodeType != Node.TEXT_NODE)) {
-			break;
-		}
-
-		// "Set end node to reference node and end offset to the length of
-		// reference node."
-		endNode = referenceNode;
-		endOffset = getNodeLength(referenceNode);
+	// "While the next equivalent point for (start node, start offset) is not
+	// null, and that boundary point's node is in the same editing host and in
+	// the same block as start node, set (start node, start offset) to its next
+	// equivalent point."
+	while (getNextEquivalentPoint(startNode, startOffset)
+	&& inSameEditingHost(getNextEquivalentPoint(startNode, startOffset)[0], startNode)
+	&& inSameBlock(getNextEquivalentPoint(startNode, startOffset)[0], startNode)) {
+		var next = getNextEquivalentPoint(startNode, startOffset);
+		startNode = next[0];
+		startOffset = next[1];
+	}
+
+	// "While the previous equivalent point for (end node, end offset) is not
+	// null, and that boundary point's node is in the same editing host and in
+	// the same block as end node, set (end node, end offset) to its previous
+	// equivalent point."
+	while (getPreviousEquivalentPoint(endNode, endOffset)
+	&& inSameEditingHost(getPreviousEquivalentPoint(endNode, endOffset)[0], endNode)
+	&& inSameBlock(getPreviousEquivalentPoint(endNode, endOffset)[0], endNode)) {
+		var prev = getPreviousEquivalentPoint(endNode, endOffset);
+		endNode = prev[0];
+		endOffset = prev[1];
 	}
 
 	// "If (end node, end offset) is not after (start node, start offset):"
--- a/preprocess	Thu Oct 27 15:16:47 2011 -0600
+++ b/preprocess	Mon Nov 07 16:01:08 2011 -0700
@@ -168,19 +168,21 @@
 
 # {{html|<foo>}} -> &lt;foo>, no wrapper.  Also, to avoid things like
 # {{code|<b>}} messing up syntax highlighting, {{html<|b>}} is also supported
-# with the same effect as {{html|<b>}}.
+# with the same effect as {{html|<b>}}.  Trailing whitespace before the }} will
+# be ignored, so that {{html|} }} will produce "}", since {{html|}}} won't work
+# (the first two "}"'s will terminate the expression).
 def htmlsub(matchobj):
     return matchobj.group(1).replace("<", "&lt;") \
          + matchobj.group(2).replace("&", "&amp;").replace("<", "&lt;")
 
-# {{code|...}} is just a shortcut for <code title>{{pre|...}}</code>.
+# {{code|...}} is just a shortcut for <code title>{{html|...}}</code>.
 def codesub(matchobj):
     return "<code title>" \
         + htmlsub(matchobj) \
         + "</code>"
 
-s = re.compile(r"\{\{html(<?)\|(.*?)\}\}", re.DOTALL).sub(htmlsub, s)
-s = re.compile(r"\{\{code(<?)\|(.*?)\}\}", re.DOTALL).sub(codesub, s)
+s = re.compile(r"\{\{html(<?)\|(.*?)\s*\}\}", re.DOTALL).sub(htmlsub, s)
+s = re.compile(r"\{\{code(<?)\|(.*?)\s*\}\}", re.DOTALL).sub(codesub, s)
 
 s = s.replace("<var>", "<var title>")
 
--- a/source.html	Thu Oct 27 15:16:47 2011 -0600
+++ b/source.html	Mon Nov 07 16:01:08 2011 -0700
@@ -5209,6 +5209,105 @@
 <!--@}-->
 <h3>Deleting the selection</h3>
 <!-- @{ -->
+<p class=note>Sometimes one location corresponds to multiple distinct boundary
+points.  For instance, in the DOM {{code|<p>Hello</p>}}, a boundary point might
+lie at the beginning of the text node or the beginning of the element node, but
+these don't logically differ much and will appear the same to the user, so we
+often want to treat them the same.  The algorithms here (currently used only
+for the delete algorithm) allow navigating through such equivalent boundary
+points, for when we want to make the selection as inclusive or exclusive as
+possible.  For deletion, we want to delete as few nodes as possible, so we move
+the start node forward and the end node backward.  In other cases we might do
+the reverse, expanding the selection.
+
+<p>Given a [[boundarypoint]] (<var>node</var>, <var>offset</var>), the
+<dfn>next equivalent point</dfn> is either a [[boundarypoint]] or null, as
+returned by the following algorithm:
+
+<ol>
+  <li>If <var>node</var>'s [[length]] is zero, return null.
+
+  <li class=note>We don't want to move into or out of zero-length nodes,
+  because that would move us straight through them.  For instance, if
+  {{code|{}<span></span>}} were equivalent to {{code|<span>{}</span>}}, it
+  would also be equivalent to {{code|<span></span>{} }}.  This produces very
+  unexpected results for nodes like {{code|<br>}}.
+
+  <li>If <var>offset</var> is <var>node</var>'s [[length]], and
+  <var>node</var>'s [[parent]] is not null, return (<var>node</var>'s
+  [[parent]], 1 + <var>node</var>'s [[index]]).
+
+  <li class=note>For instance, {{code|<span>foo[]</span>}} is equivalent to
+  {{code|<span>foo{}</span>}}, which is equivalent to {{code|<span>foo</span>{}
+  }}.
+
+  <li>If <var>node</var> has a [[child]] with [[index]] <var>offset</var>,
+  and that [[child]]'s [[length]] is not zero, return (that [[child]], 0).
+
+  <li class=note>For instance, {{code|{}<span>foo</span>}} is equivalent to
+  {{code|<span>{}foo</span>}}, which is equivalent to
+  {{code|<span>[]foo</span>}}.  As noted before, though, we don't descend into
+  empty nodes.
+
+  <li>Return null.
+</ol>
+
+<p>Given a [[boundarypoint]] (<var>node</var>, <var>offset</var>), the
+<dfn>previous equivalent point</dfn> is either a [[boundarypoint]] or null, as
+returned by the following algorithm:
+
+<ol>
+  <li>If <var>node</var>'s [[length]] is zero, return null.
+
+  <li>If <var>offset</var> is 0, and <var>node</var>'s [[parent]] is not null,
+  return (<var>node</var>'s [[parent]], <var>node</var>'s [[index]]).
+
+  <li>If <var>node</var> has a [[child]] with [[index]] <var>offset</var>
+  &minus; 1, and that [[child]]'s [[length]] is not zero, return (that
+  [[child]], that [[child]]'s [[length]]).
+
+  <li>Return null.
+</ol>
+
+<!-- Turned out not to be necessary here, but I've left it in case it turns out
+to be handy elsewhere. -->
+<!--
+<p>Two [[boundarypoints]] <var>A</var> and <var>B</var> are <dfn>equivalent
+boundary points</dfn> if the following algorithm returns true:
+
+<p class=note>This is indeed an <a
+href=http://en.wikipedia.org/wiki/Equivalence_relation>equivalence
+relation</a>.  Reflexivity, symmetry, and transitivity are all fairly obvious
+from the definition.  It also isn't hard to check that using <span>next
+equivalent point</span> instead of <span>previous equivalent point</span> would
+yield the same definition, because the two algorithms are inverses if neither
+output is null.
+
+<ol>
+  <li>While <var>A</var>'s <span>previous equivalent point</span> is not null,
+  set <var>A</var> to its <span>previous equivalent point</span>.
+
+  <li>While <var>B</var>'s <span>previous equivalent point</span> is not null,
+  set <var>B</var> to its <span>previous equivalent point</span>.
+
+  <li>Return true if <var>A</var> is the same as <var>B</var>, otherwise false.
+</ol>
+-->
+
+<p>The <dfn>block node of</dfn> a [[node]] <var>node</var> is either a
+<span>block node</span> or null, as returned by the following algorithm:
+
+<ol>
+  <li>While <var>node</var> is an <span>inline node</span>, set <var>node</var>
+  to its [[parent]].
+
+  <li>Return <var>node</var>.
+</ol>
+
+<p>Two [[nodes]] are <dfn>in the same block</dfn> if the <span>block node
+of</span> the first is non-null and the same as the <span>block node of</span>
+the second.
+
 <p class=comments>TODO: Consider what should happen for block merging in corner
 cases like display: inline-table.
 
@@ -5268,105 +5367,21 @@
   and <var>end offset</var> be the <span>active range</span>'s [[rangestart]]
   and [[rangeend]] [[nodes]] and [[bpoffsets]].
 
-  <li class=note>
-  <p>First we adjust the selection so that we don't delete more than we should.
-
-  <li>
-  <div class=comments>
-  <p>We don't want to keep going when we hit an element with no children,
-  because then we'd do something like
-
-<pre>
-foo{&lt;br />bar]
--> foo&lt;br>{&lt;/br>bar]
--> foo&lt;br />{bar]
--> foo&lt;br />[bar]</pre>
-
-  <p>and we deselected the {{code|<br>}}.
-  </div>
-
-  <p>While <var>start node</var> has at least one [[child]]:
-
-  <ol>
-    <li>
-    <div class=comments>
-    <p>For instance:
-
-<pre>&lt;b>foo[&lt;/b>&lt;i>bar]&lt;/i>
--> &lt;b>foo{&lt;/b>&lt;i>bar]&lt;/i>
--> &lt;b>foo&lt;/b>{&lt;i>bar]&lt;/i></pre>
-
-    <p>Then the next step will make it {{code|<b>foo</b><i>[bar]</i>}}.
-
-    <p>We don't want to do this for block nodes, because that would lead to
-    something like
-
-      <pre>&lt;p>foo[&lt;/p>&lt;p>]bar&lt;p></pre>
-
-    <p>ultimately collapsing, which is wrong.  Once we do the deletion, it
-    needs to wind up {{code|<p>foo[]bar</p>}}, whereas an actually collapsed
-    selection should do nothing.
-    </div>
-
-    <p>If <var>start offset</var> is <var>start node</var>'s [[length]], and
-    <var>start node</var>'s [[parent]] is <span>in the same editing
-    host</span>, and <var>start node</var> is an <span>inline node</span>, set
-    <var>start offset</var> to one plus the [[index]] of <var>start node</var>,
-    then set <var>start node</var> to its [[parent]] and continue this loop
-    from the beginning.
-
-    <li>
-    <p class=comments>This happens if the first step brought us all the way up
-    to the root.  The step immediately after this loop will bring us back down
-    again.
-
-    <p>If <var>start offset</var> is <var>start node</var>'s [[length]],
-    break from this loop.
-
-    <li>Let <var>reference node</var> be the [[child]] of <var>start node</var>
-    with [[index]] equal to <var>start offset</var>.
-
-    <li>
-    <div class=comments>
-    <p>Don't descend into an element with no children, since then it won't get
-    deleted even if it's selected.  Don't descend into a block node, because
-    then we might wind up not merging blocks when we should, e.g.
-
-<pre>foo{&lt;p>}bar&lt;/p>
--> foo&lt;p>{}bar&lt;/p></pre>
-
-    <p>and nothing gets changed.
-    </div>
-
-    <p>If <var>reference node</var> is a <span>block node</span> or an
-    [[element]] with no [[children]], or is neither an [[element]] nor a
-    [[text]] node, break from this loop.
-
-    <li>Set <var>start node</var> to <var>reference node</var> and <var>start
-    offset</var> to 0.
-  </ol>
-
-  <li>While <var>end node</var> has at least one [[child]]:
-
-  <ol>
-    <li>If <var>end offset</var> is 0, and <var>end node</var>'s
-    [[parent]] is <span>in the same editing host</span>, and <var>end
-    node</var> is an <span>inline node</span>, set <var>end offset</var> to the
-    [[index]] of <var>end node</var>, then set <var>end node</var> to its
-    [[parent]] and continue this loop from the beginning.
-
-    <li>If <var>end offset</var> is 0, break from this loop.
-
-    <li>Let <var>reference node</var> be the [[child]] of <var>end node</var>
-    with [[index]] equal to <var>end offset</var> minus one.
-
-    <li>If <var>reference node</var> is a <span>block node</span> or an
-    [[element]] with no [[children]], or is neither an [[element]] nor a
-    [[text]] node, break from this loop.
-
-    <li>Set <var>end node</var> to <var>reference node</var> and <var>end
-    offset</var> to the [[length]] of <var>reference node</var>.
-  </ol>
+  <li>While the <span>next equivalent point</span> for (<var>start node</var>,
+  <var>start offset</var>) is not null, and that [[boundarypoint]]'s [[node]]
+  is <span>in the same editing host</span> and <span>in the same block</span>
+  as <var>start node</var>, set (<var>start node</var>, <var>start
+  offset</var>) to its <span>next equivalent point</span>.
+
+  <li class=note>We don't want to leave the current block because otherwise,
+  for instance, a selection of {{code|<p>foo[</p><p>]bar</p>}} would be treated
+  as empty and nothing would be deleted, when we want it to merge the blocks.
+
+  <li>While the <span>previous equivalent point</span> for (<var>end
+  node</var>, <var>end offset</var>) is not null, and that [[boundarypoint]]'s
+  [[node]] is <span>in the same editing host</span> and <span>in the same
+  block</span> as <var>end node</var>, set (<var>end node</var>, <var>end
+  offset</var>) to its <span>previous equivalent point</span>.
 
   <li>If (<var>end node</var>, <var>end offset</var>) is not [[bpafter]]
   (<var>start node</var>, <var>start offset</var>):