dom/imptests/editing/selecttest/common.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/imptests/editing/selecttest/common.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,952 @@
     1.4 +"use strict";
     1.5 +// TODO: iframes, contenteditable/designMode
     1.6 +
     1.7 +// Everything is done in functions in this test harness, so we have to declare
     1.8 +// all the variables before use to make sure they can be reused.
     1.9 +var selection;
    1.10 +var testDiv, paras, detachedDiv, detachedPara1, detachedPara2,
    1.11 +	foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement,
    1.12 +	detachedXmlElement, detachedTextNode, foreignTextNode,
    1.13 +	detachedForeignTextNode, xmlTextNode, detachedXmlTextNode,
    1.14 +	processingInstruction, detachedProcessingInstruction, comment,
    1.15 +	detachedComment, foreignComment, detachedForeignComment, xmlComment,
    1.16 +	detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype,
    1.17 +	foreignDoctype, xmlDoctype;
    1.18 +var testRanges, testPoints, testNodes;
    1.19 +
    1.20 +function setupRangeTests() {
    1.21 +	selection = getSelection();
    1.22 +	testDiv = document.querySelector("#test");
    1.23 +	if (testDiv) {
    1.24 +		testDiv.parentNode.removeChild(testDiv);
    1.25 +	}
    1.26 +	testDiv = document.createElement("div");
    1.27 +	testDiv.id = "test";
    1.28 +	document.body.insertBefore(testDiv, document.body.firstChild);
    1.29 +	// Test some diacritics, to make sure browsers are using code units here
    1.30 +	// and not something like grapheme clusters.
    1.31 +	testDiv.innerHTML = "<p id=a>A&#x308;b&#x308;c&#x308;d&#x308;e&#x308;f&#x308;g&#x308;h&#x308;\n"
    1.32 +		+ "<p id=b style=display:none>Ijklmnop\n"
    1.33 +		+ "<p id=c>Qrstuvwx"
    1.34 +		+ "<p id=d style=display:none>Yzabcdef"
    1.35 +		+ "<p id=e style=display:none>Ghijklmn";
    1.36 +	paras = testDiv.querySelectorAll("p");
    1.37 +
    1.38 +	detachedDiv = document.createElement("div");
    1.39 +	detachedPara1 = document.createElement("p");
    1.40 +	detachedPara1.appendChild(document.createTextNode("Opqrstuv"));
    1.41 +	detachedPara2 = document.createElement("p");
    1.42 +	detachedPara2.appendChild(document.createTextNode("Wxyzabcd"));
    1.43 +	detachedDiv.appendChild(detachedPara1);
    1.44 +	detachedDiv.appendChild(detachedPara2);
    1.45 +
    1.46 +	// Opera doesn't automatically create a doctype for a new HTML document,
    1.47 +	// contrary to spec.  It also doesn't let you add doctypes to documents
    1.48 +	// after the fact through any means I've tried.  So foreignDoc in Opera
    1.49 +	// will have no doctype, foreignDoctype will be null, and Opera will fail
    1.50 +	// some tests somewhat mysteriously as a result.
    1.51 +	foreignDoc = document.implementation.createHTMLDocument("");
    1.52 +	foreignPara1 = foreignDoc.createElement("p");
    1.53 +	foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl"));
    1.54 +	foreignPara2 = foreignDoc.createElement("p");
    1.55 +	foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst"));
    1.56 +	foreignDoc.body.appendChild(foreignPara1);
    1.57 +	foreignDoc.body.appendChild(foreignPara2);
    1.58 +
    1.59 +	// Now we get to do really silly stuff, which nobody in the universe is
    1.60 +	// ever going to actually do, but the spec defines behavior, so too bad.
    1.61 +	// Testing is fun!
    1.62 +	xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y");
    1.63 +	xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
    1.64 +	detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names");
    1.65 +	detachedTextNode = document.createTextNode("Uvwxyzab");
    1.66 +	detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij");
    1.67 +	detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr");
    1.68 +	// PIs only exist in XML documents, so don't bother with document or
    1.69 +	// foreignDoc.
    1.70 +	detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp");
    1.71 +	detachedComment = document.createComment("Stuvwxyz");
    1.72 +	// Hurrah, we finally got to "z" at the end!
    1.73 +	detachedForeignComment = foreignDoc.createComment("אריה יהודה");
    1.74 +	detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר");
    1.75 +
    1.76 +	// We should also test with document fragments that actually contain stuff
    1.77 +	// . . . but, maybe later.
    1.78 +	docfrag = document.createDocumentFragment();
    1.79 +	foreignDocfrag = foreignDoc.createDocumentFragment();
    1.80 +	xmlDocfrag = xmlDoc.createDocumentFragment();
    1.81 +
    1.82 +	xmlElement = xmlDoc.createElement("igiveuponcreativenames");
    1.83 +	xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti");
    1.84 +	xmlElement.appendChild(xmlTextNode);
    1.85 +	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?');
    1.86 +	xmlDoc.appendChild(xmlElement);
    1.87 +	xmlDoc.appendChild(processingInstruction);
    1.88 +	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");
    1.89 +	xmlDoc.appendChild(xmlComment);
    1.90 +
    1.91 +	comment = document.createComment("Alphabet soup?");
    1.92 +	testDiv.appendChild(comment);
    1.93 +
    1.94 +	foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things.  I\'ve seen non-native speakers trip up on this.');
    1.95 +	foreignDoc.appendChild(foreignComment);
    1.96 +	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.");
    1.97 +	foreignDoc.body.appendChild(foreignTextNode);
    1.98 +
    1.99 +	doctype = document.doctype;
   1.100 +	foreignDoctype = foreignDoc.doctype;
   1.101 +
   1.102 +	testRanges = [
   1.103 +		// Various ranges within the text node children of different
   1.104 +		// paragraphs.  All should be valid.
   1.105 +		"[paras[0].firstChild, 0, paras[0].firstChild, 0]",
   1.106 +		"[paras[0].firstChild, 0, paras[0].firstChild, 1]",
   1.107 +		"[paras[0].firstChild, 2, paras[0].firstChild, 8]",
   1.108 +		"[paras[0].firstChild, 2, paras[0].firstChild, 9]",
   1.109 +		"[paras[1].firstChild, 0, paras[1].firstChild, 0]",
   1.110 +		"[paras[1].firstChild, 0, paras[1].firstChild, 1]",
   1.111 +		"[paras[1].firstChild, 2, paras[1].firstChild, 8]",
   1.112 +		"[paras[1].firstChild, 2, paras[1].firstChild, 9]",
   1.113 +		"[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]",
   1.114 +		"[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]",
   1.115 +		"[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]",
   1.116 +		"[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]",
   1.117 +		"[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]",
   1.118 +		"[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]",
   1.119 +		// Now try testing some elements, not just text nodes.
   1.120 +		"[document.documentElement, 0, document.documentElement, 1]",
   1.121 +		"[document.documentElement, 0, document.documentElement, 2]",
   1.122 +		"[document.documentElement, 1, document.documentElement, 2]",
   1.123 +		"[document.head, 1, document.head, 1]",
   1.124 +		"[document.body, 0, document.body, 1]",
   1.125 +		"[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]",
   1.126 +		"[foreignDoc.head, 1, foreignDoc.head, 1]",
   1.127 +		"[foreignDoc.body, 0, foreignDoc.body, 0]",
   1.128 +		"[paras[0], 0, paras[0], 0]",
   1.129 +		"[paras[0], 0, paras[0], 1]",
   1.130 +		"[detachedPara1, 0, detachedPara1, 0]",
   1.131 +		"[detachedPara1, 0, detachedPara1, 1]",
   1.132 +		// Now try some ranges that span elements.
   1.133 +		"[paras[0].firstChild, 0, paras[1].firstChild, 0]",
   1.134 +		"[paras[0].firstChild, 0, paras[1].firstChild, 8]",
   1.135 +		"[paras[0].firstChild, 3, paras[3], 1]",
   1.136 +		// How about something that spans a node and its descendant?
   1.137 +		"[paras[0], 0, paras[0].firstChild, 7]",
   1.138 +		"[testDiv, 2, paras[4], 1]",
   1.139 +		"[testDiv, 1, paras[2].firstChild, 5]",
   1.140 +		"[document.documentElement, 1, document.body, 0]",
   1.141 +		"[foreignDoc.documentElement, 1, foreignDoc.body, 0]",
   1.142 +		// Then a few more interesting things just for good measure.
   1.143 +		"[document, 0, document, 1]",
   1.144 +		"[document, 0, document, 2]",
   1.145 +		"[document, 1, document, 2]",
   1.146 +		"[testDiv, 0, comment, 5]",
   1.147 +		"[paras[2].firstChild, 4, comment, 2]",
   1.148 +		"[paras[3], 1, comment, 8]",
   1.149 +		"[foreignDoc, 0, foreignDoc, 0]",
   1.150 +		"[foreignDoc, 1, foreignComment, 2]",
   1.151 +		"[foreignDoc.body, 0, foreignTextNode, 36]",
   1.152 +		"[xmlDoc, 0, xmlDoc, 0]",
   1.153 +		// Opera 11 crashes if you extractContents() a range that ends at offset
   1.154 +		// zero in a comment.  Comment out this line to run the tests successfully.
   1.155 +		"[xmlDoc, 1, xmlComment, 0]",
   1.156 +		"[detachedTextNode, 0, detachedTextNode, 8]",
   1.157 +		"[detachedForeignTextNode, 7, detachedForeignTextNode, 7]",
   1.158 +		"[detachedForeignTextNode, 0, detachedForeignTextNode, 8]",
   1.159 +		"[detachedXmlTextNode, 7, detachedXmlTextNode, 7]",
   1.160 +		"[detachedXmlTextNode, 0, detachedXmlTextNode, 8]",
   1.161 +		"[detachedComment, 3, detachedComment, 4]",
   1.162 +		"[detachedComment, 5, detachedComment, 5]",
   1.163 +		"[detachedForeignComment, 0, detachedForeignComment, 1]",
   1.164 +		"[detachedForeignComment, 4, detachedForeignComment, 4]",
   1.165 +		"[detachedXmlComment, 2, detachedXmlComment, 6]",
   1.166 +		"[docfrag, 0, docfrag, 0]",
   1.167 +		"[foreignDocfrag, 0, foreignDocfrag, 0]",
   1.168 +		"[xmlDocfrag, 0, xmlDocfrag, 0]",
   1.169 +	];
   1.170 +
   1.171 +	testPoints = [
   1.172 +		// Various positions within the page, some invalid.  Remember that
   1.173 +		// paras[0] is visible, and paras[1] is display: none.
   1.174 +		"[paras[0].firstChild, -1]",
   1.175 +		"[paras[0].firstChild, 0]",
   1.176 +		"[paras[0].firstChild, 1]",
   1.177 +		"[paras[0].firstChild, 2]",
   1.178 +		"[paras[0].firstChild, 8]",
   1.179 +		"[paras[0].firstChild, 9]",
   1.180 +		"[paras[0].firstChild, 10]",
   1.181 +		"[paras[0].firstChild, 65535]",
   1.182 +		"[paras[1].firstChild, -1]",
   1.183 +		"[paras[1].firstChild, 0]",
   1.184 +		"[paras[1].firstChild, 1]",
   1.185 +		"[paras[1].firstChild, 2]",
   1.186 +		"[paras[1].firstChild, 8]",
   1.187 +		"[paras[1].firstChild, 9]",
   1.188 +		"[paras[1].firstChild, 10]",
   1.189 +		"[paras[1].firstChild, 65535]",
   1.190 +		"[detachedPara1.firstChild, 0]",
   1.191 +		"[detachedPara1.firstChild, 1]",
   1.192 +		"[detachedPara1.firstChild, 8]",
   1.193 +		"[detachedPara1.firstChild, 9]",
   1.194 +		"[foreignPara1.firstChild, 0]",
   1.195 +		"[foreignPara1.firstChild, 1]",
   1.196 +		"[foreignPara1.firstChild, 8]",
   1.197 +		"[foreignPara1.firstChild, 9]",
   1.198 +		// Now try testing some elements, not just text nodes.
   1.199 +		"[document.documentElement, -1]",
   1.200 +		"[document.documentElement, 0]",
   1.201 +		"[document.documentElement, 1]",
   1.202 +		"[document.documentElement, 2]",
   1.203 +		"[document.documentElement, 7]",
   1.204 +		"[document.head, 1]",
   1.205 +		"[document.body, 3]",
   1.206 +		"[foreignDoc.documentElement, 0]",
   1.207 +		"[foreignDoc.documentElement, 1]",
   1.208 +		"[foreignDoc.head, 0]",
   1.209 +		"[foreignDoc.body, 1]",
   1.210 +		"[paras[0], 0]",
   1.211 +		"[paras[0], 1]",
   1.212 +		"[paras[0], 2]",
   1.213 +		"[paras[1], 0]",
   1.214 +		"[paras[1], 1]",
   1.215 +		"[paras[1], 2]",
   1.216 +		"[detachedPara1, 0]",
   1.217 +		"[detachedPara1, 1]",
   1.218 +		"[testDiv, 0]",
   1.219 +		"[testDiv, 3]",
   1.220 +		// Then a few more interesting things just for good measure.
   1.221 +		"[document, -1]",
   1.222 +		"[document, 0]",
   1.223 +		"[document, 1]",
   1.224 +		"[document, 2]",
   1.225 +		"[document, 3]",
   1.226 +		"[comment, -1]",
   1.227 +		"[comment, 0]",
   1.228 +		"[comment, 4]",
   1.229 +		"[comment, 96]",
   1.230 +		"[foreignDoc, 0]",
   1.231 +		"[foreignDoc, 1]",
   1.232 +		"[foreignComment, 2]",
   1.233 +		"[foreignTextNode, 0]",
   1.234 +		"[foreignTextNode, 36]",
   1.235 +		"[xmlDoc, -1]",
   1.236 +		"[xmlDoc, 0]",
   1.237 +		"[xmlDoc, 1]",
   1.238 +		"[xmlDoc, 5]",
   1.239 +		"[xmlComment, 0]",
   1.240 +		"[xmlComment, 4]",
   1.241 +		"[processingInstruction, 0]",
   1.242 +		"[processingInstruction, 5]",
   1.243 +		"[processingInstruction, 9]",
   1.244 +		"[detachedTextNode, 0]",
   1.245 +		"[detachedTextNode, 8]",
   1.246 +		"[detachedForeignTextNode, 0]",
   1.247 +		"[detachedForeignTextNode, 8]",
   1.248 +		"[detachedXmlTextNode, 0]",
   1.249 +		"[detachedXmlTextNode, 8]",
   1.250 +		"[detachedProcessingInstruction, 12]",
   1.251 +		"[detachedComment, 3]",
   1.252 +		"[detachedComment, 5]",
   1.253 +		"[detachedForeignComment, 0]",
   1.254 +		"[detachedForeignComment, 4]",
   1.255 +		"[detachedXmlComment, 2]",
   1.256 +		"[docfrag, 0]",
   1.257 +		"[foreignDocfrag, 0]",
   1.258 +		"[xmlDocfrag, 0]",
   1.259 +		"[doctype, 0]",
   1.260 +		"[doctype, -17]",
   1.261 +		"[doctype, 1]",
   1.262 +		"[foreignDoctype, 0]",
   1.263 +		"[xmlDoctype, 0]",
   1.264 +	];
   1.265 +
   1.266 +	testNodes = [
   1.267 +		"paras[0]",
   1.268 +		"paras[0].firstChild",
   1.269 +		"paras[1]",
   1.270 +		"paras[1].firstChild",
   1.271 +		"foreignPara1",
   1.272 +		"foreignPara1.firstChild",
   1.273 +		"detachedPara1",
   1.274 +		"detachedPara1.firstChild",
   1.275 +		"detachedPara1",
   1.276 +		"detachedPara1.firstChild",
   1.277 +		"testDiv",
   1.278 +		"document",
   1.279 +		"detachedDiv",
   1.280 +		"detachedPara2",
   1.281 +		"foreignDoc",
   1.282 +		"foreignPara2",
   1.283 +		"xmlDoc",
   1.284 +		"xmlElement",
   1.285 +		"detachedXmlElement",
   1.286 +		"detachedTextNode",
   1.287 +		"foreignTextNode",
   1.288 +		"detachedForeignTextNode",
   1.289 +		"xmlTextNode",
   1.290 +		"detachedXmlTextNode",
   1.291 +		"processingInstruction",
   1.292 +		"detachedProcessingInstruction",
   1.293 +		"comment",
   1.294 +		"detachedComment",
   1.295 +		"foreignComment",
   1.296 +		"detachedForeignComment",
   1.297 +		"xmlComment",
   1.298 +		"detachedXmlComment",
   1.299 +		"docfrag",
   1.300 +		"foreignDocfrag",
   1.301 +		"xmlDocfrag",
   1.302 +		"doctype",
   1.303 +		"foreignDoctype",
   1.304 +		"xmlDoctype",
   1.305 +	];
   1.306 +}
   1.307 +if ("setup" in window) {
   1.308 +	setup(setupRangeTests);
   1.309 +} else {
   1.310 +	// Presumably we're running from within an iframe or something
   1.311 +	setupRangeTests();
   1.312 +}
   1.313 +
   1.314 +/**
   1.315 + * Return the length of a node as specified in DOM Range.
   1.316 + */
   1.317 +function getNodeLength(node) {
   1.318 +	if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
   1.319 +		return 0;
   1.320 +	}
   1.321 +	if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) {
   1.322 +		return node.length;
   1.323 +	}
   1.324 +	return node.childNodes.length;
   1.325 +}
   1.326 +
   1.327 +/**
   1.328 + * Returns the furthest ancestor of a Node as defined by the spec.
   1.329 + */
   1.330 +function furthestAncestor(node) {
   1.331 +	var root = node;
   1.332 +	while (root.parentNode != null) {
   1.333 +		root = root.parentNode;
   1.334 +	}
   1.335 +	return root;
   1.336 +}
   1.337 +
   1.338 +/**
   1.339 + * "The ancestor containers of a Node are the Node itself and all its
   1.340 + * ancestors."
   1.341 + *
   1.342 + * Is node1 an ancestor container of node2?
   1.343 + */
   1.344 +function isAncestorContainer(node1, node2) {
   1.345 +	return node1 == node2 ||
   1.346 +		(node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS);
   1.347 +}
   1.348 +
   1.349 +/**
   1.350 + * Returns the first Node that's after node in tree order, or null if node is
   1.351 + * the last Node.
   1.352 + */
   1.353 +function nextNode(node) {
   1.354 +	if (node.hasChildNodes()) {
   1.355 +		return node.firstChild;
   1.356 +	}
   1.357 +	return nextNodeDescendants(node);
   1.358 +}
   1.359 +
   1.360 +/**
   1.361 + * Returns the last Node that's before node in tree order, or null if node is
   1.362 + * the first Node.
   1.363 + */
   1.364 +function previousNode(node) {
   1.365 +	if (node.previousSibling) {
   1.366 +		node = node.previousSibling;
   1.367 +		while (node.hasChildNodes()) {
   1.368 +			node = node.lastChild;
   1.369 +		}
   1.370 +		return node;
   1.371 +	}
   1.372 +	return node.parentNode;
   1.373 +}
   1.374 +
   1.375 +/**
   1.376 + * Returns the next Node that's after node and all its descendants in tree
   1.377 + * order, or null if node is the last Node or an ancestor of it.
   1.378 + */
   1.379 +function nextNodeDescendants(node) {
   1.380 +	while (node && !node.nextSibling) {
   1.381 +		node = node.parentNode;
   1.382 +	}
   1.383 +	if (!node) {
   1.384 +		return null;
   1.385 +	}
   1.386 +	return node.nextSibling;
   1.387 +}
   1.388 +
   1.389 +/**
   1.390 + * Returns the ownerDocument of the Node, or the Node itself if it's a
   1.391 + * Document.
   1.392 + */
   1.393 +function ownerDocument(node) {
   1.394 +	return node.nodeType == Node.DOCUMENT_NODE
   1.395 +		? node
   1.396 +		: node.ownerDocument;
   1.397 +}
   1.398 +
   1.399 +/**
   1.400 + * Returns true if ancestor is an ancestor of descendant, false otherwise.
   1.401 + */
   1.402 +function isAncestor(ancestor, descendant) {
   1.403 +	if (!ancestor || !descendant) {
   1.404 +		return false;
   1.405 +	}
   1.406 +	while (descendant && descendant != ancestor) {
   1.407 +		descendant = descendant.parentNode;
   1.408 +	}
   1.409 +	return descendant == ancestor;
   1.410 +}
   1.411 +
   1.412 +/**
   1.413 + * Returns true if descendant is a descendant of ancestor, false otherwise.
   1.414 + */
   1.415 +function isDescendant(descendant, ancestor) {
   1.416 +	return isAncestor(ancestor, descendant);
   1.417 +}
   1.418 +
   1.419 +/**
   1.420 + * The position of two boundary points relative to one another, as defined by
   1.421 + * the spec.
   1.422 + */
   1.423 +function getPosition(nodeA, offsetA, nodeB, offsetB) {
   1.424 +	// "If node A is the same as node B, return equal if offset A equals offset
   1.425 +	// B, before if offset A is less than offset B, and after if offset A is
   1.426 +	// greater than offset B."
   1.427 +	if (nodeA == nodeB) {
   1.428 +		if (offsetA == offsetB) {
   1.429 +			return "equal";
   1.430 +		}
   1.431 +		if (offsetA < offsetB) {
   1.432 +			return "before";
   1.433 +		}
   1.434 +		if (offsetA > offsetB) {
   1.435 +			return "after";
   1.436 +		}
   1.437 +	}
   1.438 +
   1.439 +	// "If node A is after node B in tree order, compute the position of (node
   1.440 +	// B, offset B) relative to (node A, offset A). If it is before, return
   1.441 +	// after. If it is after, return before."
   1.442 +	if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
   1.443 +		var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
   1.444 +		if (pos == "before") {
   1.445 +			return "after";
   1.446 +		}
   1.447 +		if (pos == "after") {
   1.448 +			return "before";
   1.449 +		}
   1.450 +	}
   1.451 +
   1.452 +	// "If node A is an ancestor of node B:"
   1.453 +	if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
   1.454 +		// "Let child equal node B."
   1.455 +		var child = nodeB;
   1.456 +
   1.457 +		// "While child is not a child of node A, set child to its parent."
   1.458 +		while (child.parentNode != nodeA) {
   1.459 +			child = child.parentNode;
   1.460 +		}
   1.461 +
   1.462 +		// "If the index of child is less than offset A, return after."
   1.463 +		if (indexOf(child) < offsetA) {
   1.464 +			return "after";
   1.465 +		}
   1.466 +	}
   1.467 +
   1.468 +	// "Return before."
   1.469 +	return "before";
   1.470 +}
   1.471 +
   1.472 +/**
   1.473 + * "contained" as defined by DOM Range: "A Node node is contained in a range
   1.474 + * range if node's furthest ancestor is the same as range's root, and (node, 0)
   1.475 + * is after range's start, and (node, length of node) is before range's end."
   1.476 + */
   1.477 +function isContained(node, range) {
   1.478 +	var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
   1.479 +	var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
   1.480 +
   1.481 +	return furthestAncestor(node) == furthestAncestor(range.startContainer)
   1.482 +		&& pos1 == "after"
   1.483 +		&& pos2 == "before";
   1.484 +}
   1.485 +
   1.486 +/**
   1.487 + * "partially contained" as defined by DOM Range: "A Node is partially
   1.488 + * contained in a range if it is an ancestor container of the range's start but
   1.489 + * not its end, or vice versa."
   1.490 + */
   1.491 +function isPartiallyContained(node, range) {
   1.492 +	var cond1 = isAncestorContainer(node, range.startContainer);
   1.493 +	var cond2 = isAncestorContainer(node, range.endContainer);
   1.494 +	return (cond1 && !cond2) || (cond2 && !cond1);
   1.495 +}
   1.496 +
   1.497 +/**
   1.498 + * Index of a node as defined by the spec.
   1.499 + */
   1.500 +function indexOf(node) {
   1.501 +	if (!node.parentNode) {
   1.502 +		// No preceding sibling nodes, right?
   1.503 +		return 0;
   1.504 +	}
   1.505 +	var i = 0;
   1.506 +	while (node != node.parentNode.childNodes[i]) {
   1.507 +		i++;
   1.508 +	}
   1.509 +	return i;
   1.510 +}
   1.511 +
   1.512 +/**
   1.513 + * extractContents() implementation, following the spec.  If an exception is
   1.514 + * supposed to be thrown, will return a string with the name (e.g.,
   1.515 + * "HIERARCHY_REQUEST_ERR") instead of a document fragment.  It might also
   1.516 + * return an arbitrary human-readable string if a condition is hit that implies
   1.517 + * a spec bug.
   1.518 + */
   1.519 +function myExtractContents(range) {
   1.520 +	// "If the context object's detached flag is set, raise an
   1.521 +	// INVALID_STATE_ERR exception and abort these steps."
   1.522 +	try {
   1.523 +		range.collapsed;
   1.524 +	} catch (e) {
   1.525 +		return "INVALID_STATE_ERR";
   1.526 +	}
   1.527 +
   1.528 +	// "Let frag be a new DocumentFragment whose ownerDocument is the same as
   1.529 +	// the ownerDocument of the context object's start node."
   1.530 +	var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
   1.531 +		? range.startContainer
   1.532 +		: range.startContainer.ownerDocument;
   1.533 +	var frag = ownerDoc.createDocumentFragment();
   1.534 +
   1.535 +	// "If the context object's start and end are the same, abort this method,
   1.536 +	// returning frag."
   1.537 +	if (range.startContainer == range.endContainer
   1.538 +	&& range.startOffset == range.endOffset) {
   1.539 +		return frag;
   1.540 +	}
   1.541 +
   1.542 +	// "Let original start node, original start offset, original end node, and
   1.543 +	// original end offset be the context object's start and end nodes and
   1.544 +	// offsets, respectively."
   1.545 +	var originalStartNode = range.startContainer;
   1.546 +	var originalStartOffset = range.startOffset;
   1.547 +	var originalEndNode = range.endContainer;
   1.548 +	var originalEndOffset = range.endOffset;
   1.549 +
   1.550 +	// "If original start node and original end node are the same, and they are
   1.551 +	// a Text or Comment node:"
   1.552 +	if (range.startContainer == range.endContainer
   1.553 +	&& (range.startContainer.nodeType == Node.TEXT_NODE
   1.554 +	|| range.startContainer.nodeType == Node.COMMENT_NODE)) {
   1.555 +		// "Let clone be the result of calling cloneNode(false) on original
   1.556 +		// start node."
   1.557 +		var clone = originalStartNode.cloneNode(false);
   1.558 +
   1.559 +		// "Set the data of clone to the result of calling
   1.560 +		// substringData(original start offset, original end offset − original
   1.561 +		// start offset) on original start node."
   1.562 +		clone.data = originalStartNode.substringData(originalStartOffset,
   1.563 +			originalEndOffset - originalStartOffset);
   1.564 +
   1.565 +		// "Append clone as the last child of frag."
   1.566 +		frag.appendChild(clone);
   1.567 +
   1.568 +		// "Call deleteData(original start offset, original end offset −
   1.569 +		// original start offset) on original start node."
   1.570 +		originalStartNode.deleteData(originalStartOffset,
   1.571 +			originalEndOffset - originalStartOffset);
   1.572 +
   1.573 +		// "Abort this method, returning frag."
   1.574 +		return frag;
   1.575 +	}
   1.576 +
   1.577 +	// "Let common ancestor equal original start node."
   1.578 +	var commonAncestor = originalStartNode;
   1.579 +
   1.580 +	// "While common ancestor is not an ancestor container of original end
   1.581 +	// node, set common ancestor to its own parent."
   1.582 +	while (!isAncestorContainer(commonAncestor, originalEndNode)) {
   1.583 +		commonAncestor = commonAncestor.parentNode;
   1.584 +	}
   1.585 +
   1.586 +	// "If original start node is an ancestor container of original end node,
   1.587 +	// let first partially contained child be null."
   1.588 +	var firstPartiallyContainedChild;
   1.589 +	if (isAncestorContainer(originalStartNode, originalEndNode)) {
   1.590 +		firstPartiallyContainedChild = null;
   1.591 +	// "Otherwise, let first partially contained child be the first child of
   1.592 +	// common ancestor that is partially contained in the context object."
   1.593 +	} else {
   1.594 +		for (var i = 0; i < commonAncestor.childNodes.length; i++) {
   1.595 +			if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
   1.596 +				firstPartiallyContainedChild = commonAncestor.childNodes[i];
   1.597 +				break;
   1.598 +			}
   1.599 +		}
   1.600 +		if (!firstPartiallyContainedChild) {
   1.601 +			throw "Spec bug: no first partially contained child!";
   1.602 +		}
   1.603 +	}
   1.604 +
   1.605 +	// "If original end node is an ancestor container of original start node,
   1.606 +	// let last partially contained child be null."
   1.607 +	var lastPartiallyContainedChild;
   1.608 +	if (isAncestorContainer(originalEndNode, originalStartNode)) {
   1.609 +		lastPartiallyContainedChild = null;
   1.610 +	// "Otherwise, let last partially contained child be the last child of
   1.611 +	// common ancestor that is partially contained in the context object."
   1.612 +	} else {
   1.613 +		for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
   1.614 +			if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
   1.615 +				lastPartiallyContainedChild = commonAncestor.childNodes[i];
   1.616 +				break;
   1.617 +			}
   1.618 +		}
   1.619 +		if (!lastPartiallyContainedChild) {
   1.620 +			throw "Spec bug: no last partially contained child!";
   1.621 +		}
   1.622 +	}
   1.623 +
   1.624 +	// "Let contained children be a list of all children of common ancestor
   1.625 +	// that are contained in the context object, in tree order."
   1.626 +	//
   1.627 +	// "If any member of contained children is a DocumentType, raise a
   1.628 +	// HIERARCHY_REQUEST_ERR exception and abort these steps."
   1.629 +	var containedChildren = [];
   1.630 +	for (var i = 0; i < commonAncestor.childNodes.length; i++) {
   1.631 +		if (isContained(commonAncestor.childNodes[i], range)) {
   1.632 +			if (commonAncestor.childNodes[i].nodeType
   1.633 +			== Node.DOCUMENT_TYPE_NODE) {
   1.634 +				return "HIERARCHY_REQUEST_ERR";
   1.635 +			}
   1.636 +			containedChildren.push(commonAncestor.childNodes[i]);
   1.637 +		}
   1.638 +	}
   1.639 +
   1.640 +	// "If original start node is an ancestor container of original end node,
   1.641 +	// set new node to original start node and new offset to original start
   1.642 +	// offset."
   1.643 +	var newNode, newOffset;
   1.644 +	if (isAncestorContainer(originalStartNode, originalEndNode)) {
   1.645 +		newNode = originalStartNode;
   1.646 +		newOffset = originalStartOffset;
   1.647 +	// "Otherwise:"
   1.648 +	} else {
   1.649 +		// "Let reference node equal original start node."
   1.650 +		var referenceNode = originalStartNode;
   1.651 +
   1.652 +		// "While reference node's parent is not null and is not an ancestor
   1.653 +		// container of original end node, set reference node to its parent."
   1.654 +		while (referenceNode.parentNode
   1.655 +		&& !isAncestorContainer(referenceNode.parentNode, originalEndNode)) {
   1.656 +			referenceNode = referenceNode.parentNode;
   1.657 +		}
   1.658 +
   1.659 +		// "Set new node to the parent of reference node, and new offset to one
   1.660 +		// plus the index of reference node."
   1.661 +		newNode = referenceNode.parentNode;
   1.662 +		newOffset = 1 + indexOf(referenceNode);
   1.663 +	}
   1.664 +
   1.665 +	// "If first partially contained child is a Text or Comment node:"
   1.666 +	if (firstPartiallyContainedChild
   1.667 +	&& (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
   1.668 +	|| firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
   1.669 +		// "Let clone be the result of calling cloneNode(false) on original
   1.670 +		// start node."
   1.671 +		var clone = originalStartNode.cloneNode(false);
   1.672 +
   1.673 +		// "Set the data of clone to the result of calling substringData() on
   1.674 +		// original start node, with original start offset as the first
   1.675 +		// argument and (length of original start node − original start offset)
   1.676 +		// as the second."
   1.677 +		clone.data = originalStartNode.substringData(originalStartOffset,
   1.678 +			getNodeLength(originalStartNode) - originalStartOffset);
   1.679 +
   1.680 +		// "Append clone as the last child of frag."
   1.681 +		frag.appendChild(clone);
   1.682 +
   1.683 +		// "Call deleteData() on original start node, with original start
   1.684 +		// offset as the first argument and (length of original start node −
   1.685 +		// original start offset) as the second."
   1.686 +		originalStartNode.deleteData(originalStartOffset,
   1.687 +			getNodeLength(originalStartNode) - originalStartOffset);
   1.688 +	// "Otherwise, if first partially contained child is not null:"
   1.689 +	} else if (firstPartiallyContainedChild) {
   1.690 +		// "Let clone be the result of calling cloneNode(false) on first
   1.691 +		// partially contained child."
   1.692 +		var clone = firstPartiallyContainedChild.cloneNode(false);
   1.693 +
   1.694 +		// "Append clone as the last child of frag."
   1.695 +		frag.appendChild(clone);
   1.696 +
   1.697 +		// "Let subrange be a new Range whose start is (original start node,
   1.698 +		// original start offset) and whose end is (first partially contained
   1.699 +		// child, length of first partially contained child)."
   1.700 +		var subrange = ownerDoc.createRange();
   1.701 +		subrange.setStart(originalStartNode, originalStartOffset);
   1.702 +		subrange.setEnd(firstPartiallyContainedChild,
   1.703 +			getNodeLength(firstPartiallyContainedChild));
   1.704 +
   1.705 +		// "Let subfrag be the result of calling extractContents() on
   1.706 +		// subrange."
   1.707 +		var subfrag = myExtractContents(subrange);
   1.708 +
   1.709 +		// "For each child of subfrag, in order, append that child to clone as
   1.710 +		// its last child."
   1.711 +		for (var i = 0; i < subfrag.childNodes.length; i++) {
   1.712 +			clone.appendChild(subfrag.childNodes[i]);
   1.713 +		}
   1.714 +	}
   1.715 +
   1.716 +	// "For each contained child in contained children, append contained child
   1.717 +	// as the last child of frag."
   1.718 +	for (var i = 0; i < containedChildren.length; i++) {
   1.719 +		frag.appendChild(containedChildren[i]);
   1.720 +	}
   1.721 +
   1.722 +	// "If last partially contained child is a Text or Comment node:"
   1.723 +	if (lastPartiallyContainedChild
   1.724 +	&& (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
   1.725 +	|| lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
   1.726 +		// "Let clone be the result of calling cloneNode(false) on original
   1.727 +		// end node."
   1.728 +		var clone = originalEndNode.cloneNode(false);
   1.729 +
   1.730 +		// "Set the data of clone to the result of calling substringData(0,
   1.731 +		// original end offset) on original end node."
   1.732 +		clone.data = originalEndNode.substringData(0, originalEndOffset);
   1.733 +
   1.734 +		// "Append clone as the last child of frag."
   1.735 +		frag.appendChild(clone);
   1.736 +
   1.737 +		// "Call deleteData(0, original end offset) on original end node."
   1.738 +		originalEndNode.deleteData(0, originalEndOffset);
   1.739 +	// "Otherwise, if last partially contained child is not null:"
   1.740 +	} else if (lastPartiallyContainedChild) {
   1.741 +		// "Let clone be the result of calling cloneNode(false) on last
   1.742 +		// partially contained child."
   1.743 +		var clone = lastPartiallyContainedChild.cloneNode(false);
   1.744 +
   1.745 +		// "Append clone as the last child of frag."
   1.746 +		frag.appendChild(clone);
   1.747 +
   1.748 +		// "Let subrange be a new Range whose start is (last partially
   1.749 +		// contained child, 0) and whose end is (original end node, original
   1.750 +		// end offset)."
   1.751 +		var subrange = ownerDoc.createRange();
   1.752 +		subrange.setStart(lastPartiallyContainedChild, 0);
   1.753 +		subrange.setEnd(originalEndNode, originalEndOffset);
   1.754 +
   1.755 +		// "Let subfrag be the result of calling extractContents() on
   1.756 +		// subrange."
   1.757 +		var subfrag = myExtractContents(subrange);
   1.758 +
   1.759 +		// "For each child of subfrag, in order, append that child to clone as
   1.760 +		// its last child."
   1.761 +		for (var i = 0; i < subfrag.childNodes.length; i++) {
   1.762 +			clone.appendChild(subfrag.childNodes[i]);
   1.763 +		}
   1.764 +	}
   1.765 +
   1.766 +	// "Set the context object's start and end to (new node, new offset)."
   1.767 +	range.setStart(newNode, newOffset);
   1.768 +	range.setEnd(newNode, newOffset);
   1.769 +
   1.770 +	// "Return frag."
   1.771 +	return frag;
   1.772 +}
   1.773 +
   1.774 +/**
   1.775 + * insertNode() implementation, following the spec.  If an exception is
   1.776 + * supposed to be thrown, will return a string with the name (e.g.,
   1.777 + * "HIERARCHY_REQUEST_ERR") instead of a document fragment.  It might also
   1.778 + * return an arbitrary human-readable string if a condition is hit that implies
   1.779 + * a spec bug.
   1.780 + */
   1.781 +function myInsertNode(range, newNode) {
   1.782 +	// "If the context object's detached flag is set, raise an
   1.783 +	// INVALID_STATE_ERR exception and abort these steps."
   1.784 +	//
   1.785 +	// Assume that if accessing collapsed throws, it's detached.
   1.786 +	try {
   1.787 +		range.collapsed;
   1.788 +	} catch (e) {
   1.789 +		return "INVALID_STATE_ERR";
   1.790 +	}
   1.791 +
   1.792 +	// "If the context object's start node is a Text or Comment node and its
   1.793 +	// parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these
   1.794 +	// steps."
   1.795 +	if ((range.startContainer.nodeType == Node.TEXT_NODE
   1.796 +	|| range.startContainer.nodeType == Node.COMMENT_NODE)
   1.797 +	&& !range.startContainer.parentNode) {
   1.798 +		return "HIERARCHY_REQUEST_ERR";
   1.799 +	}
   1.800 +
   1.801 +	// "If the context object's start node is a Text node, run splitText() on
   1.802 +	// it with the context object's start offset as its argument, and let
   1.803 +	// reference node be the result."
   1.804 +	var referenceNode;
   1.805 +	if (range.startContainer.nodeType == Node.TEXT_NODE) {
   1.806 +		// We aren't testing how ranges vary under mutations, and browsers vary
   1.807 +		// in how they mutate for splitText, so let's just force the correct
   1.808 +		// way.
   1.809 +		var start = [range.startContainer, range.startOffset];
   1.810 +		var end = [range.endContainer, range.endOffset];
   1.811 +
   1.812 +		referenceNode = range.startContainer.splitText(range.startOffset);
   1.813 +
   1.814 +		if (start[0] == end[0]
   1.815 +		&& end[1] > start[1]) {
   1.816 +			end[0] = referenceNode;
   1.817 +			end[1] -= start[1];
   1.818 +		} else if (end[0] == start[0].parentNode
   1.819 +		&& end[1] > indexOf(referenceNode)) {
   1.820 +			end[1]++;
   1.821 +		}
   1.822 +		range.setStart(start[0], start[1]);
   1.823 +		range.setEnd(end[0], end[1]);
   1.824 +	// "Otherwise, if the context object's start node is a Comment, let
   1.825 +	// reference node be the context object's start node."
   1.826 +	} else if (range.startContainer.nodeType == Node.COMMENT_NODE) {
   1.827 +		referenceNode = range.startContainer;
   1.828 +	// "Otherwise, let reference node be the child of the context object's
   1.829 +	// start node with index equal to the context object's start offset, or
   1.830 +	// null if there is no such child."
   1.831 +	} else {
   1.832 +		referenceNode = range.startContainer.childNodes[range.startOffset];
   1.833 +		if (typeof referenceNode == "undefined") {
   1.834 +			referenceNode = null;
   1.835 +		}
   1.836 +	}
   1.837 +
   1.838 +	// "If reference node is null, let parent node be the context object's
   1.839 +	// start node."
   1.840 +	var parentNode;
   1.841 +	if (!referenceNode) {
   1.842 +		parentNode = range.startContainer;
   1.843 +	// "Otherwise, let parent node be the parent of reference node."
   1.844 +	} else {
   1.845 +		parentNode = referenceNode.parentNode;
   1.846 +	}
   1.847 +
   1.848 +	// "Call insertBefore(newNode, reference node) on parent node, re-raising
   1.849 +	// any exceptions that call raised."
   1.850 +	try {
   1.851 +		parentNode.insertBefore(newNode, referenceNode);
   1.852 +	} catch (e) {
   1.853 +		return getDomExceptionName(e);
   1.854 +	}
   1.855 +}
   1.856 +
   1.857 +/**
   1.858 + * Asserts that two nodes are equal, in the sense of isEqualNode().  If they
   1.859 + * aren't, tries to print a relatively informative reason why not.  TODO: Move
   1.860 + * this to testharness.js?
   1.861 + */
   1.862 +function assertNodesEqual(actual, expected, msg) {
   1.863 +	if (!actual.isEqualNode(expected)) {
   1.864 +		msg = "Actual and expected mismatch for " + msg + ".  ";
   1.865 +
   1.866 +		while (actual && expected) {
   1.867 +			assert_true(actual.nodeType === expected.nodeType
   1.868 +				&& actual.nodeName === expected.nodeName
   1.869 +				&& actual.nodeValue === expected.nodeValue
   1.870 +				&& actual.childNodes.length === expected.childNodes.length,
   1.871 +				"First differing node: expected " + format_value(expected)
   1.872 +				+ ", got " + format_value(actual));
   1.873 +			actual = nextNode(actual);
   1.874 +			expected = nextNode(expected);
   1.875 +		}
   1.876 +
   1.877 +		assert_unreached("DOMs were not equal but we couldn't figure out why");
   1.878 +	}
   1.879 +}
   1.880 +
   1.881 +/**
   1.882 + * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR").  In
   1.883 + * theory this should be just e.name, but in practice it's not.  So I could
   1.884 + * legitimately just return e.name, but then every engine but WebKit would fail
   1.885 + * every test, since no one seems to care much for standardizing DOMExceptions.
   1.886 + * Instead I mangle it to account for browser bugs, so as not to fail
   1.887 + * insertNode() tests (for instance) for insertBefore() bugs.  Of course, a
   1.888 + * standards-compliant browser will work right in any event.
   1.889 + *
   1.890 + * If the exception has no string property called "name" or "message", we just
   1.891 + * re-throw it.
   1.892 + */
   1.893 +function getDomExceptionName(e) {
   1.894 +	if (typeof e.name == "string"
   1.895 +	&& /^[A-Z_]+_ERR$/.test(e.name)) {
   1.896 +		// Either following the standard, or prefixing NS_ERROR_DOM (I'm
   1.897 +		// looking at you, Gecko).
   1.898 +		return e.name.replace(/^NS_ERROR_DOM_/, "");
   1.899 +	}
   1.900 +
   1.901 +	if (typeof e.message == "string"
   1.902 +	&& /^[A-Z_]+_ERR$/.test(e.message)) {
   1.903 +		// Opera
   1.904 +		return e.message;
   1.905 +	}
   1.906 +
   1.907 +	if (typeof e.message == "string"
   1.908 +	&& /^DOM Exception:/.test(e.message)) {
   1.909 +		// IE
   1.910 +		return /[A-Z_]+_ERR/.exec(e.message)[0];
   1.911 +	}
   1.912 +
   1.913 +	throw e;
   1.914 +}
   1.915 +
   1.916 +/**
   1.917 + * Given an array of endpoint data [start container, start offset, end
   1.918 + * container, end offset], returns a Range with those endpoints.
   1.919 + */
   1.920 +function rangeFromEndpoints(endpoints) {
   1.921 +	// If we just use document instead of the ownerDocument of endpoints[0],
   1.922 +	// WebKit will throw on setStart/setEnd.  This is a WebKit bug, but it's in
   1.923 +	// range, not selection, so we don't want to fail anything for it.
   1.924 +	var range = ownerDocument(endpoints[0]).createRange();
   1.925 +	range.setStart(endpoints[0], endpoints[1]);
   1.926 +	range.setEnd(endpoints[2], endpoints[3]);
   1.927 +	return range;
   1.928 +}
   1.929 +
   1.930 +/**
   1.931 + * Given an array of endpoint data [start container, start offset, end
   1.932 + * container, end offset], sets the selection to have those endpoints.  Uses
   1.933 + * addRange, so the range will be forwards.  Accepts an empty array for
   1.934 + * endpoints, in which case the selection will just be emptied.
   1.935 + */
   1.936 +function setSelectionForwards(endpoints) {
   1.937 +	selection.removeAllRanges();
   1.938 +	if (endpoints.length) {
   1.939 +		selection.addRange(rangeFromEndpoints(endpoints));
   1.940 +	}
   1.941 +}
   1.942 +
   1.943 +/**
   1.944 + * Given an array of endpoint data [start container, start offset, end
   1.945 + * container, end offset], sets the selection to have those endpoints, with the
   1.946 + * direction backwards.  Uses extend, so it will throw in IE.  Accepts an empty
   1.947 + * array for endpoints, in which case the selection will just be emptied.
   1.948 + */
   1.949 +function setSelectionBackwards(endpoints) {
   1.950 +	selection.removeAllRanges();
   1.951 +	if (endpoints.length) {
   1.952 +		selection.collapse(endpoints[2], endpoints[3]);
   1.953 +		selection.extend(endpoints[0], endpoints[1]);
   1.954 +	}
   1.955 +}

mercurial