dom/imptests/editing/selecttest/common.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 "use strict";
     2 // TODO: iframes, contenteditable/designMode
     4 // Everything is done in functions in this test harness, so we have to declare
     5 // all the variables before use to make sure they can be reused.
     6 var selection;
     7 var testDiv, paras, detachedDiv, detachedPara1, detachedPara2,
     8 	foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement,
     9 	detachedXmlElement, detachedTextNode, foreignTextNode,
    10 	detachedForeignTextNode, xmlTextNode, detachedXmlTextNode,
    11 	processingInstruction, detachedProcessingInstruction, comment,
    12 	detachedComment, foreignComment, detachedForeignComment, xmlComment,
    13 	detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype,
    14 	foreignDoctype, xmlDoctype;
    15 var testRanges, testPoints, testNodes;
    17 function setupRangeTests() {
    18 	selection = getSelection();
    19 	testDiv = document.querySelector("#test");
    20 	if (testDiv) {
    21 		testDiv.parentNode.removeChild(testDiv);
    22 	}
    23 	testDiv = document.createElement("div");
    24 	testDiv.id = "test";
    25 	document.body.insertBefore(testDiv, document.body.firstChild);
    26 	// Test some diacritics, to make sure browsers are using code units here
    27 	// and not something like grapheme clusters.
    28 	testDiv.innerHTML = "<p id=a>A&#x308;b&#x308;c&#x308;d&#x308;e&#x308;f&#x308;g&#x308;h&#x308;\n"
    29 		+ "<p id=b style=display:none>Ijklmnop\n"
    30 		+ "<p id=c>Qrstuvwx"
    31 		+ "<p id=d style=display:none>Yzabcdef"
    32 		+ "<p id=e style=display:none>Ghijklmn";
    33 	paras = testDiv.querySelectorAll("p");
    35 	detachedDiv = document.createElement("div");
    36 	detachedPara1 = document.createElement("p");
    37 	detachedPara1.appendChild(document.createTextNode("Opqrstuv"));
    38 	detachedPara2 = document.createElement("p");
    39 	detachedPara2.appendChild(document.createTextNode("Wxyzabcd"));
    40 	detachedDiv.appendChild(detachedPara1);
    41 	detachedDiv.appendChild(detachedPara2);
    43 	// Opera doesn't automatically create a doctype for a new HTML document,
    44 	// contrary to spec.  It also doesn't let you add doctypes to documents
    45 	// after the fact through any means I've tried.  So foreignDoc in Opera
    46 	// will have no doctype, foreignDoctype will be null, and Opera will fail
    47 	// some tests somewhat mysteriously as a result.
    48 	foreignDoc = document.implementation.createHTMLDocument("");
    49 	foreignPara1 = foreignDoc.createElement("p");
    50 	foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl"));
    51 	foreignPara2 = foreignDoc.createElement("p");
    52 	foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst"));
    53 	foreignDoc.body.appendChild(foreignPara1);
    54 	foreignDoc.body.appendChild(foreignPara2);
    56 	// Now we get to do really silly stuff, which nobody in the universe is
    57 	// ever going to actually do, but the spec defines behavior, so too bad.
    58 	// Testing is fun!
    59 	xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y");
    60 	xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
    61 	detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names");
    62 	detachedTextNode = document.createTextNode("Uvwxyzab");
    63 	detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij");
    64 	detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr");
    65 	// PIs only exist in XML documents, so don't bother with document or
    66 	// foreignDoc.
    67 	detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp");
    68 	detachedComment = document.createComment("Stuvwxyz");
    69 	// Hurrah, we finally got to "z" at the end!
    70 	detachedForeignComment = foreignDoc.createComment("אריה יהודה");
    71 	detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר");
    73 	// We should also test with document fragments that actually contain stuff
    74 	// . . . but, maybe later.
    75 	docfrag = document.createDocumentFragment();
    76 	foreignDocfrag = foreignDoc.createDocumentFragment();
    77 	xmlDocfrag = xmlDoc.createDocumentFragment();
    79 	xmlElement = xmlDoc.createElement("igiveuponcreativenames");
    80 	xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti");
    81 	xmlElement.appendChild(xmlTextNode);
    82 	processingInstruction = xmlDoc.createProcessingInstruction("somePI", 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?');
    83 	xmlDoc.appendChild(xmlElement);
    84 	xmlDoc.appendChild(processingInstruction);
    85 	xmlComment = xmlDoc.createComment("I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt");
    86 	xmlDoc.appendChild(xmlComment);
    88 	comment = document.createComment("Alphabet soup?");
    89 	testDiv.appendChild(comment);
    91 	foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things.  I\'ve seen non-native speakers trip up on this.');
    92 	foreignDoc.appendChild(foreignComment);
    93 	foreignTextNode = foreignDoc.createTextNode("I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now.");
    94 	foreignDoc.body.appendChild(foreignTextNode);
    96 	doctype = document.doctype;
    97 	foreignDoctype = foreignDoc.doctype;
    99 	testRanges = [
   100 		// Various ranges within the text node children of different
   101 		// paragraphs.  All should be valid.
   102 		"[paras[0].firstChild, 0, paras[0].firstChild, 0]",
   103 		"[paras[0].firstChild, 0, paras[0].firstChild, 1]",
   104 		"[paras[0].firstChild, 2, paras[0].firstChild, 8]",
   105 		"[paras[0].firstChild, 2, paras[0].firstChild, 9]",
   106 		"[paras[1].firstChild, 0, paras[1].firstChild, 0]",
   107 		"[paras[1].firstChild, 0, paras[1].firstChild, 1]",
   108 		"[paras[1].firstChild, 2, paras[1].firstChild, 8]",
   109 		"[paras[1].firstChild, 2, paras[1].firstChild, 9]",
   110 		"[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]",
   111 		"[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]",
   112 		"[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]",
   113 		"[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]",
   114 		"[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]",
   115 		"[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]",
   116 		// Now try testing some elements, not just text nodes.
   117 		"[document.documentElement, 0, document.documentElement, 1]",
   118 		"[document.documentElement, 0, document.documentElement, 2]",
   119 		"[document.documentElement, 1, document.documentElement, 2]",
   120 		"[document.head, 1, document.head, 1]",
   121 		"[document.body, 0, document.body, 1]",
   122 		"[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]",
   123 		"[foreignDoc.head, 1, foreignDoc.head, 1]",
   124 		"[foreignDoc.body, 0, foreignDoc.body, 0]",
   125 		"[paras[0], 0, paras[0], 0]",
   126 		"[paras[0], 0, paras[0], 1]",
   127 		"[detachedPara1, 0, detachedPara1, 0]",
   128 		"[detachedPara1, 0, detachedPara1, 1]",
   129 		// Now try some ranges that span elements.
   130 		"[paras[0].firstChild, 0, paras[1].firstChild, 0]",
   131 		"[paras[0].firstChild, 0, paras[1].firstChild, 8]",
   132 		"[paras[0].firstChild, 3, paras[3], 1]",
   133 		// How about something that spans a node and its descendant?
   134 		"[paras[0], 0, paras[0].firstChild, 7]",
   135 		"[testDiv, 2, paras[4], 1]",
   136 		"[testDiv, 1, paras[2].firstChild, 5]",
   137 		"[document.documentElement, 1, document.body, 0]",
   138 		"[foreignDoc.documentElement, 1, foreignDoc.body, 0]",
   139 		// Then a few more interesting things just for good measure.
   140 		"[document, 0, document, 1]",
   141 		"[document, 0, document, 2]",
   142 		"[document, 1, document, 2]",
   143 		"[testDiv, 0, comment, 5]",
   144 		"[paras[2].firstChild, 4, comment, 2]",
   145 		"[paras[3], 1, comment, 8]",
   146 		"[foreignDoc, 0, foreignDoc, 0]",
   147 		"[foreignDoc, 1, foreignComment, 2]",
   148 		"[foreignDoc.body, 0, foreignTextNode, 36]",
   149 		"[xmlDoc, 0, xmlDoc, 0]",
   150 		// Opera 11 crashes if you extractContents() a range that ends at offset
   151 		// zero in a comment.  Comment out this line to run the tests successfully.
   152 		"[xmlDoc, 1, xmlComment, 0]",
   153 		"[detachedTextNode, 0, detachedTextNode, 8]",
   154 		"[detachedForeignTextNode, 7, detachedForeignTextNode, 7]",
   155 		"[detachedForeignTextNode, 0, detachedForeignTextNode, 8]",
   156 		"[detachedXmlTextNode, 7, detachedXmlTextNode, 7]",
   157 		"[detachedXmlTextNode, 0, detachedXmlTextNode, 8]",
   158 		"[detachedComment, 3, detachedComment, 4]",
   159 		"[detachedComment, 5, detachedComment, 5]",
   160 		"[detachedForeignComment, 0, detachedForeignComment, 1]",
   161 		"[detachedForeignComment, 4, detachedForeignComment, 4]",
   162 		"[detachedXmlComment, 2, detachedXmlComment, 6]",
   163 		"[docfrag, 0, docfrag, 0]",
   164 		"[foreignDocfrag, 0, foreignDocfrag, 0]",
   165 		"[xmlDocfrag, 0, xmlDocfrag, 0]",
   166 	];
   168 	testPoints = [
   169 		// Various positions within the page, some invalid.  Remember that
   170 		// paras[0] is visible, and paras[1] is display: none.
   171 		"[paras[0].firstChild, -1]",
   172 		"[paras[0].firstChild, 0]",
   173 		"[paras[0].firstChild, 1]",
   174 		"[paras[0].firstChild, 2]",
   175 		"[paras[0].firstChild, 8]",
   176 		"[paras[0].firstChild, 9]",
   177 		"[paras[0].firstChild, 10]",
   178 		"[paras[0].firstChild, 65535]",
   179 		"[paras[1].firstChild, -1]",
   180 		"[paras[1].firstChild, 0]",
   181 		"[paras[1].firstChild, 1]",
   182 		"[paras[1].firstChild, 2]",
   183 		"[paras[1].firstChild, 8]",
   184 		"[paras[1].firstChild, 9]",
   185 		"[paras[1].firstChild, 10]",
   186 		"[paras[1].firstChild, 65535]",
   187 		"[detachedPara1.firstChild, 0]",
   188 		"[detachedPara1.firstChild, 1]",
   189 		"[detachedPara1.firstChild, 8]",
   190 		"[detachedPara1.firstChild, 9]",
   191 		"[foreignPara1.firstChild, 0]",
   192 		"[foreignPara1.firstChild, 1]",
   193 		"[foreignPara1.firstChild, 8]",
   194 		"[foreignPara1.firstChild, 9]",
   195 		// Now try testing some elements, not just text nodes.
   196 		"[document.documentElement, -1]",
   197 		"[document.documentElement, 0]",
   198 		"[document.documentElement, 1]",
   199 		"[document.documentElement, 2]",
   200 		"[document.documentElement, 7]",
   201 		"[document.head, 1]",
   202 		"[document.body, 3]",
   203 		"[foreignDoc.documentElement, 0]",
   204 		"[foreignDoc.documentElement, 1]",
   205 		"[foreignDoc.head, 0]",
   206 		"[foreignDoc.body, 1]",
   207 		"[paras[0], 0]",
   208 		"[paras[0], 1]",
   209 		"[paras[0], 2]",
   210 		"[paras[1], 0]",
   211 		"[paras[1], 1]",
   212 		"[paras[1], 2]",
   213 		"[detachedPara1, 0]",
   214 		"[detachedPara1, 1]",
   215 		"[testDiv, 0]",
   216 		"[testDiv, 3]",
   217 		// Then a few more interesting things just for good measure.
   218 		"[document, -1]",
   219 		"[document, 0]",
   220 		"[document, 1]",
   221 		"[document, 2]",
   222 		"[document, 3]",
   223 		"[comment, -1]",
   224 		"[comment, 0]",
   225 		"[comment, 4]",
   226 		"[comment, 96]",
   227 		"[foreignDoc, 0]",
   228 		"[foreignDoc, 1]",
   229 		"[foreignComment, 2]",
   230 		"[foreignTextNode, 0]",
   231 		"[foreignTextNode, 36]",
   232 		"[xmlDoc, -1]",
   233 		"[xmlDoc, 0]",
   234 		"[xmlDoc, 1]",
   235 		"[xmlDoc, 5]",
   236 		"[xmlComment, 0]",
   237 		"[xmlComment, 4]",
   238 		"[processingInstruction, 0]",
   239 		"[processingInstruction, 5]",
   240 		"[processingInstruction, 9]",
   241 		"[detachedTextNode, 0]",
   242 		"[detachedTextNode, 8]",
   243 		"[detachedForeignTextNode, 0]",
   244 		"[detachedForeignTextNode, 8]",
   245 		"[detachedXmlTextNode, 0]",
   246 		"[detachedXmlTextNode, 8]",
   247 		"[detachedProcessingInstruction, 12]",
   248 		"[detachedComment, 3]",
   249 		"[detachedComment, 5]",
   250 		"[detachedForeignComment, 0]",
   251 		"[detachedForeignComment, 4]",
   252 		"[detachedXmlComment, 2]",
   253 		"[docfrag, 0]",
   254 		"[foreignDocfrag, 0]",
   255 		"[xmlDocfrag, 0]",
   256 		"[doctype, 0]",
   257 		"[doctype, -17]",
   258 		"[doctype, 1]",
   259 		"[foreignDoctype, 0]",
   260 		"[xmlDoctype, 0]",
   261 	];
   263 	testNodes = [
   264 		"paras[0]",
   265 		"paras[0].firstChild",
   266 		"paras[1]",
   267 		"paras[1].firstChild",
   268 		"foreignPara1",
   269 		"foreignPara1.firstChild",
   270 		"detachedPara1",
   271 		"detachedPara1.firstChild",
   272 		"detachedPara1",
   273 		"detachedPara1.firstChild",
   274 		"testDiv",
   275 		"document",
   276 		"detachedDiv",
   277 		"detachedPara2",
   278 		"foreignDoc",
   279 		"foreignPara2",
   280 		"xmlDoc",
   281 		"xmlElement",
   282 		"detachedXmlElement",
   283 		"detachedTextNode",
   284 		"foreignTextNode",
   285 		"detachedForeignTextNode",
   286 		"xmlTextNode",
   287 		"detachedXmlTextNode",
   288 		"processingInstruction",
   289 		"detachedProcessingInstruction",
   290 		"comment",
   291 		"detachedComment",
   292 		"foreignComment",
   293 		"detachedForeignComment",
   294 		"xmlComment",
   295 		"detachedXmlComment",
   296 		"docfrag",
   297 		"foreignDocfrag",
   298 		"xmlDocfrag",
   299 		"doctype",
   300 		"foreignDoctype",
   301 		"xmlDoctype",
   302 	];
   303 }
   304 if ("setup" in window) {
   305 	setup(setupRangeTests);
   306 } else {
   307 	// Presumably we're running from within an iframe or something
   308 	setupRangeTests();
   309 }
   311 /**
   312  * Return the length of a node as specified in DOM Range.
   313  */
   314 function getNodeLength(node) {
   315 	if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
   316 		return 0;
   317 	}
   318 	if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) {
   319 		return node.length;
   320 	}
   321 	return node.childNodes.length;
   322 }
   324 /**
   325  * Returns the furthest ancestor of a Node as defined by the spec.
   326  */
   327 function furthestAncestor(node) {
   328 	var root = node;
   329 	while (root.parentNode != null) {
   330 		root = root.parentNode;
   331 	}
   332 	return root;
   333 }
   335 /**
   336  * "The ancestor containers of a Node are the Node itself and all its
   337  * ancestors."
   338  *
   339  * Is node1 an ancestor container of node2?
   340  */
   341 function isAncestorContainer(node1, node2) {
   342 	return node1 == node2 ||
   343 		(node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS);
   344 }
   346 /**
   347  * Returns the first Node that's after node in tree order, or null if node is
   348  * the last Node.
   349  */
   350 function nextNode(node) {
   351 	if (node.hasChildNodes()) {
   352 		return node.firstChild;
   353 	}
   354 	return nextNodeDescendants(node);
   355 }
   357 /**
   358  * Returns the last Node that's before node in tree order, or null if node is
   359  * the first Node.
   360  */
   361 function previousNode(node) {
   362 	if (node.previousSibling) {
   363 		node = node.previousSibling;
   364 		while (node.hasChildNodes()) {
   365 			node = node.lastChild;
   366 		}
   367 		return node;
   368 	}
   369 	return node.parentNode;
   370 }
   372 /**
   373  * Returns the next Node that's after node and all its descendants in tree
   374  * order, or null if node is the last Node or an ancestor of it.
   375  */
   376 function nextNodeDescendants(node) {
   377 	while (node && !node.nextSibling) {
   378 		node = node.parentNode;
   379 	}
   380 	if (!node) {
   381 		return null;
   382 	}
   383 	return node.nextSibling;
   384 }
   386 /**
   387  * Returns the ownerDocument of the Node, or the Node itself if it's a
   388  * Document.
   389  */
   390 function ownerDocument(node) {
   391 	return node.nodeType == Node.DOCUMENT_NODE
   392 		? node
   393 		: node.ownerDocument;
   394 }
   396 /**
   397  * Returns true if ancestor is an ancestor of descendant, false otherwise.
   398  */
   399 function isAncestor(ancestor, descendant) {
   400 	if (!ancestor || !descendant) {
   401 		return false;
   402 	}
   403 	while (descendant && descendant != ancestor) {
   404 		descendant = descendant.parentNode;
   405 	}
   406 	return descendant == ancestor;
   407 }
   409 /**
   410  * Returns true if descendant is a descendant of ancestor, false otherwise.
   411  */
   412 function isDescendant(descendant, ancestor) {
   413 	return isAncestor(ancestor, descendant);
   414 }
   416 /**
   417  * The position of two boundary points relative to one another, as defined by
   418  * the spec.
   419  */
   420 function getPosition(nodeA, offsetA, nodeB, offsetB) {
   421 	// "If node A is the same as node B, return equal if offset A equals offset
   422 	// B, before if offset A is less than offset B, and after if offset A is
   423 	// greater than offset B."
   424 	if (nodeA == nodeB) {
   425 		if (offsetA == offsetB) {
   426 			return "equal";
   427 		}
   428 		if (offsetA < offsetB) {
   429 			return "before";
   430 		}
   431 		if (offsetA > offsetB) {
   432 			return "after";
   433 		}
   434 	}
   436 	// "If node A is after node B in tree order, compute the position of (node
   437 	// B, offset B) relative to (node A, offset A). If it is before, return
   438 	// after. If it is after, return before."
   439 	if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
   440 		var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
   441 		if (pos == "before") {
   442 			return "after";
   443 		}
   444 		if (pos == "after") {
   445 			return "before";
   446 		}
   447 	}
   449 	// "If node A is an ancestor of node B:"
   450 	if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
   451 		// "Let child equal node B."
   452 		var child = nodeB;
   454 		// "While child is not a child of node A, set child to its parent."
   455 		while (child.parentNode != nodeA) {
   456 			child = child.parentNode;
   457 		}
   459 		// "If the index of child is less than offset A, return after."
   460 		if (indexOf(child) < offsetA) {
   461 			return "after";
   462 		}
   463 	}
   465 	// "Return before."
   466 	return "before";
   467 }
   469 /**
   470  * "contained" as defined by DOM Range: "A Node node is contained in a range
   471  * range if node's furthest ancestor is the same as range's root, and (node, 0)
   472  * is after range's start, and (node, length of node) is before range's end."
   473  */
   474 function isContained(node, range) {
   475 	var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
   476 	var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
   478 	return furthestAncestor(node) == furthestAncestor(range.startContainer)
   479 		&& pos1 == "after"
   480 		&& pos2 == "before";
   481 }
   483 /**
   484  * "partially contained" as defined by DOM Range: "A Node is partially
   485  * contained in a range if it is an ancestor container of the range's start but
   486  * not its end, or vice versa."
   487  */
   488 function isPartiallyContained(node, range) {
   489 	var cond1 = isAncestorContainer(node, range.startContainer);
   490 	var cond2 = isAncestorContainer(node, range.endContainer);
   491 	return (cond1 && !cond2) || (cond2 && !cond1);
   492 }
   494 /**
   495  * Index of a node as defined by the spec.
   496  */
   497 function indexOf(node) {
   498 	if (!node.parentNode) {
   499 		// No preceding sibling nodes, right?
   500 		return 0;
   501 	}
   502 	var i = 0;
   503 	while (node != node.parentNode.childNodes[i]) {
   504 		i++;
   505 	}
   506 	return i;
   507 }
   509 /**
   510  * extractContents() implementation, following the spec.  If an exception is
   511  * supposed to be thrown, will return a string with the name (e.g.,
   512  * "HIERARCHY_REQUEST_ERR") instead of a document fragment.  It might also
   513  * return an arbitrary human-readable string if a condition is hit that implies
   514  * a spec bug.
   515  */
   516 function myExtractContents(range) {
   517 	// "If the context object's detached flag is set, raise an
   518 	// INVALID_STATE_ERR exception and abort these steps."
   519 	try {
   520 		range.collapsed;
   521 	} catch (e) {
   522 		return "INVALID_STATE_ERR";
   523 	}
   525 	// "Let frag be a new DocumentFragment whose ownerDocument is the same as
   526 	// the ownerDocument of the context object's start node."
   527 	var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
   528 		? range.startContainer
   529 		: range.startContainer.ownerDocument;
   530 	var frag = ownerDoc.createDocumentFragment();
   532 	// "If the context object's start and end are the same, abort this method,
   533 	// returning frag."
   534 	if (range.startContainer == range.endContainer
   535 	&& range.startOffset == range.endOffset) {
   536 		return frag;
   537 	}
   539 	// "Let original start node, original start offset, original end node, and
   540 	// original end offset be the context object's start and end nodes and
   541 	// offsets, respectively."
   542 	var originalStartNode = range.startContainer;
   543 	var originalStartOffset = range.startOffset;
   544 	var originalEndNode = range.endContainer;
   545 	var originalEndOffset = range.endOffset;
   547 	// "If original start node and original end node are the same, and they are
   548 	// a Text or Comment node:"
   549 	if (range.startContainer == range.endContainer
   550 	&& (range.startContainer.nodeType == Node.TEXT_NODE
   551 	|| range.startContainer.nodeType == Node.COMMENT_NODE)) {
   552 		// "Let clone be the result of calling cloneNode(false) on original
   553 		// start node."
   554 		var clone = originalStartNode.cloneNode(false);
   556 		// "Set the data of clone to the result of calling
   557 		// substringData(original start offset, original end offset − original
   558 		// start offset) on original start node."
   559 		clone.data = originalStartNode.substringData(originalStartOffset,
   560 			originalEndOffset - originalStartOffset);
   562 		// "Append clone as the last child of frag."
   563 		frag.appendChild(clone);
   565 		// "Call deleteData(original start offset, original end offset −
   566 		// original start offset) on original start node."
   567 		originalStartNode.deleteData(originalStartOffset,
   568 			originalEndOffset - originalStartOffset);
   570 		// "Abort this method, returning frag."
   571 		return frag;
   572 	}
   574 	// "Let common ancestor equal original start node."
   575 	var commonAncestor = originalStartNode;
   577 	// "While common ancestor is not an ancestor container of original end
   578 	// node, set common ancestor to its own parent."
   579 	while (!isAncestorContainer(commonAncestor, originalEndNode)) {
   580 		commonAncestor = commonAncestor.parentNode;
   581 	}
   583 	// "If original start node is an ancestor container of original end node,
   584 	// let first partially contained child be null."
   585 	var firstPartiallyContainedChild;
   586 	if (isAncestorContainer(originalStartNode, originalEndNode)) {
   587 		firstPartiallyContainedChild = null;
   588 	// "Otherwise, let first partially contained child be the first child of
   589 	// common ancestor that is partially contained in the context object."
   590 	} else {
   591 		for (var i = 0; i < commonAncestor.childNodes.length; i++) {
   592 			if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
   593 				firstPartiallyContainedChild = commonAncestor.childNodes[i];
   594 				break;
   595 			}
   596 		}
   597 		if (!firstPartiallyContainedChild) {
   598 			throw "Spec bug: no first partially contained child!";
   599 		}
   600 	}
   602 	// "If original end node is an ancestor container of original start node,
   603 	// let last partially contained child be null."
   604 	var lastPartiallyContainedChild;
   605 	if (isAncestorContainer(originalEndNode, originalStartNode)) {
   606 		lastPartiallyContainedChild = null;
   607 	// "Otherwise, let last partially contained child be the last child of
   608 	// common ancestor that is partially contained in the context object."
   609 	} else {
   610 		for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
   611 			if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
   612 				lastPartiallyContainedChild = commonAncestor.childNodes[i];
   613 				break;
   614 			}
   615 		}
   616 		if (!lastPartiallyContainedChild) {
   617 			throw "Spec bug: no last partially contained child!";
   618 		}
   619 	}
   621 	// "Let contained children be a list of all children of common ancestor
   622 	// that are contained in the context object, in tree order."
   623 	//
   624 	// "If any member of contained children is a DocumentType, raise a
   625 	// HIERARCHY_REQUEST_ERR exception and abort these steps."
   626 	var containedChildren = [];
   627 	for (var i = 0; i < commonAncestor.childNodes.length; i++) {
   628 		if (isContained(commonAncestor.childNodes[i], range)) {
   629 			if (commonAncestor.childNodes[i].nodeType
   630 			== Node.DOCUMENT_TYPE_NODE) {
   631 				return "HIERARCHY_REQUEST_ERR";
   632 			}
   633 			containedChildren.push(commonAncestor.childNodes[i]);
   634 		}
   635 	}
   637 	// "If original start node is an ancestor container of original end node,
   638 	// set new node to original start node and new offset to original start
   639 	// offset."
   640 	var newNode, newOffset;
   641 	if (isAncestorContainer(originalStartNode, originalEndNode)) {
   642 		newNode = originalStartNode;
   643 		newOffset = originalStartOffset;
   644 	// "Otherwise:"
   645 	} else {
   646 		// "Let reference node equal original start node."
   647 		var referenceNode = originalStartNode;
   649 		// "While reference node's parent is not null and is not an ancestor
   650 		// container of original end node, set reference node to its parent."
   651 		while (referenceNode.parentNode
   652 		&& !isAncestorContainer(referenceNode.parentNode, originalEndNode)) {
   653 			referenceNode = referenceNode.parentNode;
   654 		}
   656 		// "Set new node to the parent of reference node, and new offset to one
   657 		// plus the index of reference node."
   658 		newNode = referenceNode.parentNode;
   659 		newOffset = 1 + indexOf(referenceNode);
   660 	}
   662 	// "If first partially contained child is a Text or Comment node:"
   663 	if (firstPartiallyContainedChild
   664 	&& (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
   665 	|| firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
   666 		// "Let clone be the result of calling cloneNode(false) on original
   667 		// start node."
   668 		var clone = originalStartNode.cloneNode(false);
   670 		// "Set the data of clone to the result of calling substringData() on
   671 		// original start node, with original start offset as the first
   672 		// argument and (length of original start node − original start offset)
   673 		// as the second."
   674 		clone.data = originalStartNode.substringData(originalStartOffset,
   675 			getNodeLength(originalStartNode) - originalStartOffset);
   677 		// "Append clone as the last child of frag."
   678 		frag.appendChild(clone);
   680 		// "Call deleteData() on original start node, with original start
   681 		// offset as the first argument and (length of original start node −
   682 		// original start offset) as the second."
   683 		originalStartNode.deleteData(originalStartOffset,
   684 			getNodeLength(originalStartNode) - originalStartOffset);
   685 	// "Otherwise, if first partially contained child is not null:"
   686 	} else if (firstPartiallyContainedChild) {
   687 		// "Let clone be the result of calling cloneNode(false) on first
   688 		// partially contained child."
   689 		var clone = firstPartiallyContainedChild.cloneNode(false);
   691 		// "Append clone as the last child of frag."
   692 		frag.appendChild(clone);
   694 		// "Let subrange be a new Range whose start is (original start node,
   695 		// original start offset) and whose end is (first partially contained
   696 		// child, length of first partially contained child)."
   697 		var subrange = ownerDoc.createRange();
   698 		subrange.setStart(originalStartNode, originalStartOffset);
   699 		subrange.setEnd(firstPartiallyContainedChild,
   700 			getNodeLength(firstPartiallyContainedChild));
   702 		// "Let subfrag be the result of calling extractContents() on
   703 		// subrange."
   704 		var subfrag = myExtractContents(subrange);
   706 		// "For each child of subfrag, in order, append that child to clone as
   707 		// its last child."
   708 		for (var i = 0; i < subfrag.childNodes.length; i++) {
   709 			clone.appendChild(subfrag.childNodes[i]);
   710 		}
   711 	}
   713 	// "For each contained child in contained children, append contained child
   714 	// as the last child of frag."
   715 	for (var i = 0; i < containedChildren.length; i++) {
   716 		frag.appendChild(containedChildren[i]);
   717 	}
   719 	// "If last partially contained child is a Text or Comment node:"
   720 	if (lastPartiallyContainedChild
   721 	&& (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
   722 	|| lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
   723 		// "Let clone be the result of calling cloneNode(false) on original
   724 		// end node."
   725 		var clone = originalEndNode.cloneNode(false);
   727 		// "Set the data of clone to the result of calling substringData(0,
   728 		// original end offset) on original end node."
   729 		clone.data = originalEndNode.substringData(0, originalEndOffset);
   731 		// "Append clone as the last child of frag."
   732 		frag.appendChild(clone);
   734 		// "Call deleteData(0, original end offset) on original end node."
   735 		originalEndNode.deleteData(0, originalEndOffset);
   736 	// "Otherwise, if last partially contained child is not null:"
   737 	} else if (lastPartiallyContainedChild) {
   738 		// "Let clone be the result of calling cloneNode(false) on last
   739 		// partially contained child."
   740 		var clone = lastPartiallyContainedChild.cloneNode(false);
   742 		// "Append clone as the last child of frag."
   743 		frag.appendChild(clone);
   745 		// "Let subrange be a new Range whose start is (last partially
   746 		// contained child, 0) and whose end is (original end node, original
   747 		// end offset)."
   748 		var subrange = ownerDoc.createRange();
   749 		subrange.setStart(lastPartiallyContainedChild, 0);
   750 		subrange.setEnd(originalEndNode, originalEndOffset);
   752 		// "Let subfrag be the result of calling extractContents() on
   753 		// subrange."
   754 		var subfrag = myExtractContents(subrange);
   756 		// "For each child of subfrag, in order, append that child to clone as
   757 		// its last child."
   758 		for (var i = 0; i < subfrag.childNodes.length; i++) {
   759 			clone.appendChild(subfrag.childNodes[i]);
   760 		}
   761 	}
   763 	// "Set the context object's start and end to (new node, new offset)."
   764 	range.setStart(newNode, newOffset);
   765 	range.setEnd(newNode, newOffset);
   767 	// "Return frag."
   768 	return frag;
   769 }
   771 /**
   772  * insertNode() implementation, following the spec.  If an exception is
   773  * supposed to be thrown, will return a string with the name (e.g.,
   774  * "HIERARCHY_REQUEST_ERR") instead of a document fragment.  It might also
   775  * return an arbitrary human-readable string if a condition is hit that implies
   776  * a spec bug.
   777  */
   778 function myInsertNode(range, newNode) {
   779 	// "If the context object's detached flag is set, raise an
   780 	// INVALID_STATE_ERR exception and abort these steps."
   781 	//
   782 	// Assume that if accessing collapsed throws, it's detached.
   783 	try {
   784 		range.collapsed;
   785 	} catch (e) {
   786 		return "INVALID_STATE_ERR";
   787 	}
   789 	// "If the context object's start node is a Text or Comment node and its
   790 	// parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these
   791 	// steps."
   792 	if ((range.startContainer.nodeType == Node.TEXT_NODE
   793 	|| range.startContainer.nodeType == Node.COMMENT_NODE)
   794 	&& !range.startContainer.parentNode) {
   795 		return "HIERARCHY_REQUEST_ERR";
   796 	}
   798 	// "If the context object's start node is a Text node, run splitText() on
   799 	// it with the context object's start offset as its argument, and let
   800 	// reference node be the result."
   801 	var referenceNode;
   802 	if (range.startContainer.nodeType == Node.TEXT_NODE) {
   803 		// We aren't testing how ranges vary under mutations, and browsers vary
   804 		// in how they mutate for splitText, so let's just force the correct
   805 		// way.
   806 		var start = [range.startContainer, range.startOffset];
   807 		var end = [range.endContainer, range.endOffset];
   809 		referenceNode = range.startContainer.splitText(range.startOffset);
   811 		if (start[0] == end[0]
   812 		&& end[1] > start[1]) {
   813 			end[0] = referenceNode;
   814 			end[1] -= start[1];
   815 		} else if (end[0] == start[0].parentNode
   816 		&& end[1] > indexOf(referenceNode)) {
   817 			end[1]++;
   818 		}
   819 		range.setStart(start[0], start[1]);
   820 		range.setEnd(end[0], end[1]);
   821 	// "Otherwise, if the context object's start node is a Comment, let
   822 	// reference node be the context object's start node."
   823 	} else if (range.startContainer.nodeType == Node.COMMENT_NODE) {
   824 		referenceNode = range.startContainer;
   825 	// "Otherwise, let reference node be the child of the context object's
   826 	// start node with index equal to the context object's start offset, or
   827 	// null if there is no such child."
   828 	} else {
   829 		referenceNode = range.startContainer.childNodes[range.startOffset];
   830 		if (typeof referenceNode == "undefined") {
   831 			referenceNode = null;
   832 		}
   833 	}
   835 	// "If reference node is null, let parent node be the context object's
   836 	// start node."
   837 	var parentNode;
   838 	if (!referenceNode) {
   839 		parentNode = range.startContainer;
   840 	// "Otherwise, let parent node be the parent of reference node."
   841 	} else {
   842 		parentNode = referenceNode.parentNode;
   843 	}
   845 	// "Call insertBefore(newNode, reference node) on parent node, re-raising
   846 	// any exceptions that call raised."
   847 	try {
   848 		parentNode.insertBefore(newNode, referenceNode);
   849 	} catch (e) {
   850 		return getDomExceptionName(e);
   851 	}
   852 }
   854 /**
   855  * Asserts that two nodes are equal, in the sense of isEqualNode().  If they
   856  * aren't, tries to print a relatively informative reason why not.  TODO: Move
   857  * this to testharness.js?
   858  */
   859 function assertNodesEqual(actual, expected, msg) {
   860 	if (!actual.isEqualNode(expected)) {
   861 		msg = "Actual and expected mismatch for " + msg + ".  ";
   863 		while (actual && expected) {
   864 			assert_true(actual.nodeType === expected.nodeType
   865 				&& actual.nodeName === expected.nodeName
   866 				&& actual.nodeValue === expected.nodeValue
   867 				&& actual.childNodes.length === expected.childNodes.length,
   868 				"First differing node: expected " + format_value(expected)
   869 				+ ", got " + format_value(actual));
   870 			actual = nextNode(actual);
   871 			expected = nextNode(expected);
   872 		}
   874 		assert_unreached("DOMs were not equal but we couldn't figure out why");
   875 	}
   876 }
   878 /**
   879  * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR").  In
   880  * theory this should be just e.name, but in practice it's not.  So I could
   881  * legitimately just return e.name, but then every engine but WebKit would fail
   882  * every test, since no one seems to care much for standardizing DOMExceptions.
   883  * Instead I mangle it to account for browser bugs, so as not to fail
   884  * insertNode() tests (for instance) for insertBefore() bugs.  Of course, a
   885  * standards-compliant browser will work right in any event.
   886  *
   887  * If the exception has no string property called "name" or "message", we just
   888  * re-throw it.
   889  */
   890 function getDomExceptionName(e) {
   891 	if (typeof e.name == "string"
   892 	&& /^[A-Z_]+_ERR$/.test(e.name)) {
   893 		// Either following the standard, or prefixing NS_ERROR_DOM (I'm
   894 		// looking at you, Gecko).
   895 		return e.name.replace(/^NS_ERROR_DOM_/, "");
   896 	}
   898 	if (typeof e.message == "string"
   899 	&& /^[A-Z_]+_ERR$/.test(e.message)) {
   900 		// Opera
   901 		return e.message;
   902 	}
   904 	if (typeof e.message == "string"
   905 	&& /^DOM Exception:/.test(e.message)) {
   906 		// IE
   907 		return /[A-Z_]+_ERR/.exec(e.message)[0];
   908 	}
   910 	throw e;
   911 }
   913 /**
   914  * Given an array of endpoint data [start container, start offset, end
   915  * container, end offset], returns a Range with those endpoints.
   916  */
   917 function rangeFromEndpoints(endpoints) {
   918 	// If we just use document instead of the ownerDocument of endpoints[0],
   919 	// WebKit will throw on setStart/setEnd.  This is a WebKit bug, but it's in
   920 	// range, not selection, so we don't want to fail anything for it.
   921 	var range = ownerDocument(endpoints[0]).createRange();
   922 	range.setStart(endpoints[0], endpoints[1]);
   923 	range.setEnd(endpoints[2], endpoints[3]);
   924 	return range;
   925 }
   927 /**
   928  * Given an array of endpoint data [start container, start offset, end
   929  * container, end offset], sets the selection to have those endpoints.  Uses
   930  * addRange, so the range will be forwards.  Accepts an empty array for
   931  * endpoints, in which case the selection will just be emptied.
   932  */
   933 function setSelectionForwards(endpoints) {
   934 	selection.removeAllRanges();
   935 	if (endpoints.length) {
   936 		selection.addRange(rangeFromEndpoints(endpoints));
   937 	}
   938 }
   940 /**
   941  * Given an array of endpoint data [start container, start offset, end
   942  * container, end offset], sets the selection to have those endpoints, with the
   943  * direction backwards.  Uses extend, so it will throw in IE.  Accepts an empty
   944  * array for endpoints, in which case the selection will just be emptied.
   945  */
   946 function setSelectionBackwards(endpoints) {
   947 	selection.removeAllRanges();
   948 	if (endpoints.length) {
   949 		selection.collapse(endpoints[2], endpoints[3]);
   950 		selection.extend(endpoints[0], endpoints[1]);
   951 	}
   952 }

mercurial