Rewrite extend() stuff to modern standards
authorAryeh Gregor <ayg@aryeh.name>
Thu, 12 Jan 2012 11:44:11 -0700
changeset 689 f4ad311eb645
parent 688 abb6e4cc12ec
child 690 f6680bdd77b4
Rewrite extend() stuff to modern standards

This makes everything simpler, clearer, and shorter.
editing.html
preprocess
selecttest/extend.html
source.html
--- a/editing.html	Thu Jan 12 10:17:39 2012 -0700
+++ b/editing.html	Thu Jan 12 11:44:11 2012 -0700
@@ -792,6 +792,9 @@
 forwards (including if the user didn't create the <a href=#concept-selection title=concept-selection>selection</a>, created it by
 selecting an entire part of the page using a keyboard shortcut, etc.).
 
+<p class=XXX>Wouldn't it make more sense if addRange()/removeRange() reset
+direction?
+
 <p><a href=#concept-selection title=concept-selection>Selections</a> also have an <dfn id=anchor>anchor</dfn> and a <dfn id=focus>focus</dfn>.  If
 the <a href=#concept-selection title=concept-selection>selection</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a> is null, its <a href=#anchor>anchor</a> and
 <a href=#focus>focus</a> are both null.  If the <a href=#concept-selection title=concept-selection>selection</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a> is not null
@@ -807,14 +810,14 @@
   readonly attribute unsigned long <a href=#dom-selection-focusoffset title=dom-Selection-focusOffset>focusOffset</a>;
 
   readonly attribute boolean <a href=#dom-selection-iscollapsed title=dom-Selection-isCollapsed>isCollapsed</a>;
-  void               <a href=#dom-selection-collapse title=dom-Selection-collapse>collapse</a>(<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#node>Node</a> parentNode, unsigned long offset);
+  void               <a href=#dom-selection-collapse title=dom-Selection-collapse>collapse</a>(<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#node>Node</a> node, unsigned long offset);
   void               <a href=#dom-selection-collapsetostart title=dom-Selection-collapseToStart>collapseToStart</a>();
   void               <a href=#dom-selection-collapsetoend title=dom-Selection-collapseToEnd>collapseToEnd</a>();
 
-  void               <a href=#dom-selection-extend title=dom-Selection-extend>extend</a>(<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#node>Node</a> parentNode, unsigned long offset);
+  void               <a href=#dom-selection-extend title=dom-Selection-extend>extend</a>(<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#node>Node</a> node, unsigned long offset);
   void               <a href=#dom-selection-modify title=dom-Selection-modify>modify</a>(DOMString alter, DOMString direction, DOMString granularity);
 
-  void               <a href=#dom-selection-selectallchildren title=dom-Selection-selectAllChildren>selectAllChildren</a>(<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#node>Node</a> parentNode);
+  void               <a href=#dom-selection-selectallchildren title=dom-Selection-selectAllChildren>selectAllChildren</a>(<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#node>Node</a> node);
   void               <a href=#dom-selection-deletefromdocument title=dom-Selection-deleteFromDocument>deleteFromDocument</a>();
 
   readonly attribute unsigned long <a href=#dom-selection-rangecount title=dom-Selection-rangeCount>rangeCount</a>;
@@ -912,12 +915,14 @@
     <p>Returns true if there's no selection or if the selection is empty.
     Otherwise, returns false.
 
-  <dt><var title="">selection</var> . <code title=dom-Selection-collapse><a href=#dom-selection-collapse>collapse</a></code>(<var title="">parentNode</var>, <var title="">offset</var>)
+  <dt><var title="">selection</var> .
+  <code title=dom-Selection-collapse><a href=#dom-selection-collapse>collapse</a></code>(<var title="">node</var>,
+  <var title="">offset</var>)
   <dd>
     <p>Replaces the selection with a collapsed one at the given position.
 
     <p>Throws an <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#indexsizeerror>IndexSizeError</a></code> exception if <var title="">offset</var> is negative
-    or longer than <var title="">parentNode</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>.
+    or longer than <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>.
 
   <dt><var title="">selection</var> . <code title=dom-Selection-collapseToStart><a href=#dom-selection-collapsetostart>collapseToStart</a></code>()
   <dd>
@@ -933,14 +938,17 @@
 
     <p>Throws an <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#invalidstateerror>InvalidStateError</a></code> exception if there is no selection.
 
-  <dt><var title="">selection</var> . <code title=dom-Selection-extend><a href=#dom-selection-extend>extend</a></code>(<var title="">parentNode</var>, <var title="">offset</var>)
+  <dt><var title="">selection</var> .
+  <code title=dom-Selection-extend><a href=#dom-selection-extend>extend</a></code>(<var title="">node</var>,
+  <var title="">offset</var>)
   <dd>
     <p>Changes the <a href=#focus>focus</a> while leaving the <a href=#anchor>anchor</a> in
     place.
 
-    <p>Throws an <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#invalidstateerror>InvalidStateError</a></code> if there's no selection, and an
+    <p>Throws an <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#invalidstateerror>InvalidStateError</a></code> if there's no selection, an
+    <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#invalidnodetypeerror>InvalidNodeTypeError</a></code> if <var title="">node</var> is a <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-doctype title=concept-doctype>doctype</a>, and an
     <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#indexsizeerror>IndexSizeError</a></code> exception if <var title="">offset</var> is negative or longer
-    than <var title="">parentNode</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>.
+    than <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>.
 
   <dt><var title="">selection</var> . <code title=dom-Selection-modify><a href=#dom-selection-modify>modify</a></code>(<var title="">alter</var>, <var title="">direction</var>, <var title="">granularity</var>)
   <dd>
@@ -962,11 +970,11 @@
 attribute must return true if the <a href=#anchor>anchor</a> and <a href=#focus>focus</a>
 are the same (including if both are null).  Otherwise it must return false.
 
-<p>The <dfn id=dom-selection-collapse title=dom-Selection-collapse><code>collapse(<var title="">parentNode</var>,
+<p>The <dfn id=dom-selection-collapse title=dom-Selection-collapse><code>collapse(<var title="">node</var>,
 <var title="">offset</var>)</code></dfn> method must create a new <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>,
 <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> both its <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> to
-(<var title="">parentNode</var>, <var title="">offset</var>), and set the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s
-<a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a> to the newly-created <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>.
+(<var title="">node</var>, <var title="">offset</var>), and set the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>
+to the newly-created <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>.
 
 <p class=comments>For collapseToStart/End, IE9 mutates the existing range,
 while Firefox 9.0a2 and Chrome 15 dev replace it with a new one.  The spec
@@ -992,11 +1000,9 @@
 (implemented extend() in 2007).  I'm mostly ignoring Opera, because gsnedders
 tells me its implementation isn't compatible.
 
-<p>The <dfn id=dom-selection-extend title=dom-Selection-extend><code>extend(<var title="">parentNode</var>,
+<p>The <dfn id=dom-selection-extend title=dom-Selection-extend><code>extend(<var title="">node</var>,
 <var title="">offset</var>)</code></dfn> method must run these steps:
 
-<p class=XXX>Does this mutate the existing range or make a new one?
-
 <ol>
   <li>
   <p class=comments>Gecko raises a nonstandard exception, WebKit initializes to
@@ -1006,38 +1012,38 @@
   <p>If the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a> is null, <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-throw title=concept-throw>throw</a> an
   <code class=external data-anolis-spec=dom><a href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#invalidstateerror>InvalidStateError</a></code> exception and abort these steps.
 
-  <li>Let <var title="">range</var> be the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>.
-
-  <li>
-  <p class=comments>Gecko does this.  I can't work out what WebKit does, but it
-  seems weird.  Why backwards?  I don't know, it's what Gecko seems to do.
-  (Direction in WebKit does not appear to be black-box detectable in this
-  case.)
-
-  <p>If <var title="">parentNode</var>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-tree-root title=concept-tree-root>root</a> is not the same as <var title="">range</var>'s
-  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-root title=concept-range-root>root</a>, <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> <var title="">range</var>'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> to (<var title="">parentNode</var>, <var title="">offset</var>), set the
-  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s <a href=#concept-selection-dir title=concept-selection-dir>direction</a> to backwards, and abort these steps.
-
   <li>Let <var title="">anchor</var> and <var title="">focus</var> be the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s
-  <a href=#anchor>anchor</a> and <a href=#focus>focus</a>, and let <var title="">newFocus</var> be
-  the <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> given by <var title="">parentNode</var> and <var title="">offset</var>.
-
-  <li>
-  <p class=comments>Gecko actually seems to set the direction to forwards if
-  the selection was collapsed.  But this doesn't make any sense, since it
-  doesn't appear to change the direction otherwise, so I'm just going to call
-  it a bug.  (WebKit's direction here does not seem to be black-box
-  detectable.)
-
-  <p>If <var title="">focus</var> and <var title="">newFocus</var> are the same, abort these
-  steps.
-
-  <li><a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>Set</a> <var title="">range</var>'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> to <var title="">anchor</var> and
-  its <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> to <var title="">newFocus</var>, if <var title="">anchor</var> is <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-before title=concept-range-bp-before>before</a>
-  or equal to <var title="">newFocus</var>; or vice versa, if it's <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>.
-
-  <li>If <var title="">newFocus</var> is <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-before title=concept-range-bp-before>before</a> <var title="">anchor</var>, set the
+  <a href=#anchor>anchor</a> and <a href=#focus>focus</a>, and let <var title="">new focus</var> be
+  the <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>).
+
+  <li>
+  <p class=comments>Firefox 12.0a1 seems to mutate the existing range.  IE9
+  doesn't support extend(), and it's impossible to tell whether Chrome 17 dev
+  or Opera Next 12.00 alpha mutate or replace, because getRangeAt() returns a
+  copy anyway.  Nevertheless, I go against Gecko here, to be consistent with
+  collapse().
+
+  <p>Let <var title="">new range</var> be a new <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>.
+
+  <li>
+  <p class=comments>Gecko sets the direction backwards here.  Why backwards?  I
+  don't know.  I'm ignoring direction for collapsed selections for now.
+
+  <p>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-tree-root title=concept-tree-root>root</a> is not the same as the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s
+  <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-root title=concept-range-root>root</a>, <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> <var title="">new range</var>'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> to (<var title="">node</var>, <var title="">offset</var>).
+
+  <li>Otherwise, if <var title="">anchor</var> is <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-before title=concept-range-bp-before>before</a> or equal to
+  <var title="">new focus</var>, <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> <var title="">new range</var>'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> to
+  <var title="">anchor</var>, then <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> its <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> to
+  <var title="">new focus</var>.
+
+  <li>Otherwise, <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> <var title="">new range</var>'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> to <var title="">new
+  focus</var>, then <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-set title=concept-range-bp-set>set</a> its <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> to <var title="">anchor</var>.
+
+  <li>Set the <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range title=concept-range>range</a> to <var title="">new range</var>.
+
+  <li>If <var title="">new focus</var> is <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-range-bp-before title=concept-range-bp-before>before</a> <var title="">anchor</var>, set the
   <a class=external data-anolis-spec=dom href=http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#context-object>context object</a>'s <a href=#concept-selection-dir title=concept-selection-dir>direction</a> to backwards.  Otherwise, set it to forwards.
 </ol>
 
@@ -1131,7 +1137,7 @@
 <hr>
 
 <dl class=domintro>
-  <dt><var title="">selection</var> . <code title=dom-Selection-selectAllChildren><a href=#dom-selection-selectallchildren>selectAllChildren</a></code>(<var title="">parentNode</var>)
+  <dt><var title="">selection</var> . <code title=dom-Selection-selectAllChildren><a href=#dom-selection-selectallchildren>selectAllChildren</a></code>(<var title="">node</var>)
   <dd>
     <p>Replaces the selection with one that contains all the contents of the
     given element.
--- a/preprocess	Thu Jan 12 10:17:39 2012 -0700
+++ b/preprocess	Thu Jan 12 11:44:11 2012 -0700
@@ -41,6 +41,7 @@
     'dd': '<code data-anolis-spec=html title="the dd element">dd</code>',
     'dl': '<code data-anolis-spec=html title="the dl element">dl</code>',
     'dt': '<code data-anolis-spec=html title="the dt element">dt</code>',
+    'doctype': '<span data-anolis-spec=dom title=concept-doctype>doctype</span>',
     'document': '<span data-anolis-spec=dom title=concept-document>document</span>',
     'documentfragment': '<code data-anolis-spec=dom>DocumentFragment</code>',
     'documenttype': '<code data-anolis-spec=dom>DocumentType</code>',
--- a/selecttest/extend.html	Thu Jan 12 10:17:39 2012 -0700
+++ b/selecttest/extend.html	Thu Jan 12 11:44:11 2012 -0700
@@ -6,232 +6,142 @@
 <script src=common.js></script>
 <div id=log></div>
 <script>
-/**
- * Returns "forwards" if the selection direction is forwards, "backwards" if
- * it's backwards.  This appears not to work in WebKit at all, because there
- * seems to be no way of adding a range or replacing the current range without
- * calling removeAllRanges(), which resets the direction.  So we're nice and
- * look at the current range if possible; otherwise we do some stuff that
- * involves calling removeRange(), which doesn't exist in WebKit, so it will
- * fail the test.
- */
-function getSelectionDirection() {
-	if (selection.anchorNode != selection.focusNode
-	|| selection.anchorOffset != selection.focusOffset) {
-		var range = selection.getRangeAt(selection.rangeCount - 1);
-		// We can determine the direction without mangling anything.
-		if (selection.anchorNode == range.startContainer
-		&& selection.anchorOffset == range.startOffset) {
-			return "forwards";
-		}
-		if (selection.anchorNode == range.endContainer
-		&& selection.anchorOffset == range.endOffset) {
-			return "backwards";
-		}
-		throw "Something buggy with directions";
-	}
+"use strict";
 
-	var range = document.createRange();
-	range.setStart(paras[0].firstChild, 0);
-	range.setEnd(paras[0].firstChild, 1);
-	selection.addRange(range);
-	if (selection.anchorOffset == range.startOffset) {
-		selection.removeRange(range);
-		return "forwards";
-	}
-	if (selection.anchorOffset == range.endOffset) {
-		selection.removeRange(range);
-		return "backwards";
-	}
-}
+// Also test a selection with no ranges
+testRanges.unshift("[]");
 
 /**
  * We test Selections that go both forwards and backwards here.  In the latter
  * case we need to use extend() to force it to go backwards, which is fair
- * enough, since that's what we're testing.
+ * enough, since that's what we're testing.  We test collapsed selections only
+ * once.
  */
-
-var originalSelectionDirection;
-
-function testExtendForwards(initialRanges, extendTarget) {
-	originalSelectionDirection = "forwards";
-	selection.removeAllRanges();
-
-	for (var i = 0; i < initialRanges.length; i += 4) {
-		var range = ownerDocument(initialRanges[i]).createRange();
-		range.setStart(initialRanges[i], initialRanges[i + 1]);
-		range.setEnd(initialRanges[i + 2], initialRanges[i + 3]);
-		selection.addRange(range);
-	}
+for (var i = 0; i < testRanges.length; i++) {
+	var endpoints = eval(testRanges[i]);
+	for (var j = 0; j < testPoints.length; j++) {
+		if (endpoints[0] == endpoints[2]
+		&& endpoints[1] == endpoints[3]) {
+			// Test collapsed selections only once
+			test(function() {
+				setSelectionForwards(endpoints);
+				testExtend(endpoints, eval(testPoints[j]));
+			}, "extend() with range " + i + " " + testRanges[i]
+			+ " and point " + j + " " + testPoints[j]);
+		} else {
+			test(function() {
+				setSelectionForwards(endpoints);
+				testExtend(endpoints, eval(testPoints[j]));
+			}, "extend() forwards with range " + i + " " + testRanges[i]
+			+ " and point " + j + " " + testPoints[j]);
 
-	testExtend(extendTarget, initialRanges.length/4);
-}
-
-function testExtendBackwards(initialRanges, extendTarget) {
-	originalSelectionDirection = "backwards";
-	selection.removeAllRanges();
-
-	for (var i = 0; i < initialRanges.length; i += 4) {
-		// To get a backwards selection, we add ranges by appending a
-		// zero-length range at the end, then extend()ing backwards to the
-		// start.  This fails in Opera, since Opera ignores addRange() on a
-		// collapsed range.  FIXME: This doesn't actually make the initial
-		// selection backwards, if the range we're given is collapsed.
-		var range = ownerDocument(initialRanges[i]).createRange();
-		range.setStart(initialRanges[i + 2], initialRanges[i + 3]);
-		range.setEnd(initialRanges[i + 2], initialRanges[i + 3]);
-		selection.addRange(range);
-		selection.extend(initialRanges[i], initialRanges[i + 1]);
+			test(function() {
+				setSelectionBackwards(endpoints);
+				testExtend(endpoints, eval(testPoints[j]));
+			}, "extend() backwards with range " + i + " " + testRanges[i]
+			+ " and point " + j + " " + testPoints[j]);
+		}
 	}
-
-	testExtend(extendTarget, initialRanges.length/4);
 }
 
-function testExtend(extendTarget, numRanges) {
-	assert_equals(selection.rangeCount, numRanges,
-		"Failed sanity check: selection.rangeCount is wrong.  Perhaps addRange() failed.");
+function testExtend(endpoints, target) {
+	assert_equals(getSelection().rangeCount, endpoints.length/4,
+		"Sanity check: rangeCount must be correct");
 
-	var node = extendTarget[0];
-	var offset = extendTarget[1];
+	var node = target[0];
+	var offset = target[1];
 
-	if (selection.rangeCount == 0) {
+	// "If the context object's range is null, throw an InvalidStateError
+	// exception and abort these steps."
+	if (getSelection().rangeCount == 0) {
 		assert_throws("INVALID_STATE_ERR", function() {
 			selection.extend(node, offset);
-		}, "extend() when rangeCount is 0 must throw INVALID_STATE_ERR");
-		return;
-	}
-
-	if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
-		assert_throws("INVALID_NODE_TYPE_ERR", function() {
-			selection.extend(node, offset);
-		}, "extend() to a doctype must throw INVALID_NODE_TYPE_ERR");
-		return;
-	}
-
-	if (offset < 0 || offset > getNodeLength(node)) {
-		assert_throws("INDEX_SIZE_ERR", function() {
-			selection.extend(node, offset);
-		}, "extend() to an offset that's negative or greater than node length (" + getNodeLength(node) + ") must throw INDEX_SIZE_ERR");
-		return;
-	}
-
-	var range = selection.getRangeAt(selection.rangeCount - 1);
-	var rangeRoot = furthestAncestor(range.startContainer);
-	var nodeRoot = furthestAncestor(node);
-
-	assert_equals(rangeRoot, furthestAncestor(range.endContainer),
-		"The furthest ancestor of a Range's start and end must always be the same (I think)");
-
-	if (rangeRoot != nodeRoot) {
-		selection.extend(node, offset);
-		assert_equals(selection.anchorNode, node,
-			"If the furthest ancestors of the range and extend() target differ, anchorNode must be set to the target node");
-		assert_equals(selection.anchorOffset, offset,
-			"If the furthest ancestors of the range and extend() target differ, anchorOffset must be set to the target offset");
-		assert_equals(selection.focusNode, node,
-			"If the furthest ancestors of the range and extend() target differ, focusNode must be set to the target node");
-		assert_equals(selection.focusOffset, offset,
-			"If the furthest ancestors of the range and extend() target differ, focusOffset must be set to the target offset");
-		assert_equals(getSelectionDirection(), "backwards",
-			"If the furthest ancestors of the range and extent() target differ, the new selection must be backwards");
+		}, "extend() when rangeCount is 0 must throw InvalidStateError");
 		return;
 	}
 
-	if (selection.focusNode == node && selection.focusOffset == offset) {
-		// extend() must do nothing.
-		var oldFocusNode = selection.focusNode;
-		var oldFocusOffset = selection.focusOffset;
-		var oldAnchorNode = selection.anchorNode;
-		var oldAnchorOffset = selection.anchorOffset;
-		var oldRanges = [];
-		for (var i = 0; i < selection.rangeCount; i++) {
-			oldRanges.push(selection.getRangeAt(i));
-		}
-		selection.extend(node, offset);
-		assert_equals(selection.focusNode, oldFocusNode,
-			"extend() to the current focus must not change focusNode");
-		assert_equals(selection.focusOffset, oldFocusOffset,
-			"extend() to the current focus must not change focusOffset");
-		assert_equals(selection.anchorNode, oldAnchorNode,
-			"extend() to the current focus must not change anchorNode");
-		assert_equals(selection.anchorOffset, oldAnchorOffset,
-			"extend() to the current focus must not change anchorOffset");
-		assert_equals(selection.rangeCount, oldRanges.length,
-			"extend() to the current focus must not change rangeCount");
-		for (var i = 0; i < oldRanges.length; i++) {
-			assert_equals(selection.getRangeAt(i), oldRanges[i],
-				"extend() to the current focus must not change any Ranges");
-		}
-		assert_equals(getSelectionDirection(), originalSelectionDirection,
-			"extend() of a selection to the current focus must not change direction");
+	assert_equals(getSelection().getRangeAt(0).startContainer, endpoints[0],
+		"Sanity check: startContainer must be correct");
+	assert_equals(getSelection().getRangeAt(0).startOffset, endpoints[1],
+		"Sanity check: startOffset must be correct");
+	assert_equals(getSelection().getRangeAt(0).endContainer, endpoints[2],
+		"Sanity check: endContainer must be correct");
+	assert_equals(getSelection().getRangeAt(0).endOffset, endpoints[3],
+		"Sanity check: endOffset must be correct");
+
+	// "Let anchor and focus be the context object's anchor and focus, and let
+	// new focus be the boundary point (node, offset)."
+	var anchorNode = getSelection().anchorNode;
+	var anchorOffset = getSelection().anchorOffset;
+	var focusNode = getSelection().focusNode;
+	var focusOffset = getSelection().focusOffset;
+
+	// "Let new range be a new range."
+	//
+	// We'll always be setting either new range's start or its end to new
+	// focus, so we'll always throw at some point.  Test that now.
+	//
+	// From DOM4's "set the start or end of a range": "If node is a doctype,
+	// throw an "InvalidNodeTypeError" exception and terminate these steps."
+	if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+		assert_throws("INVALID_NODE_TYPE_ERR", function() {
+			selection.extend(node, offset);
+		}, "extend() to a doctype must throw InvalidNodeTypeError");
 		return;
 	}
 
-	var oldAnchorNode = selection.anchorNode;
-	var oldAnchorOffset = selection.anchorOffset;
-	var oldFocusNode = selection.focusNode;
-	var oldFocusOffset = selection.focusOffset;
-	var oldRanges = [];
-	for (var i = 0; i < selection.rangeCount; i++) {
-		oldRanges.push(selection.getRangeAt(i));
+	// From DOM4's "set the start or end of a range": "If offset is greater
+	// than node's length, throw an "IndexSizeError" exception and terminate
+	// these steps."
+	//
+	// FIXME: We should be casting offset to an unsigned int per WebIDL.  Until
+	// we do, we need the offset < 0 check too.
+	if (offset < 0 || offset > getNodeLength(node)) {
+		assert_throws("INDEX_SIZE_ERR", function() {
+			selection.extend(node, offset);
+		}, "extend() to an offset that's greater than node length (" + getNodeLength(node) + ") must throw IndexSizeError");
+		return;
 	}
-	selection.extend(node, offset);
-	assert_equals(selection.anchorNode, oldAnchorNode,
-		"extend() must not change anchorNode in the usual case");
-	assert_equals(selection.anchorOffset, oldAnchorOffset,
-		"extend() must not change anchorOffset in the usual case");
-	assert_equals(selection.rangeCount, oldRanges.length,
-		"extend() must not change rangeCount in the usual case");
-	for (var i = 0; i < oldRanges.length - 1; i++) {
-		assert_equals(selection.getRangeAt(i), oldRanges[i],
-			"extend() must not change any Range but the last in the usual case");
+
+	// Now back to the editing spec.
+	var originalRange = getSelection().getRangeAt(0);
+
+	// "If node's root is not the same as the context object's range's root,
+	// set new range's start and end to (node, offset)."
+	//
+	// "Otherwise, if anchor is before or equal to new focus, set new range's
+	// start to anchor, then set its end to new focus."
+	//
+	// "Otherwise, set new range's start to new focus, then set its end to
+	// anchor."
+	//
+	// "Set the context object's range to new range."
+	//
+	// "If new focus is before anchor, set the context object's direction to
+	// backwards. Otherwise, set it to forwards."
+	//
+	// The upshot of all these is summed up by just testing the anchor and
+	// offset.
+	getSelection().extend(node, offset);
+
+	if (furthestAncestor(anchorNode) == furthestAncestor(node)) {
+		assert_equals(getSelection().anchorNode, anchorNode,
+			"anchorNode must not change if the node passed to extend() has the same root as the original range");
+		assert_equals(getSelection().anchorOffset, anchorOffset,
+			"anchorOffset must not change if the node passed to extend() has the same root as the original range");
+	} else {
+		assert_equals(getSelection().anchorNode, node,
+			"anchorNode must be the node passed to extend() if it has a different root from the original range");
+		assert_equals(getSelection().anchorOffset, offset,
+			"anchorOffset must be the offset passed to extend() if the node has a different root from the original range");
 	}
-	assert_equals(selection.focusNode, node,
-		"extend() must update focusNode to the target node in the usual case");
-	assert_equals(selection.focusOffset, offset,
-		"extend() must update focusOffset to the target offset in the usual case");
-
-	var expectedDirection;
-	var range = document.createRange();
-	range.setStart(oldAnchorNode, oldAnchorOffset);
-	range.setEnd(oldAnchorNode, oldAnchorOffset);
-	if (range.comparePoint(node, offset) >= 0) {
-		expectedDirection = "forwards";
-	} else {
-		expectedDirection = "backwards";
-	}
-	assert_equals(getSelectionDirection(), expectedDirection,
-		"extend() must set direction appropriately in the usual case");
+	assert_equals(getSelection().focusNode, node,
+		"focusNode must be the node passed to extend()");
+	assert_equals(getSelection().focusOffset, offset,
+		"focusOffset must be the offset passed to extend()");
+	assert_not_equals(getSelection().getRangeAt(0), originalRange,
+		"extend() must replace any existing range with a new one, not mutate the existing one");
 }
 
-// Also test a selection with no ranges
-testRanges.unshift("[]");
-
-var tests = [];
-for (var i = 0; i < testRanges.length; i++) {
-	for (var j = 0; j < testPoints.length; j++) {
-		tests.push([
-			"extend() forwards with range " + i + " " + testRanges[i] + " and point " + j + " " + testPoints[j],
-			eval(testRanges[i]),
-			eval(testPoints[j])
-		]);
-	}
-}
-generate_tests(testExtendForwards, tests);
-
-// Copy-pasted with "forwards" changed to "backwards" :/
-var tests = [];
-for (var i = 0; i < testRanges.length; i++) {
-	for (var j = 0; j < testPoints.length; j++) {
-		tests.push([
-			"extend() backwards with range " + i + " " + testRanges[i] + " and point " + j + " " + testPoints[j],
-			eval(testRanges[i]),
-			eval(testPoints[j])
-		]);
-	}
-}
-generate_tests(testExtendBackwards, tests);
-
-// Let's be tidy.
 testDiv.style.display = "none";
 </script>
--- a/source.html	Thu Jan 12 10:17:39 2012 -0700
+++ b/source.html	Thu Jan 12 11:44:11 2012 -0700
@@ -736,6 +736,9 @@
 forwards (including if the user didn't create the [[selection]], created it by
 selecting an entire part of the page using a keyboard shortcut, etc.).
 
+<p class=XXX>Wouldn't it make more sense if addRange()/removeRange() reset
+direction?
+
 <p>[[Selections]] also have an <dfn>anchor</dfn> and a <dfn>focus</dfn>.  If
 the [[selection]]'s [[range]] is null, its <span>anchor</span> and
 <span>focus</span> are both null.  If the [[selection]]'s [[range]] is not null
@@ -751,14 +754,14 @@
   readonly attribute unsigned long <span title=dom-Selection-focusOffset>focusOffset</span>;
 
   readonly attribute boolean <span title=dom-Selection-isCollapsed>isCollapsed</span>;
-  void               <span title=dom-Selection-collapse>collapse</span>(<span data-anolis-spec=dom>Node</span> parentNode, unsigned long offset);
+  void               <span title=dom-Selection-collapse>collapse</span>(<span data-anolis-spec=dom>Node</span> node, unsigned long offset);
   void               <span title=dom-Selection-collapseToStart>collapseToStart</span>();
   void               <span title=dom-Selection-collapseToEnd>collapseToEnd</span>();
 
-  void               <span title=dom-Selection-extend>extend</span>(<span data-anolis-spec=dom>Node</span> parentNode, unsigned long offset);
+  void               <span title=dom-Selection-extend>extend</span>(<span data-anolis-spec=dom>Node</span> node, unsigned long offset);
   void               <span title=dom-Selection-modify>modify</span>(DOMString alter, DOMString direction, DOMString granularity);
 
-  void               <span title=dom-Selection-selectAllChildren>selectAllChildren</span>(<span data-anolis-spec=dom>Node</span> parentNode);
+  void               <span title=dom-Selection-selectAllChildren>selectAllChildren</span>(<span data-anolis-spec=dom>Node</span> node);
   void               <span title=dom-Selection-deleteFromDocument>deleteFromDocument</span>();
 
   readonly attribute unsigned long <span title=dom-Selection-rangeCount>rangeCount</span>;
@@ -858,12 +861,14 @@
     <p>Returns true if there's no selection or if the selection is empty.
     Otherwise, returns false.
 
-  <dt><var>selection</var> . <code title=dom-Selection-collapse>collapse</code>(<var>parentNode</var>, <var>offset</var>)
+  <dt><var>selection</var> .
+  <code title=dom-Selection-collapse>collapse</code>(<var>node</var>,
+  <var>offset</var>)
   <dd>
     <p>Replaces the selection with a collapsed one at the given position.
 
     <p>Throws an [[IndexSizeError]] exception if <var>offset</var> is negative
-    or longer than <var>parentNode</var>'s [[length]].
+    or longer than <var>node</var>'s [[length]].
 
   <dt><var>selection</var> . <code title=dom-Selection-collapseToStart>collapseToStart</code>()
   <dd>
@@ -879,14 +884,17 @@
 
     <p>Throws an [[InvalidStateError]] exception if there is no selection.
 
-  <dt><var>selection</var> . <code title=dom-Selection-extend>extend</code>(<var>parentNode</var>, <var>offset</var>)
+  <dt><var>selection</var> .
+  <code title=dom-Selection-extend>extend</code>(<var>node</var>,
+  <var>offset</var>)
   <dd>
     <p>Changes the <span>focus</span> while leaving the <span>anchor</span> in
     place.
 
-    <p>Throws an [[InvalidStateError]] if there's no selection, and an
+    <p>Throws an [[InvalidStateError]] if there's no selection, an
+    [[InvalidNodeTypeError]] if <var>node</var> is a [[doctype]], and an
     [[IndexSizeError]] exception if <var>offset</var> is negative or longer
-    than <var>parentNode</var>'s [[length]].
+    than <var>node</var>'s [[length]].
 
   <dt><var>selection</var> . <code title=dom-Selection-modify>modify</code>(<var>alter</var>, <var>direction</var>, <var>granularity</var>)
   <dd>
@@ -908,11 +916,11 @@
 attribute must return true if the <span>anchor</span> and <span>focus</span>
 are the same (including if both are null).  Otherwise it must return false.
 
-<p>The <dfn title=dom-Selection-collapse><code>collapse(<var>parentNode</var>,
+<p>The <dfn title=dom-Selection-collapse><code>collapse(<var>node</var>,
 <var>offset</var>)</code></dfn> method must create a new [[range]],
 [[rangeset]] both its [[rangestart]] and [[rangeend]] to
-(<var>parentNode</var>, <var>offset</var>), and set the [[contextobject]]'s
-[[range]] to the newly-created [[range]].
+(<var>node</var>, <var>offset</var>), and set the [[contextobject]]'s [[range]]
+to the newly-created [[range]].
 
 <p class=comments>For collapseToStart/End, IE9 mutates the existing range,
 while Firefox 9.0a2 and Chrome 15 dev replace it with a new one.  The spec
@@ -940,11 +948,9 @@
 (implemented extend() in 2007).  I'm mostly ignoring Opera, because gsnedders
 tells me its implementation isn't compatible.
 
-<p>The <dfn title=dom-Selection-extend><code>extend(<var>parentNode</var>,
+<p>The <dfn title=dom-Selection-extend><code>extend(<var>node</var>,
 <var>offset</var>)</code></dfn> method must run these steps:
 
-<p class=XXX>Does this mutate the existing range or make a new one?
-
 <ol>
   <li>
   <p class=comments>Gecko raises a nonstandard exception, WebKit initializes to
@@ -954,38 +960,38 @@
   <p>If the [[contextobject]]'s [[range]] is null, [[throw]] an
   [[InvalidStateError]] exception and abort these steps.
 
-  <li>Let <var>range</var> be the [[contextobject]]'s [[range]].
-
-  <li>
-  <p class=comments>Gecko does this.  I can't work out what WebKit does, but it
-  seems weird.  Why backwards?  I don't know, it's what Gecko seems to do.
-  (Direction in WebKit does not appear to be black-box detectable in this
-  case.)
-
-  <p>If <var>parentNode</var>'s [[root]] is not the same as <var>range</var>'s
-  [[rangeroot]], [[rangeset]] <var>range</var>'s [[rangestart]] and
-  [[rangeend]] to (<var>parentNode</var>, <var>offset</var>), set the
-  [[contextobject]]'s [[seldir]] to backwards, and abort these steps.
-
   <li>Let <var>anchor</var> and <var>focus</var> be the [[contextobject]]'s
-  <span>anchor</span> and <span>focus</span>, and let <var>newFocus</var> be
-  the [[boundarypoint]] given by <var>parentNode</var> and <var>offset</var>.
-
-  <li>
-  <p class=comments>Gecko actually seems to set the direction to forwards if
-  the selection was collapsed.  But this doesn't make any sense, since it
-  doesn't appear to change the direction otherwise, so I'm just going to call
-  it a bug.  (WebKit's direction here does not seem to be black-box
-  detectable.)
-
-  <p>If <var>focus</var> and <var>newFocus</var> are the same, abort these
-  steps.
-
-  <li>[[Rangeset]] <var>range</var>'s [[rangestart]] to <var>anchor</var> and
-  its [[rangeend]] to <var>newFocus</var>, if <var>anchor</var> is [[bpbefore]]
-  or equal to <var>newFocus</var>; or vice versa, if it's [[bpafter]].
-
-  <li>If <var>newFocus</var> is [[bpbefore]] <var>anchor</var>, set the
+  <span>anchor</span> and <span>focus</span>, and let <var>new focus</var> be
+  the [[boundarypoint]] (<var>node</var>, <var>offset</var>).
+
+  <li>
+  <p class=comments>Firefox 12.0a1 seems to mutate the existing range.  IE9
+  doesn't support extend(), and it's impossible to tell whether Chrome 17 dev
+  or Opera Next 12.00 alpha mutate or replace, because getRangeAt() returns a
+  copy anyway.  Nevertheless, I go against Gecko here, to be consistent with
+  collapse().
+
+  <p>Let <var>new range</var> be a new [[range]].
+
+  <li>
+  <p class=comments>Gecko sets the direction backwards here.  Why backwards?  I
+  don't know.  I'm ignoring direction for collapsed selections for now.
+
+  <p>If <var>node</var>'s [[root]] is not the same as the [[contextobject]]'s
+  [[range]]'s [[rangeroot]], [[rangeset]] <var>new range</var>'s [[rangestart]]
+  and [[rangeend]] to (<var>node</var>, <var>offset</var>).
+
+  <li>Otherwise, if <var>anchor</var> is [[bpbefore]] or equal to
+  <var>new focus</var>, [[rangeset]] <var>new range</var>'s [[rangestart]] to
+  <var>anchor</var>, then [[rangeset]] its [[rangeend]] to
+  <var>new focus</var>.
+
+  <li>Otherwise, [[rangeset]] <var>new range</var>'s [[rangestart]] to <var>new
+  focus</var>, then [[rangeset]] its [[rangeend]] to <var>anchor</var>.
+
+  <li>Set the [[contextobject]]'s [[range]] to <var>new range</var>.
+
+  <li>If <var>new focus</var> is [[bpbefore]] <var>anchor</var>, set the
   [[contextobject]]'s [[seldir]] to backwards.  Otherwise, set it to forwards.
 </ol>
 
@@ -1080,7 +1086,7 @@
 <hr>
 
 <dl class=domintro>
-  <dt><var>selection</var> . <code title=dom-Selection-selectAllChildren>selectAllChildren</code>(<var>parentNode</var>)
+  <dt><var>selection</var> . <code title=dom-Selection-selectAllChildren>selectAllChildren</code>(<var>node</var>)
   <dd>
     <p>Replaces the selection with one that contains all the contents of the
     given element.