| OLD | NEW |
| (Empty) | |
| 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", "abcd
e", "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("whippoor
will", "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 yo
u know that ":syn sync fromstart" is very useful when using vim to edit large am
ounts 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" mea
n 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 ab
out 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 successful
ly. |
| 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_INST
RUCTION_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 } |
| OLD | NEW |