common.js (39001B)
1 "use strict"; 2 // TODO: iframes, contenteditable/designMode 3 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; 16 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>Äb̈c̈d̈ëf̈g̈ḧ\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"); 34 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); 42 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); 55 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("בן חיים אליעזר"); 72 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(); 78 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); 87 88 comment = document.createComment("Alphabet soup?"); 89 testDiv.appendChild(comment); 90 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); 95 96 doctype = document.doctype; 97 foreignDoctype = foreignDoc.doctype; 98 99 // Chromium project has a limitation of text file size, and it is applied to 100 // test result documents too. Generating tests with testRanges or 101 // testPoints can exceed the limitation easily. Some tests were split into 102 // multiple files such as addRange-NN.html. If you add more ranges, points, 103 // or tests, a Chromium project member might split affected tests. 104 // 105 // In selection/, a rough estimation of the limit is 4,000 test() functions 106 // per a file. 107 testRanges = [ 108 // Various ranges within the text node children of different 109 // paragraphs. All should be valid. 110 "[paras[0].firstChild, 0, paras[0].firstChild, 0]", 111 "[paras[0].firstChild, 0, paras[0].firstChild, 1]", 112 "[paras[0].firstChild, 2, paras[0].firstChild, 8]", 113 "[paras[0].firstChild, 2, paras[0].firstChild, 9]", 114 "[paras[1].firstChild, 0, paras[1].firstChild, 0]", 115 "[paras[1].firstChild, 0, paras[1].firstChild, 1]", 116 "[paras[1].firstChild, 2, paras[1].firstChild, 8]", 117 "[paras[1].firstChild, 2, paras[1].firstChild, 9]", 118 "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]", 119 "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]", 120 "[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]", 121 "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]", 122 "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]", 123 "[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]", 124 // Now try testing some elements, not just text nodes. 125 "[document.documentElement, 0, document.documentElement, 1]", 126 "[document.documentElement, 0, document.documentElement, 2]", 127 "[document.documentElement, 1, document.documentElement, 2]", 128 "[document.head, 1, document.head, 1]", 129 "[document.body, 0, document.body, 1]", 130 "[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]", 131 "[foreignDoc.head, 1, foreignDoc.head, 1]", 132 "[foreignDoc.body, 0, foreignDoc.body, 0]", 133 "[paras[0], 0, paras[0], 0]", 134 "[paras[0], 0, paras[0], 1]", 135 "[detachedPara1, 0, detachedPara1, 0]", 136 "[detachedPara1, 0, detachedPara1, 1]", 137 // Now try some ranges that span elements. 138 "[paras[0].firstChild, 0, paras[1].firstChild, 0]", 139 "[paras[0].firstChild, 0, paras[1].firstChild, 8]", 140 "[paras[0].firstChild, 3, paras[3], 1]", 141 // How about something that spans a node and its descendant? 142 "[paras[0], 0, paras[0].firstChild, 7]", 143 "[testDiv, 2, paras[4], 1]", 144 "[testDiv, 1, paras[2].firstChild, 5]", 145 "[document.documentElement, 1, document.body, 0]", 146 "[foreignDoc.documentElement, 1, foreignDoc.body, 0]", 147 // Then a few more interesting things just for good measure. 148 "[document, 0, document, 1]", 149 "[document, 0, document, 2]", 150 "[document, 1, document, 2]", 151 "[testDiv, 0, comment, 5]", 152 "[paras[2].firstChild, 4, comment, 2]", 153 "[paras[3], 1, comment, 8]", 154 "[foreignDoc, 0, foreignDoc, 0]", 155 "[foreignDoc, 1, foreignComment, 2]", 156 "[foreignDoc.body, 0, foreignTextNode, 36]", 157 "[xmlDoc, 0, xmlDoc, 0]", 158 // Opera 11 crashes if you extractContents() a range that ends at offset 159 // zero in a comment. Comment out this line to run the tests successfully. 160 "[xmlDoc, 1, xmlComment, 0]", 161 "[detachedTextNode, 0, detachedTextNode, 8]", 162 "[detachedForeignTextNode, 7, detachedForeignTextNode, 7]", 163 "[detachedForeignTextNode, 0, detachedForeignTextNode, 8]", 164 "[detachedXmlTextNode, 7, detachedXmlTextNode, 7]", 165 "[detachedXmlTextNode, 0, detachedXmlTextNode, 8]", 166 "[detachedComment, 3, detachedComment, 4]", 167 "[detachedComment, 5, detachedComment, 5]", 168 "[detachedForeignComment, 0, detachedForeignComment, 1]", 169 "[detachedForeignComment, 4, detachedForeignComment, 4]", 170 "[detachedXmlComment, 2, detachedXmlComment, 6]", 171 "[docfrag, 0, docfrag, 0]", 172 "[foreignDocfrag, 0, foreignDocfrag, 0]", 173 "[xmlDocfrag, 0, xmlDocfrag, 0]", 174 ]; 175 176 testPoints = [ 177 // Various positions within the page, some invalid. Remember that 178 // paras[0] is visible, and paras[1] is display: none. 179 "[paras[0].firstChild, -1]", 180 "[paras[0].firstChild, 0]", 181 "[paras[0].firstChild, 1]", 182 "[paras[0].firstChild, 2]", 183 "[paras[0].firstChild, 8]", 184 "[paras[0].firstChild, 9]", 185 "[paras[0].firstChild, 10]", 186 "[paras[0].firstChild, 65535]", 187 "[paras[1].firstChild, -1]", 188 "[paras[1].firstChild, 0]", 189 "[paras[1].firstChild, 1]", 190 "[paras[1].firstChild, 2]", 191 "[paras[1].firstChild, 8]", 192 "[paras[1].firstChild, 9]", 193 "[paras[1].firstChild, 10]", 194 "[paras[1].firstChild, 65535]", 195 "[detachedPara1.firstChild, 0]", 196 "[detachedPara1.firstChild, 1]", 197 "[detachedPara1.firstChild, 8]", 198 "[detachedPara1.firstChild, 9]", 199 "[foreignPara1.firstChild, 0]", 200 "[foreignPara1.firstChild, 1]", 201 "[foreignPara1.firstChild, 8]", 202 "[foreignPara1.firstChild, 9]", 203 // Now try testing some elements, not just text nodes. 204 "[document.documentElement, -1]", 205 "[document.documentElement, 0]", 206 "[document.documentElement, 1]", 207 "[document.documentElement, 2]", 208 "[document.documentElement, 7]", 209 "[document.head, 1]", 210 "[document.body, 3]", 211 "[foreignDoc.documentElement, 0]", 212 "[foreignDoc.documentElement, 1]", 213 "[foreignDoc.head, 0]", 214 "[foreignDoc.body, 1]", 215 "[paras[0], 0]", 216 "[paras[0], 1]", 217 "[paras[0], 2]", 218 "[paras[1], 0]", 219 "[paras[1], 1]", 220 "[paras[1], 2]", 221 "[detachedPara1, 0]", 222 "[detachedPara1, 1]", 223 "[testDiv, 0]", 224 "[testDiv, 3]", 225 // Then a few more interesting things just for good measure. 226 "[document, -1]", 227 "[document, 0]", 228 "[document, 1]", 229 "[document, 2]", 230 "[document, 3]", 231 "[comment, -1]", 232 "[comment, 0]", 233 "[comment, 4]", 234 "[comment, 96]", 235 "[foreignDoc, 0]", 236 "[foreignDoc, 1]", 237 "[foreignComment, 2]", 238 "[foreignTextNode, 0]", 239 "[foreignTextNode, 36]", 240 "[xmlDoc, -1]", 241 "[xmlDoc, 0]", 242 "[xmlDoc, 1]", 243 "[xmlDoc, 5]", 244 "[xmlComment, 0]", 245 "[xmlComment, 4]", 246 "[processingInstruction, 0]", 247 "[processingInstruction, 5]", 248 "[processingInstruction, 9]", 249 "[detachedTextNode, 0]", 250 "[detachedTextNode, 8]", 251 "[detachedForeignTextNode, 0]", 252 "[detachedForeignTextNode, 8]", 253 "[detachedXmlTextNode, 0]", 254 "[detachedXmlTextNode, 8]", 255 "[detachedProcessingInstruction, 12]", 256 "[detachedComment, 3]", 257 "[detachedComment, 5]", 258 "[detachedForeignComment, 0]", 259 "[detachedForeignComment, 4]", 260 "[detachedXmlComment, 2]", 261 "[docfrag, 0]", 262 "[foreignDocfrag, 0]", 263 "[xmlDocfrag, 0]", 264 "[doctype, 0]", 265 "[doctype, -17]", 266 "[doctype, 1]", 267 "[foreignDoctype, 0]", 268 "[xmlDoctype, 0]", 269 ]; 270 271 testNodes = [ 272 "paras[0]", 273 "paras[0].firstChild", 274 "paras[1]", 275 "paras[1].firstChild", 276 "foreignPara1", 277 "foreignPara1.firstChild", 278 "detachedPara1", 279 "detachedPara1.firstChild", 280 "detachedPara1", 281 "detachedPara1.firstChild", 282 "testDiv", 283 "document", 284 "detachedDiv", 285 "detachedPara2", 286 "foreignDoc", 287 "foreignPara2", 288 "xmlDoc", 289 "xmlElement", 290 "detachedXmlElement", 291 "detachedTextNode", 292 "foreignTextNode", 293 "detachedForeignTextNode", 294 "xmlTextNode", 295 "detachedXmlTextNode", 296 "processingInstruction", 297 "detachedProcessingInstruction", 298 "comment", 299 "detachedComment", 300 "foreignComment", 301 "detachedForeignComment", 302 "xmlComment", 303 "detachedXmlComment", 304 "docfrag", 305 "foreignDocfrag", 306 "xmlDocfrag", 307 "doctype", 308 "foreignDoctype", 309 "xmlDoctype", 310 ]; 311 } 312 if ("setup" in window) { 313 setup(setupRangeTests); 314 } else { 315 // Presumably we're running from within an iframe or something 316 setupRangeTests(); 317 } 318 319 /** 320 * Return the length of a node as specified in DOM Range. 321 */ 322 function getNodeLength(node) { 323 if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { 324 return 0; 325 } 326 if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) { 327 return node.length; 328 } 329 return node.childNodes.length; 330 } 331 332 /** 333 * Returns the furthest ancestor of a Node as defined by the spec. 334 */ 335 function furthestAncestor(node) { 336 var root = node; 337 while (root.parentNode != null) { 338 root = root.parentNode; 339 } 340 return root; 341 } 342 343 /** 344 * "The ancestor containers of a Node are the Node itself and all its 345 * ancestors." 346 * 347 * Is node1 an ancestor container of node2? 348 */ 349 function isAncestorContainer(node1, node2) { 350 return node1 == node2 || 351 (node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS); 352 } 353 354 /** 355 * Returns the first Node that's after node in tree order, or null if node is 356 * the last Node. 357 */ 358 function nextNode(node) { 359 if (node.hasChildNodes()) { 360 return node.firstChild; 361 } 362 return nextNodeDescendants(node); 363 } 364 365 /** 366 * Returns the last Node that's before node in tree order, or null if node is 367 * the first Node. 368 */ 369 function previousNode(node) { 370 if (node.previousSibling) { 371 node = node.previousSibling; 372 while (node.hasChildNodes()) { 373 node = node.lastChild; 374 } 375 return node; 376 } 377 return node.parentNode; 378 } 379 380 /** 381 * Returns the next Node that's after node and all its descendants in tree 382 * order, or null if node is the last Node or an ancestor of it. 383 */ 384 function nextNodeDescendants(node) { 385 while (node && !node.nextSibling) { 386 node = node.parentNode; 387 } 388 if (!node) { 389 return null; 390 } 391 return node.nextSibling; 392 } 393 394 /** 395 * Returns the ownerDocument of the Node, or the Node itself if it's a 396 * Document. 397 */ 398 function ownerDocument(node) { 399 return node.nodeType == Node.DOCUMENT_NODE 400 ? node 401 : node.ownerDocument; 402 } 403 404 /** 405 * Returns true if ancestor is an ancestor of descendant, false otherwise. 406 */ 407 function isAncestor(ancestor, descendant) { 408 if (!ancestor || !descendant) { 409 return false; 410 } 411 while (descendant && descendant != ancestor) { 412 descendant = descendant.parentNode; 413 } 414 return descendant == ancestor; 415 } 416 417 /** 418 * Returns true if descendant is a descendant of ancestor, false otherwise. 419 */ 420 function isDescendant(descendant, ancestor) { 421 return isAncestor(ancestor, descendant); 422 } 423 424 /** 425 * The position of two boundary points relative to one another, as defined by 426 * the spec. 427 */ 428 function getPosition(nodeA, offsetA, nodeB, offsetB) { 429 // "If node A is the same as node B, return equal if offset A equals offset 430 // B, before if offset A is less than offset B, and after if offset A is 431 // greater than offset B." 432 if (nodeA == nodeB) { 433 if (offsetA == offsetB) { 434 return "equal"; 435 } 436 if (offsetA < offsetB) { 437 return "before"; 438 } 439 if (offsetA > offsetB) { 440 return "after"; 441 } 442 } 443 444 // "If node A is after node B in tree order, compute the position of (node 445 // B, offset B) relative to (node A, offset A). If it is before, return 446 // after. If it is after, return before." 447 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) { 448 var pos = getPosition(nodeB, offsetB, nodeA, offsetA); 449 if (pos == "before") { 450 return "after"; 451 } 452 if (pos == "after") { 453 return "before"; 454 } 455 } 456 457 // "If node A is an ancestor of node B:" 458 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) { 459 // "Let child equal node B." 460 var child = nodeB; 461 462 // "While child is not a child of node A, set child to its parent." 463 while (child.parentNode != nodeA) { 464 child = child.parentNode; 465 } 466 467 // "If the index of child is less than offset A, return after." 468 if (indexOf(child) < offsetA) { 469 return "after"; 470 } 471 } 472 473 // "Return before." 474 return "before"; 475 } 476 477 /** 478 * "contained" as defined by DOM Range: "A Node node is contained in a range 479 * range if node's furthest ancestor is the same as range's root, and (node, 0) 480 * is after range's start, and (node, length of node) is before range's end." 481 */ 482 function isContained(node, range) { 483 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); 484 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); 485 486 return furthestAncestor(node) == furthestAncestor(range.startContainer) 487 && pos1 == "after" 488 && pos2 == "before"; 489 } 490 491 /** 492 * "partially contained" as defined by DOM Range: "A Node is partially 493 * contained in a range if it is an ancestor container of the range's start but 494 * not its end, or vice versa." 495 */ 496 function isPartiallyContained(node, range) { 497 var cond1 = isAncestorContainer(node, range.startContainer); 498 var cond2 = isAncestorContainer(node, range.endContainer); 499 return (cond1 && !cond2) || (cond2 && !cond1); 500 } 501 502 /** 503 * Index of a node as defined by the spec. 504 */ 505 function indexOf(node) { 506 if (!node.parentNode) { 507 // No preceding sibling nodes, right? 508 return 0; 509 } 510 var i = 0; 511 while (node != node.parentNode.childNodes[i]) { 512 i++; 513 } 514 return i; 515 } 516 517 /** 518 * extractContents() implementation, following the spec. If an exception is 519 * supposed to be thrown, will return a string with the name (e.g., 520 * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also 521 * return an arbitrary human-readable string if a condition is hit that implies 522 * a spec bug. 523 */ 524 function myExtractContents(range) { 525 // "If the context object's detached flag is set, raise an 526 // INVALID_STATE_ERR exception and abort these steps." 527 try { 528 range.collapsed; 529 } catch (e) { 530 return "INVALID_STATE_ERR"; 531 } 532 533 // "Let frag be a new DocumentFragment whose ownerDocument is the same as 534 // the ownerDocument of the context object's start node." 535 var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE 536 ? range.startContainer 537 : range.startContainer.ownerDocument; 538 var frag = ownerDoc.createDocumentFragment(); 539 540 // "If the context object's start and end are the same, abort this method, 541 // returning frag." 542 if (range.startContainer == range.endContainer 543 && range.startOffset == range.endOffset) { 544 return frag; 545 } 546 547 // "Let original start node, original start offset, original end node, and 548 // original end offset be the context object's start and end nodes and 549 // offsets, respectively." 550 var originalStartNode = range.startContainer; 551 var originalStartOffset = range.startOffset; 552 var originalEndNode = range.endContainer; 553 var originalEndOffset = range.endOffset; 554 555 // "If original start node and original end node are the same, and they are 556 // a Text or Comment node:" 557 if (range.startContainer == range.endContainer 558 && (range.startContainer.nodeType == Node.TEXT_NODE 559 || range.startContainer.nodeType == Node.COMMENT_NODE)) { 560 // "Let clone be the result of calling cloneNode(false) on original 561 // start node." 562 var clone = originalStartNode.cloneNode(false); 563 564 // "Set the data of clone to the result of calling 565 // substringData(original start offset, original end offset − original 566 // start offset) on original start node." 567 clone.data = originalStartNode.substringData(originalStartOffset, 568 originalEndOffset - originalStartOffset); 569 570 // "Append clone as the last child of frag." 571 frag.appendChild(clone); 572 573 // "Call deleteData(original start offset, original end offset − 574 // original start offset) on original start node." 575 originalStartNode.deleteData(originalStartOffset, 576 originalEndOffset - originalStartOffset); 577 578 // "Abort this method, returning frag." 579 return frag; 580 } 581 582 // "Let common ancestor equal original start node." 583 var commonAncestor = originalStartNode; 584 585 // "While common ancestor is not an ancestor container of original end 586 // node, set common ancestor to its own parent." 587 while (!isAncestorContainer(commonAncestor, originalEndNode)) { 588 commonAncestor = commonAncestor.parentNode; 589 } 590 591 // "If original start node is an ancestor container of original end node, 592 // let first partially contained child be null." 593 var firstPartiallyContainedChild; 594 if (isAncestorContainer(originalStartNode, originalEndNode)) { 595 firstPartiallyContainedChild = null; 596 // "Otherwise, let first partially contained child be the first child of 597 // common ancestor that is partially contained in the context object." 598 } else { 599 for (var i = 0; i < commonAncestor.childNodes.length; i++) { 600 if (isPartiallyContained(commonAncestor.childNodes[i], range)) { 601 firstPartiallyContainedChild = commonAncestor.childNodes[i]; 602 break; 603 } 604 } 605 if (!firstPartiallyContainedChild) { 606 throw "Spec bug: no first partially contained child!"; 607 } 608 } 609 610 // "If original end node is an ancestor container of original start node, 611 // let last partially contained child be null." 612 var lastPartiallyContainedChild; 613 if (isAncestorContainer(originalEndNode, originalStartNode)) { 614 lastPartiallyContainedChild = null; 615 // "Otherwise, let last partially contained child be the last child of 616 // common ancestor that is partially contained in the context object." 617 } else { 618 for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) { 619 if (isPartiallyContained(commonAncestor.childNodes[i], range)) { 620 lastPartiallyContainedChild = commonAncestor.childNodes[i]; 621 break; 622 } 623 } 624 if (!lastPartiallyContainedChild) { 625 throw "Spec bug: no last partially contained child!"; 626 } 627 } 628 629 // "Let contained children be a list of all children of common ancestor 630 // that are contained in the context object, in tree order." 631 // 632 // "If any member of contained children is a DocumentType, raise a 633 // HIERARCHY_REQUEST_ERR exception and abort these steps." 634 var containedChildren = []; 635 for (var i = 0; i < commonAncestor.childNodes.length; i++) { 636 if (isContained(commonAncestor.childNodes[i], range)) { 637 if (commonAncestor.childNodes[i].nodeType 638 == Node.DOCUMENT_TYPE_NODE) { 639 return "HIERARCHY_REQUEST_ERR"; 640 } 641 containedChildren.push(commonAncestor.childNodes[i]); 642 } 643 } 644 645 // "If original start node is an ancestor container of original end node, 646 // set new node to original start node and new offset to original start 647 // offset." 648 var newNode, newOffset; 649 if (isAncestorContainer(originalStartNode, originalEndNode)) { 650 newNode = originalStartNode; 651 newOffset = originalStartOffset; 652 // "Otherwise:" 653 } else { 654 // "Let reference node equal original start node." 655 var referenceNode = originalStartNode; 656 657 // "While reference node's parent is not null and is not an ancestor 658 // container of original end node, set reference node to its parent." 659 while (referenceNode.parentNode 660 && !isAncestorContainer(referenceNode.parentNode, originalEndNode)) { 661 referenceNode = referenceNode.parentNode; 662 } 663 664 // "Set new node to the parent of reference node, and new offset to one 665 // plus the index of reference node." 666 newNode = referenceNode.parentNode; 667 newOffset = 1 + indexOf(referenceNode); 668 } 669 670 // "If first partially contained child is a Text or Comment node:" 671 if (firstPartiallyContainedChild 672 && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE 673 || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) { 674 // "Let clone be the result of calling cloneNode(false) on original 675 // start node." 676 var clone = originalStartNode.cloneNode(false); 677 678 // "Set the data of clone to the result of calling substringData() on 679 // original start node, with original start offset as the first 680 // argument and (length of original start node − original start offset) 681 // as the second." 682 clone.data = originalStartNode.substringData(originalStartOffset, 683 getNodeLength(originalStartNode) - originalStartOffset); 684 685 // "Append clone as the last child of frag." 686 frag.appendChild(clone); 687 688 // "Call deleteData() on original start node, with original start 689 // offset as the first argument and (length of original start node − 690 // original start offset) as the second." 691 originalStartNode.deleteData(originalStartOffset, 692 getNodeLength(originalStartNode) - originalStartOffset); 693 // "Otherwise, if first partially contained child is not null:" 694 } else if (firstPartiallyContainedChild) { 695 // "Let clone be the result of calling cloneNode(false) on first 696 // partially contained child." 697 var clone = firstPartiallyContainedChild.cloneNode(false); 698 699 // "Append clone as the last child of frag." 700 frag.appendChild(clone); 701 702 // "Let subrange be a new Range whose start is (original start node, 703 // original start offset) and whose end is (first partially contained 704 // child, length of first partially contained child)." 705 var subrange = ownerDoc.createRange(); 706 subrange.setStart(originalStartNode, originalStartOffset); 707 subrange.setEnd(firstPartiallyContainedChild, 708 getNodeLength(firstPartiallyContainedChild)); 709 710 // "Let subfrag be the result of calling extractContents() on 711 // subrange." 712 var subfrag = myExtractContents(subrange); 713 714 // "For each child of subfrag, in order, append that child to clone as 715 // its last child." 716 for (var i = 0; i < subfrag.childNodes.length; i++) { 717 clone.appendChild(subfrag.childNodes[i]); 718 } 719 } 720 721 // "For each contained child in contained children, append contained child 722 // as the last child of frag." 723 for (var i = 0; i < containedChildren.length; i++) { 724 frag.appendChild(containedChildren[i]); 725 } 726 727 // "If last partially contained child is a Text or Comment node:" 728 if (lastPartiallyContainedChild 729 && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE 730 || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) { 731 // "Let clone be the result of calling cloneNode(false) on original 732 // end node." 733 var clone = originalEndNode.cloneNode(false); 734 735 // "Set the data of clone to the result of calling substringData(0, 736 // original end offset) on original end node." 737 clone.data = originalEndNode.substringData(0, originalEndOffset); 738 739 // "Append clone as the last child of frag." 740 frag.appendChild(clone); 741 742 // "Call deleteData(0, original end offset) on original end node." 743 originalEndNode.deleteData(0, originalEndOffset); 744 // "Otherwise, if last partially contained child is not null:" 745 } else if (lastPartiallyContainedChild) { 746 // "Let clone be the result of calling cloneNode(false) on last 747 // partially contained child." 748 var clone = lastPartiallyContainedChild.cloneNode(false); 749 750 // "Append clone as the last child of frag." 751 frag.appendChild(clone); 752 753 // "Let subrange be a new Range whose start is (last partially 754 // contained child, 0) and whose end is (original end node, original 755 // end offset)." 756 var subrange = ownerDoc.createRange(); 757 subrange.setStart(lastPartiallyContainedChild, 0); 758 subrange.setEnd(originalEndNode, originalEndOffset); 759 760 // "Let subfrag be the result of calling extractContents() on 761 // subrange." 762 var subfrag = myExtractContents(subrange); 763 764 // "For each child of subfrag, in order, append that child to clone as 765 // its last child." 766 for (var i = 0; i < subfrag.childNodes.length; i++) { 767 clone.appendChild(subfrag.childNodes[i]); 768 } 769 } 770 771 // "Set the context object's start and end to (new node, new offset)." 772 range.setStart(newNode, newOffset); 773 range.setEnd(newNode, newOffset); 774 775 // "Return frag." 776 return frag; 777 } 778 779 /** 780 * insertNode() implementation, following the spec. If an exception is 781 * supposed to be thrown, will return a string with the name (e.g., 782 * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also 783 * return an arbitrary human-readable string if a condition is hit that implies 784 * a spec bug. 785 */ 786 function myInsertNode(range, newNode) { 787 // "If the context object's detached flag is set, raise an 788 // INVALID_STATE_ERR exception and abort these steps." 789 // 790 // Assume that if accessing collapsed throws, it's detached. 791 try { 792 range.collapsed; 793 } catch (e) { 794 return "INVALID_STATE_ERR"; 795 } 796 797 // "If the context object's start node is a Text or Comment node and its 798 // parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these 799 // steps." 800 if ((range.startContainer.nodeType == Node.TEXT_NODE 801 || range.startContainer.nodeType == Node.COMMENT_NODE) 802 && !range.startContainer.parentNode) { 803 return "HIERARCHY_REQUEST_ERR"; 804 } 805 806 // "If the context object's start node is a Text node, run splitText() on 807 // it with the context object's start offset as its argument, and let 808 // reference node be the result." 809 var referenceNode; 810 if (range.startContainer.nodeType == Node.TEXT_NODE) { 811 // We aren't testing how ranges vary under mutations, and browsers vary 812 // in how they mutate for splitText, so let's just force the correct 813 // way. 814 var start = [range.startContainer, range.startOffset]; 815 var end = [range.endContainer, range.endOffset]; 816 817 referenceNode = range.startContainer.splitText(range.startOffset); 818 819 if (start[0] == end[0] 820 && end[1] > start[1]) { 821 end[0] = referenceNode; 822 end[1] -= start[1]; 823 } else if (end[0] == start[0].parentNode 824 && end[1] > indexOf(referenceNode)) { 825 end[1]++; 826 } 827 range.setStart(start[0], start[1]); 828 range.setEnd(end[0], end[1]); 829 // "Otherwise, if the context object's start node is a Comment, let 830 // reference node be the context object's start node." 831 } else if (range.startContainer.nodeType == Node.COMMENT_NODE) { 832 referenceNode = range.startContainer; 833 // "Otherwise, let reference node be the child of the context object's 834 // start node with index equal to the context object's start offset, or 835 // null if there is no such child." 836 } else { 837 referenceNode = range.startContainer.childNodes[range.startOffset]; 838 if (typeof referenceNode == "undefined") { 839 referenceNode = null; 840 } 841 } 842 843 // "If reference node is null, let parent node be the context object's 844 // start node." 845 var parentNode; 846 if (!referenceNode) { 847 parentNode = range.startContainer; 848 // "Otherwise, let parent node be the parent of reference node." 849 } else { 850 parentNode = referenceNode.parentNode; 851 } 852 853 // "Call insertBefore(newNode, reference node) on parent node, re-raising 854 // any exceptions that call raised." 855 try { 856 parentNode.insertBefore(newNode, referenceNode); 857 } catch (e) { 858 return getDomExceptionName(e); 859 } 860 } 861 862 /** 863 * Asserts that two nodes are equal, in the sense of isEqualNode(). If they 864 * aren't, tries to print a relatively informative reason why not. TODO: Move 865 * this to testharness.js? 866 */ 867 function assertNodesEqual(actual, expected, msg) { 868 if (!actual.isEqualNode(expected)) { 869 msg = "Actual and expected mismatch for " + msg + ". "; 870 871 while (actual && expected) { 872 assert_true(actual.nodeType === expected.nodeType 873 && actual.nodeName === expected.nodeName 874 && actual.nodeValue === expected.nodeValue 875 && actual.childNodes.length === expected.childNodes.length, 876 "First differing node: expected " + format_value(expected) 877 + ", got " + format_value(actual)); 878 actual = nextNode(actual); 879 expected = nextNode(expected); 880 } 881 882 assert_unreached("DOMs were not equal but we couldn't figure out why"); 883 } 884 } 885 886 /** 887 * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR"). In 888 * theory this should be just e.name, but in practice it's not. So I could 889 * legitimately just return e.name, but then every engine but WebKit would fail 890 * every test, since no one seems to care much for standardizing DOMExceptions. 891 * Instead I mangle it to account for browser bugs, so as not to fail 892 * insertNode() tests (for instance) for insertBefore() bugs. Of course, a 893 * standards-compliant browser will work right in any event. 894 * 895 * If the exception has no string property called "name" or "message", we just 896 * re-throw it. 897 */ 898 function getDomExceptionName(e) { 899 if (typeof e.name == "string" 900 && /^[A-Z_]+_ERR$/.test(e.name)) { 901 // Either following the standard, or prefixing NS_ERROR_DOM (I'm 902 // looking at you, Gecko). 903 return e.name.replace(/^NS_ERROR_DOM_/, ""); 904 } 905 906 if (typeof e.message == "string" 907 && /^[A-Z_]+_ERR$/.test(e.message)) { 908 // Opera 909 return e.message; 910 } 911 912 if (typeof e.message == "string" 913 && /^DOM Exception:/.test(e.message)) { 914 // IE 915 return /[A-Z_]+_ERR/.exec(e.message)[0]; 916 } 917 918 throw e; 919 } 920 921 /** 922 * Given an array of endpoint data [start container, start offset, end 923 * container, end offset], returns a Range with those endpoints. 924 */ 925 function rangeFromEndpoints(endpoints) { 926 // If we just use document instead of the ownerDocument of endpoints[0], 927 // WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in 928 // range, not selection, so we don't want to fail anything for it. 929 var range = ownerDocument(endpoints[0]).createRange(); 930 range.setStart(endpoints[0], endpoints[1]); 931 range.setEnd(endpoints[2], endpoints[3]); 932 return range; 933 } 934 935 /** 936 * Given an array of endpoint data [start container, start offset, end 937 * container, end offset], sets the selection to have those endpoints. Uses 938 * addRange, so the range will be forwards. Accepts an empty array for 939 * endpoints, in which case the selection will just be emptied. 940 */ 941 function setSelectionForwards(endpoints) { 942 selection.removeAllRanges(); 943 if (endpoints.length) { 944 selection.addRange(rangeFromEndpoints(endpoints)); 945 } 946 } 947 948 /** 949 * Given an array of endpoint data [start container, start offset, end 950 * container, end offset], sets the selection to have those endpoints, with the 951 * direction backwards. Uses extend, so it will throw in IE. Accepts an empty 952 * array for endpoints, in which case the selection will just be emptied. 953 */ 954 function setSelectionBackwards(endpoints) { 955 selection.removeAllRanges(); 956 if (endpoints.length) { 957 selection.collapse(endpoints[2], endpoints[3]); 958 selection.extend(endpoints[0], endpoints[1]); 959 } 960 } 961 962 /** 963 * Verify that the specified func doesn't change the selection. 964 * This function should be used in testharness tests. 965 */ 966 function assertSelectionNoChange(func) { 967 var originalCount = selection.rangeCount; 968 var originalRange = originalCount == 0 ? null : selection.getRangeAt(0); 969 var originalAnchorNode = selection.anchorNode; 970 var originalAnchorOffset = selection.anchorOffset; 971 var originalFocusNode = selection.focusNode; 972 var originalFocusOffset = selection.focusOffset; 973 974 func(); 975 976 assert_equals(selection.rangeCount, originalCount, 977 "The operation should not add Range"); 978 assert_equals(selection.anchorNode, originalAnchorNode, 979 "The operation should not update anchorNode"); 980 assert_equals(selection.anchorOffset, originalAnchorOffset, 981 "The operation should not update anchorOffset"); 982 assert_equals(selection.focusNode, originalFocusNode, 983 "The operation should not update focusNode"); 984 assert_equals(selection.focusOffset, originalFocusOffset, 985 "The operation should not update focusOffset"); 986 if (originalCount < 1) 987 return; 988 assert_equals(selection.getRangeAt(0), originalRange, 989 "The operation should not replace a registered Range"); 990 } 991 992 /** 993 * Check if the specified node can be selectable with window.getSelection() 994 * methods. 995 */ 996 function isSelectableNode(node) { 997 if (!node) 998 return false; 999 if (node.nodeType == Node.DOCUMENT_TYPE_NODE) 1000 return false; 1001 return document.contains(node); 1002 }