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>Äb̈c̈d̈ëf̈g̈ḧ\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 +}