OLD | NEW |
| (Empty) |
1 "use strict"; | |
2 // Written by Aryeh Gregor <ayg@aryeh.name> | |
3 | |
4 // TODO: iframes, contenteditable/designMode | |
5 | |
6 // Everything is done in functions in this test harness, so we have to declare | |
7 // all the variables before use to make sure they can be reused. | |
8 var testDiv, paras, detachedDiv, detachedPara1, detachedPara2, | |
9 foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement, | |
10 detachedXmlElement, detachedTextNode, foreignTextNode, | |
11 detachedForeignTextNode, xmlTextNode, detachedXmlTextNode, | |
12 processingInstruction, detachedProcessingInstruction, comment, | |
13 detachedComment, foreignComment, detachedForeignComment, xmlComment, | |
14 detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype, | |
15 foreignDoctype, xmlDoctype; | |
16 var testRangesShort, testRanges, testPoints, testNodesShort, testNodes; | |
17 | |
18 function setupRangeTests() { | |
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 | |
27 paras = []; | |
28 paras.push(document.createElement("p")); | |
29 paras[0].setAttribute("id", "a"); | |
30 // Test some diacritics, to make sure browsers are using code units here | |
31 // and not something like grapheme clusters. | |
32 paras[0].textContent = "A\u0308b\u0308c\u0308d\u0308e\u0308f\u0308g\u0308h\u
0308\n"; | |
33 testDiv.appendChild(paras[0]); | |
34 | |
35 paras.push(document.createElement("p")); | |
36 paras[1].setAttribute("id", "b"); | |
37 paras[1].setAttribute("style", "display:none"); | |
38 paras[1].textContent = "Ijklmnop\n"; | |
39 testDiv.appendChild(paras[1]); | |
40 | |
41 paras.push(document.createElement("p")); | |
42 paras[2].setAttribute("id", "c"); | |
43 paras[2].textContent = "Qrstuvwx"; | |
44 testDiv.appendChild(paras[2]); | |
45 | |
46 paras.push(document.createElement("p")); | |
47 paras[3].setAttribute("id", "d"); | |
48 paras[3].setAttribute("style", "display:none"); | |
49 paras[3].textContent = "Yzabcdef"; | |
50 testDiv.appendChild(paras[3]); | |
51 | |
52 paras.push(document.createElement("p")); | |
53 paras[4].setAttribute("id", "e"); | |
54 paras[4].setAttribute("style", "display:none"); | |
55 paras[4].textContent = "Ghijklmn"; | |
56 testDiv.appendChild(paras[4]); | |
57 | |
58 detachedDiv = document.createElement("div"); | |
59 detachedPara1 = document.createElement("p"); | |
60 detachedPara1.appendChild(document.createTextNode("Opqrstuv")); | |
61 detachedPara2 = document.createElement("p"); | |
62 detachedPara2.appendChild(document.createTextNode("Wxyzabcd")); | |
63 detachedDiv.appendChild(detachedPara1); | |
64 detachedDiv.appendChild(detachedPara2); | |
65 | |
66 // Opera doesn't automatically create a doctype for a new HTML document, | |
67 // contrary to spec. It also doesn't let you add doctypes to documents | |
68 // after the fact through any means I've tried. So foreignDoc in Opera | |
69 // will have no doctype, foreignDoctype will be null, and Opera will fail | |
70 // some tests somewhat mysteriously as a result. | |
71 foreignDoc = document.implementation.createHTMLDocument(""); | |
72 foreignPara1 = foreignDoc.createElement("p"); | |
73 foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl")); | |
74 foreignPara2 = foreignDoc.createElement("p"); | |
75 foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst")); | |
76 foreignDoc.body.appendChild(foreignPara1); | |
77 foreignDoc.body.appendChild(foreignPara2); | |
78 | |
79 // Now we get to do really silly stuff, which nobody in the universe is | |
80 // ever going to actually do, but the spec defines behavior, so too bad. | |
81 // Testing is fun! | |
82 xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcd
e", "x\"'y"); | |
83 xmlDoc = document.implementation.createDocument(null, null, xmlDoctype); | |
84 detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element
-names"); | |
85 detachedTextNode = document.createTextNode("Uvwxyzab"); | |
86 detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij"); | |
87 detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr"); | |
88 // PIs only exist in XML documents, so don't bother with document or | |
89 // foreignDoc. | |
90 detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoor
will", "chirp chirp chirp"); | |
91 detachedComment = document.createComment("Stuvwxyz"); | |
92 // Hurrah, we finally got to "z" at the end! | |
93 detachedForeignComment = foreignDoc.createComment("אריה יהודה"); | |
94 detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר"); | |
95 | |
96 // We should also test with document fragments that actually contain stuff | |
97 // . . . but, maybe later. | |
98 docfrag = document.createDocumentFragment(); | |
99 foreignDocfrag = foreignDoc.createDocumentFragment(); | |
100 xmlDocfrag = xmlDoc.createDocumentFragment(); | |
101 | |
102 xmlElement = xmlDoc.createElement("igiveuponcreativenames"); | |
103 xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti"); | |
104 xmlElement.appendChild(xmlTextNode); | |
105 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?'); | |
106 xmlDoc.appendChild(xmlElement); | |
107 xmlDoc.appendChild(processingInstruction); | |
108 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"); | |
109 xmlDoc.appendChild(xmlComment); | |
110 | |
111 comment = document.createComment("Alphabet soup?"); | |
112 testDiv.appendChild(comment); | |
113 | |
114 foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mea
n different things. I\'ve seen non-native speakers trip up on this.'); | |
115 foreignDoc.appendChild(foreignComment); | |
116 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
."); | |
117 foreignDoc.body.appendChild(foreignTextNode); | |
118 | |
119 doctype = document.doctype; | |
120 foreignDoctype = foreignDoc.doctype; | |
121 | |
122 testRangesShort = [ | |
123 // Various ranges within the text node children of different | |
124 // paragraphs. All should be valid. | |
125 "[paras[0].firstChild, 0, paras[0].firstChild, 0]", | |
126 "[paras[0].firstChild, 0, paras[0].firstChild, 1]", | |
127 "[paras[0].firstChild, 2, paras[0].firstChild, 8]", | |
128 "[paras[0].firstChild, 2, paras[0].firstChild, 9]", | |
129 "[paras[1].firstChild, 0, paras[1].firstChild, 0]", | |
130 "[paras[1].firstChild, 2, paras[1].firstChild, 9]", | |
131 "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]", | |
132 "[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]", | |
133 "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]", | |
134 "[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]", | |
135 // Now try testing some elements, not just text nodes. | |
136 "[document.documentElement, 0, document.documentElement, 1]", | |
137 "[document.documentElement, 0, document.documentElement, 2]", | |
138 "[document.documentElement, 1, document.documentElement, 2]", | |
139 "[document.head, 1, document.head, 1]", | |
140 "[document.body, 4, document.body, 5]", | |
141 "[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]", | |
142 "[paras[0], 0, paras[0], 1]", | |
143 "[detachedPara1, 0, detachedPara1, 1]", | |
144 // Now try some ranges that span elements. | |
145 "[paras[0].firstChild, 0, paras[1].firstChild, 0]", | |
146 "[paras[0].firstChild, 0, paras[1].firstChild, 8]", | |
147 "[paras[0].firstChild, 3, paras[3], 1]", | |
148 // How about something that spans a node and its descendant? | |
149 "[paras[0], 0, paras[0].firstChild, 7]", | |
150 "[testDiv, 2, paras[4], 1]", | |
151 // Then a few more interesting things just for good measure. | |
152 "[document, 0, document, 1]", | |
153 "[document, 0, document, 2]", | |
154 "[comment, 2, comment, 3]", | |
155 "[testDiv, 0, comment, 5]", | |
156 "[foreignDoc, 1, foreignComment, 2]", | |
157 "[foreignDoc.body, 0, foreignTextNode, 36]", | |
158 "[xmlDoc, 1, xmlComment, 0]", | |
159 "[detachedTextNode, 0, detachedTextNode, 8]", | |
160 "[detachedForeignTextNode, 0, detachedForeignTextNode, 8]", | |
161 "[detachedXmlTextNode, 0, detachedXmlTextNode, 8]", | |
162 "[detachedComment, 3, detachedComment, 4]", | |
163 "[detachedForeignComment, 0, detachedForeignComment, 1]", | |
164 "[detachedXmlComment, 2, detachedXmlComment, 6]", | |
165 "[docfrag, 0, docfrag, 0]", | |
166 "[processingInstruction, 0, processingInstruction, 4]", | |
167 ]; | |
168 | |
169 testRanges = testRangesShort.concat([ | |
170 "[paras[1].firstChild, 0, paras[1].firstChild, 1]", | |
171 "[paras[1].firstChild, 2, paras[1].firstChild, 8]", | |
172 "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]", | |
173 "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]", | |
174 "[foreignDoc.head, 1, foreignDoc.head, 1]", | |
175 "[foreignDoc.body, 0, foreignDoc.body, 0]", | |
176 "[paras[0], 0, paras[0], 0]", | |
177 "[detachedPara1, 0, detachedPara1, 0]", | |
178 "[testDiv, 1, paras[2].firstChild, 5]", | |
179 "[document.documentElement, 1, document.body, 0]", | |
180 "[foreignDoc.documentElement, 1, foreignDoc.body, 0]", | |
181 "[document, 1, document, 2]", | |
182 "[paras[2].firstChild, 4, comment, 2]", | |
183 "[paras[3], 1, comment, 8]", | |
184 "[foreignDoc, 0, foreignDoc, 0]", | |
185 "[xmlDoc, 0, xmlDoc, 0]", | |
186 "[detachedForeignTextNode, 7, detachedForeignTextNode, 7]", | |
187 "[detachedXmlTextNode, 7, detachedXmlTextNode, 7]", | |
188 "[detachedComment, 5, detachedComment, 5]", | |
189 "[detachedForeignComment, 4, detachedForeignComment, 4]", | |
190 "[foreignDocfrag, 0, foreignDocfrag, 0]", | |
191 "[xmlDocfrag, 0, xmlDocfrag, 0]", | |
192 ]); | |
193 | |
194 testPoints = [ | |
195 // Various positions within the page, some invalid. Remember that | |
196 // paras[0] is visible, and paras[1] is display: none. | |
197 "[paras[0].firstChild, -1]", | |
198 "[paras[0].firstChild, 0]", | |
199 "[paras[0].firstChild, 1]", | |
200 "[paras[0].firstChild, 2]", | |
201 "[paras[0].firstChild, 8]", | |
202 "[paras[0].firstChild, 9]", | |
203 "[paras[0].firstChild, 10]", | |
204 "[paras[0].firstChild, 65535]", | |
205 "[paras[1].firstChild, -1]", | |
206 "[paras[1].firstChild, 0]", | |
207 "[paras[1].firstChild, 1]", | |
208 "[paras[1].firstChild, 2]", | |
209 "[paras[1].firstChild, 8]", | |
210 "[paras[1].firstChild, 9]", | |
211 "[paras[1].firstChild, 10]", | |
212 "[paras[1].firstChild, 65535]", | |
213 "[detachedPara1.firstChild, 0]", | |
214 "[detachedPara1.firstChild, 1]", | |
215 "[detachedPara1.firstChild, 8]", | |
216 "[detachedPara1.firstChild, 9]", | |
217 "[foreignPara1.firstChild, 0]", | |
218 "[foreignPara1.firstChild, 1]", | |
219 "[foreignPara1.firstChild, 8]", | |
220 "[foreignPara1.firstChild, 9]", | |
221 // Now try testing some elements, not just text nodes. | |
222 "[document.documentElement, -1]", | |
223 "[document.documentElement, 0]", | |
224 "[document.documentElement, 1]", | |
225 "[document.documentElement, 2]", | |
226 "[document.documentElement, 7]", | |
227 "[document.head, 1]", | |
228 "[document.body, 3]", | |
229 "[foreignDoc.documentElement, 0]", | |
230 "[foreignDoc.documentElement, 1]", | |
231 "[foreignDoc.head, 0]", | |
232 "[foreignDoc.body, 1]", | |
233 "[paras[0], 0]", | |
234 "[paras[0], 1]", | |
235 "[paras[0], 2]", | |
236 "[paras[1], 0]", | |
237 "[paras[1], 1]", | |
238 "[paras[1], 2]", | |
239 "[detachedPara1, 0]", | |
240 "[detachedPara1, 1]", | |
241 "[testDiv, 0]", | |
242 "[testDiv, 3]", | |
243 // Then a few more interesting things just for good measure. | |
244 "[document, -1]", | |
245 "[document, 0]", | |
246 "[document, 1]", | |
247 "[document, 2]", | |
248 "[document, 3]", | |
249 "[comment, -1]", | |
250 "[comment, 0]", | |
251 "[comment, 4]", | |
252 "[comment, 96]", | |
253 "[foreignDoc, 0]", | |
254 "[foreignDoc, 1]", | |
255 "[foreignComment, 2]", | |
256 "[foreignTextNode, 0]", | |
257 "[foreignTextNode, 36]", | |
258 "[xmlDoc, -1]", | |
259 "[xmlDoc, 0]", | |
260 "[xmlDoc, 1]", | |
261 "[xmlDoc, 5]", | |
262 "[xmlComment, 0]", | |
263 "[xmlComment, 4]", | |
264 "[processingInstruction, 0]", | |
265 "[processingInstruction, 5]", | |
266 "[processingInstruction, 9]", | |
267 "[detachedTextNode, 0]", | |
268 "[detachedTextNode, 8]", | |
269 "[detachedForeignTextNode, 0]", | |
270 "[detachedForeignTextNode, 8]", | |
271 "[detachedXmlTextNode, 0]", | |
272 "[detachedXmlTextNode, 8]", | |
273 "[detachedProcessingInstruction, 12]", | |
274 "[detachedComment, 3]", | |
275 "[detachedComment, 5]", | |
276 "[detachedForeignComment, 0]", | |
277 "[detachedForeignComment, 4]", | |
278 "[detachedXmlComment, 2]", | |
279 "[docfrag, 0]", | |
280 "[foreignDocfrag, 0]", | |
281 "[xmlDocfrag, 0]", | |
282 "[doctype, 0]", | |
283 "[doctype, -17]", | |
284 "[doctype, 1]", | |
285 "[foreignDoctype, 0]", | |
286 "[xmlDoctype, 0]", | |
287 ]; | |
288 | |
289 testNodesShort = [ | |
290 "paras[0]", | |
291 "paras[0].firstChild", | |
292 "paras[1].firstChild", | |
293 "foreignPara1", | |
294 "foreignPara1.firstChild", | |
295 "detachedPara1", | |
296 "detachedPara1.firstChild", | |
297 "document", | |
298 "detachedDiv", | |
299 "foreignDoc", | |
300 "foreignPara2", | |
301 "xmlDoc", | |
302 "xmlElement", | |
303 "detachedTextNode", | |
304 "foreignTextNode", | |
305 "processingInstruction", | |
306 "detachedProcessingInstruction", | |
307 "comment", | |
308 "detachedComment", | |
309 "docfrag", | |
310 "doctype", | |
311 "foreignDoctype", | |
312 ]; | |
313 | |
314 testNodes = testNodesShort.concat([ | |
315 "paras[1]", | |
316 "detachedPara2", | |
317 "detachedPara2.firstChild", | |
318 "testDiv", | |
319 "detachedXmlElement", | |
320 "detachedForeignTextNode", | |
321 "xmlTextNode", | |
322 "detachedXmlTextNode", | |
323 "xmlComment", | |
324 "foreignComment", | |
325 "detachedForeignComment", | |
326 "detachedXmlComment", | |
327 "foreignDocfrag", | |
328 "xmlDocfrag", | |
329 "xmlDoctype", | |
330 ]); | |
331 } | |
332 if ("setup" in window) { | |
333 setup(setupRangeTests); | |
334 } else { | |
335 // Presumably we're running from within an iframe or something | |
336 setupRangeTests(); | |
337 } | |
338 | |
339 /** | |
340 * The "length" of a node as defined by the Ranges section of DOM4. | |
341 */ | |
342 function nodeLength(node) { | |
343 // "The length of a node node depends on node: | |
344 // | |
345 // "DocumentType | |
346 // "0." | |
347 if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { | |
348 return 0; | |
349 } | |
350 // "Text | |
351 // "ProcessingInstruction | |
352 // "Comment | |
353 // "Its length attribute value." | |
354 // Browsers don't historically support the length attribute on | |
355 // ProcessingInstruction, so to avoid spurious failures, do | |
356 // node.data.length instead of node.length. | |
357 if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INST
RUCTION_NODE || node.nodeType == Node.COMMENT_NODE) { | |
358 return node.data.length; | |
359 } | |
360 // "Any other node | |
361 // "Its number of children." | |
362 return node.childNodes.length; | |
363 } | |
364 | |
365 /** | |
366 * Returns the furthest ancestor of a Node as defined by the spec. | |
367 */ | |
368 function furthestAncestor(node) { | |
369 var root = node; | |
370 while (root.parentNode != null) { | |
371 root = root.parentNode; | |
372 } | |
373 return root; | |
374 } | |
375 | |
376 /** | |
377 * "The ancestor containers of a Node are the Node itself and all its | |
378 * ancestors." | |
379 * | |
380 * Is node1 an ancestor container of node2? | |
381 */ | |
382 function isAncestorContainer(node1, node2) { | |
383 return node1 == node2 || | |
384 (node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS)
; | |
385 } | |
386 | |
387 /** | |
388 * Returns the first Node that's after node in tree order, or null if node is | |
389 * the last Node. | |
390 */ | |
391 function nextNode(node) { | |
392 if (node.hasChildNodes()) { | |
393 return node.firstChild; | |
394 } | |
395 return nextNodeDescendants(node); | |
396 } | |
397 | |
398 /** | |
399 * Returns the last Node that's before node in tree order, or null if node is | |
400 * the first Node. | |
401 */ | |
402 function previousNode(node) { | |
403 if (node.previousSibling) { | |
404 node = node.previousSibling; | |
405 while (node.hasChildNodes()) { | |
406 node = node.lastChild; | |
407 } | |
408 return node; | |
409 } | |
410 return node.parentNode; | |
411 } | |
412 | |
413 /** | |
414 * Returns the next Node that's after node and all its descendants in tree | |
415 * order, or null if node is the last Node or an ancestor of it. | |
416 */ | |
417 function nextNodeDescendants(node) { | |
418 while (node && !node.nextSibling) { | |
419 node = node.parentNode; | |
420 } | |
421 if (!node) { | |
422 return null; | |
423 } | |
424 return node.nextSibling; | |
425 } | |
426 | |
427 /** | |
428 * Returns the ownerDocument of the Node, or the Node itself if it's a | |
429 * Document. | |
430 */ | |
431 function ownerDocument(node) { | |
432 return node.nodeType == Node.DOCUMENT_NODE | |
433 ? node | |
434 : node.ownerDocument; | |
435 } | |
436 | |
437 /** | |
438 * Returns true if ancestor is an ancestor of descendant, false otherwise. | |
439 */ | |
440 function isAncestor(ancestor, descendant) { | |
441 if (!ancestor || !descendant) { | |
442 return false; | |
443 } | |
444 while (descendant && descendant != ancestor) { | |
445 descendant = descendant.parentNode; | |
446 } | |
447 return descendant == ancestor; | |
448 } | |
449 | |
450 /** | |
451 * Returns true if ancestor is an inclusive ancestor of descendant, false | |
452 * otherwise. | |
453 */ | |
454 function isInclusiveAncestor(ancestor, descendant) { | |
455 return ancestor === descendant || isAncestor(ancestor, descendant); | |
456 } | |
457 | |
458 /** | |
459 * Returns true if descendant is a descendant of ancestor, false otherwise. | |
460 */ | |
461 function isDescendant(descendant, ancestor) { | |
462 return isAncestor(ancestor, descendant); | |
463 } | |
464 | |
465 /** | |
466 * Returns true if descendant is an inclusive descendant of ancestor, false | |
467 * otherwise. | |
468 */ | |
469 function isInclusiveDescendant(descendant, ancestor) { | |
470 return descendant === ancestor || isDescendant(descendant, ancestor); | |
471 } | |
472 | |
473 /** | |
474 * The position of two boundary points relative to one another, as defined by | |
475 * the spec. | |
476 */ | |
477 function getPosition(nodeA, offsetA, nodeB, offsetB) { | |
478 // "If node A is the same as node B, return equal if offset A equals offset | |
479 // B, before if offset A is less than offset B, and after if offset A is | |
480 // greater than offset B." | |
481 if (nodeA == nodeB) { | |
482 if (offsetA == offsetB) { | |
483 return "equal"; | |
484 } | |
485 if (offsetA < offsetB) { | |
486 return "before"; | |
487 } | |
488 if (offsetA > offsetB) { | |
489 return "after"; | |
490 } | |
491 } | |
492 | |
493 // "If node A is after node B in tree order, compute the position of (node | |
494 // B, offset B) relative to (node A, offset A). If it is before, return | |
495 // after. If it is after, return before." | |
496 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING)
{ | |
497 var pos = getPosition(nodeB, offsetB, nodeA, offsetA); | |
498 if (pos == "before") { | |
499 return "after"; | |
500 } | |
501 if (pos == "after") { | |
502 return "before"; | |
503 } | |
504 } | |
505 | |
506 // "If node A is an ancestor of node B:" | |
507 if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS)
{ | |
508 // "Let child equal node B." | |
509 var child = nodeB; | |
510 | |
511 // "While child is not a child of node A, set child to its parent." | |
512 while (child.parentNode != nodeA) { | |
513 child = child.parentNode; | |
514 } | |
515 | |
516 // "If the index of child is less than offset A, return after." | |
517 if (indexOf(child) < offsetA) { | |
518 return "after"; | |
519 } | |
520 } | |
521 | |
522 // "Return before." | |
523 return "before"; | |
524 } | |
525 | |
526 /** | |
527 * "contained" as defined by DOM Range: "A Node node is contained in a range | |
528 * range if node's furthest ancestor is the same as range's root, and (node, 0) | |
529 * is after range's start, and (node, length of node) is before range's end." | |
530 */ | |
531 function isContained(node, range) { | |
532 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); | |
533 var pos2 = getPosition(node, nodeLength(node), range.endContainer, range.end
Offset); | |
534 | |
535 return furthestAncestor(node) == furthestAncestor(range.startContainer) | |
536 && pos1 == "after" | |
537 && pos2 == "before"; | |
538 } | |
539 | |
540 /** | |
541 * "partially contained" as defined by DOM Range: "A Node is partially | |
542 * contained in a range if it is an ancestor container of the range's start but | |
543 * not its end, or vice versa." | |
544 */ | |
545 function isPartiallyContained(node, range) { | |
546 var cond1 = isAncestorContainer(node, range.startContainer); | |
547 var cond2 = isAncestorContainer(node, range.endContainer); | |
548 return (cond1 && !cond2) || (cond2 && !cond1); | |
549 } | |
550 | |
551 /** | |
552 * Index of a node as defined by the spec. | |
553 */ | |
554 function indexOf(node) { | |
555 if (!node.parentNode) { | |
556 // No preceding sibling nodes, right? | |
557 return 0; | |
558 } | |
559 var i = 0; | |
560 while (node != node.parentNode.childNodes[i]) { | |
561 i++; | |
562 } | |
563 return i; | |
564 } | |
565 | |
566 /** | |
567 * extractContents() implementation, following the spec. If an exception is | |
568 * supposed to be thrown, will return a string with the name (e.g., | |
569 * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also | |
570 * return an arbitrary human-readable string if a condition is hit that implies | |
571 * a spec bug. | |
572 */ | |
573 function myExtractContents(range) { | |
574 // "Let frag be a new DocumentFragment whose ownerDocument is the same as | |
575 // the ownerDocument of the context object's start node." | |
576 var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE | |
577 ? range.startContainer | |
578 : range.startContainer.ownerDocument; | |
579 var frag = ownerDoc.createDocumentFragment(); | |
580 | |
581 // "If the context object's start and end are the same, abort this method, | |
582 // returning frag." | |
583 if (range.startContainer == range.endContainer | |
584 && range.startOffset == range.endOffset) { | |
585 return frag; | |
586 } | |
587 | |
588 // "Let original start node, original start offset, original end node, and | |
589 // original end offset be the context object's start and end nodes and | |
590 // offsets, respectively." | |
591 var originalStartNode = range.startContainer; | |
592 var originalStartOffset = range.startOffset; | |
593 var originalEndNode = range.endContainer; | |
594 var originalEndOffset = range.endOffset; | |
595 | |
596 // "If original start node is original end node, and they are a Text, | |
597 // ProcessingInstruction, or Comment node:" | |
598 if (range.startContainer == range.endContainer | |
599 && (range.startContainer.nodeType == Node.TEXT_NODE | |
600 || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE | |
601 || range.startContainer.nodeType == Node.COMMENT_NODE)) { | |
602 // "Let clone be the result of calling cloneNode(false) on original | |
603 // start node." | |
604 var clone = originalStartNode.cloneNode(false); | |
605 | |
606 // "Set the data of clone to the result of calling | |
607 // substringData(original start offset, original end offset − original | |
608 // start offset) on original start node." | |
609 clone.data = originalStartNode.substringData(originalStartOffset, | |
610 originalEndOffset - originalStartOffset); | |
611 | |
612 // "Append clone as the last child of frag." | |
613 frag.appendChild(clone); | |
614 | |
615 // "Call deleteData(original start offset, original end offset − | |
616 // original start offset) on original start node." | |
617 originalStartNode.deleteData(originalStartOffset, | |
618 originalEndOffset - originalStartOffset); | |
619 | |
620 // "Abort this method, returning frag." | |
621 return frag; | |
622 } | |
623 | |
624 // "Let common ancestor equal original start node." | |
625 var commonAncestor = originalStartNode; | |
626 | |
627 // "While common ancestor is not an ancestor container of original end | |
628 // node, set common ancestor to its own parent." | |
629 while (!isAncestorContainer(commonAncestor, originalEndNode)) { | |
630 commonAncestor = commonAncestor.parentNode; | |
631 } | |
632 | |
633 // "If original start node is an ancestor container of original end node, | |
634 // let first partially contained child be null." | |
635 var firstPartiallyContainedChild; | |
636 if (isAncestorContainer(originalStartNode, originalEndNode)) { | |
637 firstPartiallyContainedChild = null; | |
638 // "Otherwise, let first partially contained child be the first child of | |
639 // common ancestor that is partially contained in the context object." | |
640 } else { | |
641 for (var i = 0; i < commonAncestor.childNodes.length; i++) { | |
642 if (isPartiallyContained(commonAncestor.childNodes[i], range)) { | |
643 firstPartiallyContainedChild = commonAncestor.childNodes[i]; | |
644 break; | |
645 } | |
646 } | |
647 if (!firstPartiallyContainedChild) { | |
648 throw "Spec bug: no first partially contained child!"; | |
649 } | |
650 } | |
651 | |
652 // "If original end node is an ancestor container of original start node, | |
653 // let last partially contained child be null." | |
654 var lastPartiallyContainedChild; | |
655 if (isAncestorContainer(originalEndNode, originalStartNode)) { | |
656 lastPartiallyContainedChild = null; | |
657 // "Otherwise, let last partially contained child be the last child of | |
658 // common ancestor that is partially contained in the context object." | |
659 } else { | |
660 for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) { | |
661 if (isPartiallyContained(commonAncestor.childNodes[i], range)) { | |
662 lastPartiallyContainedChild = commonAncestor.childNodes[i]; | |
663 break; | |
664 } | |
665 } | |
666 if (!lastPartiallyContainedChild) { | |
667 throw "Spec bug: no last partially contained child!"; | |
668 } | |
669 } | |
670 | |
671 // "Let contained children be a list of all children of common ancestor | |
672 // that are contained in the context object, in tree order." | |
673 // | |
674 // "If any member of contained children is a DocumentType, raise a | |
675 // HIERARCHY_REQUEST_ERR exception and abort these steps." | |
676 var containedChildren = []; | |
677 for (var i = 0; i < commonAncestor.childNodes.length; i++) { | |
678 if (isContained(commonAncestor.childNodes[i], range)) { | |
679 if (commonAncestor.childNodes[i].nodeType | |
680 == Node.DOCUMENT_TYPE_NODE) { | |
681 return "HIERARCHY_REQUEST_ERR"; | |
682 } | |
683 containedChildren.push(commonAncestor.childNodes[i]); | |
684 } | |
685 } | |
686 | |
687 // "If original start node is an ancestor container of original end node, | |
688 // set new node to original start node and new offset to original start | |
689 // offset." | |
690 var newNode, newOffset; | |
691 if (isAncestorContainer(originalStartNode, originalEndNode)) { | |
692 newNode = originalStartNode; | |
693 newOffset = originalStartOffset; | |
694 // "Otherwise:" | |
695 } else { | |
696 // "Let reference node equal original start node." | |
697 var referenceNode = originalStartNode; | |
698 | |
699 // "While reference node's parent is not null and is not an ancestor | |
700 // container of original end node, set reference node to its parent." | |
701 while (referenceNode.parentNode | |
702 && !isAncestorContainer(referenceNode.parentNode, originalEndNode)) { | |
703 referenceNode = referenceNode.parentNode; | |
704 } | |
705 | |
706 // "Set new node to the parent of reference node, and new offset to one | |
707 // plus the index of reference node." | |
708 newNode = referenceNode.parentNode; | |
709 newOffset = 1 + indexOf(referenceNode); | |
710 } | |
711 | |
712 // "If first partially contained child is a Text, ProcessingInstruction, or | |
713 // Comment node:" | |
714 if (firstPartiallyContainedChild | |
715 && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE | |
716 || firstPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE | |
717 || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) { | |
718 // "Let clone be the result of calling cloneNode(false) on original | |
719 // start node." | |
720 var clone = originalStartNode.cloneNode(false); | |
721 | |
722 // "Set the data of clone to the result of calling substringData() on | |
723 // original start node, with original start offset as the first | |
724 // argument and (length of original start node − original start offset) | |
725 // as the second." | |
726 clone.data = originalStartNode.substringData(originalStartOffset, | |
727 nodeLength(originalStartNode) - originalStartOffset); | |
728 | |
729 // "Append clone as the last child of frag." | |
730 frag.appendChild(clone); | |
731 | |
732 // "Call deleteData() on original start node, with original start | |
733 // offset as the first argument and (length of original start node − | |
734 // original start offset) as the second." | |
735 originalStartNode.deleteData(originalStartOffset, | |
736 nodeLength(originalStartNode) - originalStartOffset); | |
737 // "Otherwise, if first partially contained child is not null:" | |
738 } else if (firstPartiallyContainedChild) { | |
739 // "Let clone be the result of calling cloneNode(false) on first | |
740 // partially contained child." | |
741 var clone = firstPartiallyContainedChild.cloneNode(false); | |
742 | |
743 // "Append clone as the last child of frag." | |
744 frag.appendChild(clone); | |
745 | |
746 // "Let subrange be a new Range whose start is (original start node, | |
747 // original start offset) and whose end is (first partially contained | |
748 // child, length of first partially contained child)." | |
749 var subrange = ownerDoc.createRange(); | |
750 subrange.setStart(originalStartNode, originalStartOffset); | |
751 subrange.setEnd(firstPartiallyContainedChild, | |
752 nodeLength(firstPartiallyContainedChild)); | |
753 | |
754 // "Let subfrag be the result of calling extractContents() on | |
755 // subrange." | |
756 var subfrag = myExtractContents(subrange); | |
757 | |
758 // "For each child of subfrag, in order, append that child to clone as | |
759 // its last child." | |
760 for (var i = 0; i < subfrag.childNodes.length; i++) { | |
761 clone.appendChild(subfrag.childNodes[i]); | |
762 } | |
763 } | |
764 | |
765 // "For each contained child in contained children, append contained child | |
766 // as the last child of frag." | |
767 for (var i = 0; i < containedChildren.length; i++) { | |
768 frag.appendChild(containedChildren[i]); | |
769 } | |
770 | |
771 // "If last partially contained child is a Text, ProcessingInstruction, or | |
772 // Comment node:" | |
773 if (lastPartiallyContainedChild | |
774 && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE | |
775 || lastPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE | |
776 || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) { | |
777 // "Let clone be the result of calling cloneNode(false) on original | |
778 // end node." | |
779 var clone = originalEndNode.cloneNode(false); | |
780 | |
781 // "Set the data of clone to the result of calling substringData(0, | |
782 // original end offset) on original end node." | |
783 clone.data = originalEndNode.substringData(0, originalEndOffset); | |
784 | |
785 // "Append clone as the last child of frag." | |
786 frag.appendChild(clone); | |
787 | |
788 // "Call deleteData(0, original end offset) on original end node." | |
789 originalEndNode.deleteData(0, originalEndOffset); | |
790 // "Otherwise, if last partially contained child is not null:" | |
791 } else if (lastPartiallyContainedChild) { | |
792 // "Let clone be the result of calling cloneNode(false) on last | |
793 // partially contained child." | |
794 var clone = lastPartiallyContainedChild.cloneNode(false); | |
795 | |
796 // "Append clone as the last child of frag." | |
797 frag.appendChild(clone); | |
798 | |
799 // "Let subrange be a new Range whose start is (last partially | |
800 // contained child, 0) and whose end is (original end node, original | |
801 // end offset)." | |
802 var subrange = ownerDoc.createRange(); | |
803 subrange.setStart(lastPartiallyContainedChild, 0); | |
804 subrange.setEnd(originalEndNode, originalEndOffset); | |
805 | |
806 // "Let subfrag be the result of calling extractContents() on | |
807 // subrange." | |
808 var subfrag = myExtractContents(subrange); | |
809 | |
810 // "For each child of subfrag, in order, append that child to clone as | |
811 // its last child." | |
812 for (var i = 0; i < subfrag.childNodes.length; i++) { | |
813 clone.appendChild(subfrag.childNodes[i]); | |
814 } | |
815 } | |
816 | |
817 // "Set the context object's start and end to (new node, new offset)." | |
818 range.setStart(newNode, newOffset); | |
819 range.setEnd(newNode, newOffset); | |
820 | |
821 // "Return frag." | |
822 return frag; | |
823 } | |
824 | |
825 /** | |
826 * insertNode() implementation, following the spec. If an exception is meant | |
827 * to be thrown, will return a string with the expected exception name, for | |
828 * instance "HIERARCHY_REQUEST_ERR". | |
829 */ | |
830 function myInsertNode(range, node) { | |
831 // "If range's start node is a ProcessingInstruction or Comment node, or is | |
832 // a Text node whose parent is null, or is node, throw an | |
833 // "HierarchyRequestError" exception and terminate these steps." | |
834 if (range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE | |
835 || range.startContainer.nodeType == Node.COMMENT_NODE | |
836 || (range.startContainer.nodeType == Node.TEXT_NODE | |
837 && !range.startContainer.parentNode) | |
838 || range.startContainer == node) { | |
839 return "HIERARCHY_REQUEST_ERR"; | |
840 } | |
841 | |
842 // "Let referenceNode be null." | |
843 var referenceNode = null; | |
844 | |
845 // "If range's start node is a Text node, set referenceNode to that Text nod
e." | |
846 if (range.startContainer.nodeType == Node.TEXT_NODE) { | |
847 referenceNode = range.startContainer; | |
848 | |
849 // "Otherwise, set referenceNode to the child of start node whose index
is | |
850 // start offset, and null if there is no such child." | |
851 } else { | |
852 if (range.startOffset < range.startContainer.childNodes.length) { | |
853 referenceNode = range.startContainer.childNodes[range.startOffset]; | |
854 } else { | |
855 referenceNode = null; | |
856 } | |
857 } | |
858 | |
859 // "Let parent be range's start node if referenceNode is null, and | |
860 // referenceNode's parent otherwise." | |
861 var parent_ = referenceNode === null ? range.startContainer : | |
862 referenceNode.parentNode; | |
863 | |
864 // "Ensure pre-insertion validity of node into parent before | |
865 // referenceNode." | |
866 var error = ensurePreInsertionValidity(node, parent_, referenceNode); | |
867 if (error) { | |
868 return error; | |
869 } | |
870 | |
871 // "If range's start node is a Text node, set referenceNode to the result | |
872 // of splitting it with offset range's start offset." | |
873 if (range.startContainer.nodeType == Node.TEXT_NODE) { | |
874 referenceNode = range.startContainer.splitText(range.startOffset); | |
875 } | |
876 | |
877 // "If node is referenceNode, set referenceNode to its next sibling." | |
878 if (node == referenceNode) { | |
879 referenceNode = referenceNode.nextSibling; | |
880 } | |
881 | |
882 // "If node's parent is not null, remove node from its parent." | |
883 if (node.parentNode) { | |
884 node.parentNode.removeChild(node); | |
885 } | |
886 | |
887 // "Let newOffset be parent's length if referenceNode is null, and | |
888 // referenceNode's index otherwise." | |
889 var newOffset = referenceNode === null ? nodeLength(parent_) : | |
890 indexOf(referenceNode); | |
891 | |
892 // "Increase newOffset by node's length if node is a DocumentFragment node, | |
893 // and one otherwise." | |
894 newOffset += node.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? | |
895 nodeLength(node) : 1; | |
896 | |
897 // "Pre-insert node into parent before referenceNode." | |
898 parent_.insertBefore(node, referenceNode); | |
899 | |
900 // "If range's start and end are the same, set range's end to (parent, | |
901 // newOffset)." | |
902 if (range.startContainer == range.endContainer | |
903 && range.startOffset == range.endOffset) { | |
904 range.setEnd(parent_, newOffset); | |
905 } | |
906 } | |
907 | |
908 // To make filter() calls more readable | |
909 function isElement(node) { | |
910 return node.nodeType == Node.ELEMENT_NODE; | |
911 } | |
912 | |
913 function isText(node) { | |
914 return node.nodeType == Node.TEXT_NODE; | |
915 } | |
916 | |
917 function isDoctype(node) { | |
918 return node.nodeType == Node.DOCUMENT_TYPE_NODE; | |
919 } | |
920 | |
921 function ensurePreInsertionValidity(node, parent_, child) { | |
922 // "If parent is not a Document, DocumentFragment, or Element node, throw a | |
923 // HierarchyRequestError." | |
924 if (parent_.nodeType != Node.DOCUMENT_NODE | |
925 && parent_.nodeType != Node.DOCUMENT_FRAGMENT_NODE | |
926 && parent_.nodeType != Node.ELEMENT_NODE) { | |
927 return "HIERARCHY_REQUEST_ERR"; | |
928 } | |
929 | |
930 // "If node is a host-including inclusive ancestor of parent, throw a | |
931 // HierarchyRequestError." | |
932 // | |
933 // XXX Does not account for host | |
934 if (isInclusiveAncestor(node, parent_)) { | |
935 return "HIERARCHY_REQUEST_ERR"; | |
936 } | |
937 | |
938 // "If child is not null and its parent is not parent, throw a NotFoundError | |
939 // exception." | |
940 if (child && child.parentNode != parent_) { | |
941 return "NOT_FOUND_ERR"; | |
942 } | |
943 | |
944 // "If node is not a DocumentFragment, DocumentType, Element, Text, | |
945 // ProcessingInstruction, or Comment node, throw a HierarchyRequestError." | |
946 if (node.nodeType != Node.DOCUMENT_FRAGMENT_NODE | |
947 && node.nodeType != Node.DOCUMENT_TYPE_NODE | |
948 && node.nodeType != Node.ELEMENT_NODE | |
949 && node.nodeType != Node.TEXT_NODE | |
950 && node.nodeType != Node.PROCESSING_INSTRUCTION_NODE | |
951 && node.nodeType != Node.COMMENT_NODE) { | |
952 return "HIERARCHY_REQUEST_ERR"; | |
953 } | |
954 | |
955 // "If either node is a Text node and parent is a document, or node is a | |
956 // doctype and parent is not a document, throw a HierarchyRequestError." | |
957 if ((node.nodeType == Node.TEXT_NODE | |
958 && parent_.nodeType == Node.DOCUMENT_NODE) | |
959 || (node.nodeType == Node.DOCUMENT_TYPE_NODE | |
960 && parent_.nodeType != Node.DOCUMENT_NODE)) { | |
961 return "HIERARCHY_REQUEST_ERR"; | |
962 } | |
963 | |
964 // "If parent is a document, and any of the statements below, switched on | |
965 // node, are true, throw a HierarchyRequestError." | |
966 if (parent_.nodeType == Node.DOCUMENT_NODE) { | |
967 switch (node.nodeType) { | |
968 case Node.DOCUMENT_FRAGMENT_NODE: | |
969 // "If node has more than one element child or has a Text node | |
970 // child. Otherwise, if node has one element child and either | |
971 // parent has an element child, child is a doctype, or child is not | |
972 // null and a doctype is following child." | |
973 if ([].filter.call(node.childNodes, isElement).length > 1) { | |
974 return "HIERARCHY_REQUEST_ERR"; | |
975 } | |
976 | |
977 if ([].some.call(node.childNodes, isText)) { | |
978 return "HIERARCHY_REQUEST_ERR"; | |
979 } | |
980 | |
981 if ([].filter.call(node.childNodes, isElement).length == 1) { | |
982 if ([].some.call(parent_.childNodes, isElement)) { | |
983 return "HIERARCHY_REQUEST_ERR"; | |
984 } | |
985 | |
986 if (child && child.nodeType == Node.DOCUMENT_TYPE_NODE) { | |
987 return "HIERARCHY_REQUEST_ERR"; | |
988 } | |
989 | |
990 if (child && [].slice.call(parent_.childNodes, indexOf(child) +
1) | |
991 .filter(isDoctype)) { | |
992 return "HIERARCHY_REQUEST_ERR"; | |
993 } | |
994 } | |
995 break; | |
996 | |
997 case Node.ELEMENT_NODE: | |
998 // "parent has an element child, child is a doctype, or child is | |
999 // not null and a doctype is following child." | |
1000 if ([].some.call(parent_.childNodes, isElement)) { | |
1001 return "HIERARCHY_REQUEST_ERR"; | |
1002 } | |
1003 | |
1004 if (child.nodeType == Node.DOCUMENT_TYPE_NODE) { | |
1005 return "HIERARCHY_REQUEST_ERR"; | |
1006 } | |
1007 | |
1008 if (child && [].slice.call(parent_.childNodes, indexOf(child) + 1) | |
1009 .filter(isDoctype)) { | |
1010 return "HIERARCHY_REQUEST_ERR"; | |
1011 } | |
1012 break; | |
1013 | |
1014 case Node.DOCUMENT_TYPE_NODE: | |
1015 // "parent has a doctype child, an element is preceding child, or | |
1016 // child is null and parent has an element child." | |
1017 if ([].some.call(parent_.childNodes, isDoctype)) { | |
1018 return "HIERARCHY_REQUEST_ERR"; | |
1019 } | |
1020 | |
1021 if (child && [].slice.call(parent_.childNodes, 0, indexOf(child)) | |
1022 .some(isElement)) { | |
1023 return "HIERARCHY_REQUEST_ERR"; | |
1024 } | |
1025 | |
1026 if (!child && [].some.call(parent_.childNodes, isElement)) { | |
1027 return "HIERARCHY_REQUEST_ERR"; | |
1028 } | |
1029 break; | |
1030 } | |
1031 } | |
1032 } | |
1033 | |
1034 /** | |
1035 * Asserts that two nodes are equal, in the sense of isEqualNode(). If they | |
1036 * aren't, tries to print a relatively informative reason why not. TODO: Move | |
1037 * this to testharness.js? | |
1038 */ | |
1039 function assertNodesEqual(actual, expected, msg) { | |
1040 if (!actual.isEqualNode(expected)) { | |
1041 msg = "Actual and expected mismatch for " + msg + ". "; | |
1042 | |
1043 while (actual && expected) { | |
1044 assert_true(actual.nodeType === expected.nodeType | |
1045 && actual.nodeName === expected.nodeName | |
1046 && actual.nodeValue === expected.nodeValue, | |
1047 "First differing node: expected " + format_value(expected) | |
1048 + ", got " + format_value(actual) + " [" + msg + "]"); | |
1049 actual = nextNode(actual); | |
1050 expected = nextNode(expected); | |
1051 } | |
1052 | |
1053 assert_unreached("DOMs were not equal but we couldn't figure out why"); | |
1054 } | |
1055 } | |
1056 | |
1057 /** | |
1058 * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR"). | |
1059 */ | |
1060 function getDomExceptionName(e) { | |
1061 var ret = null; | |
1062 for (var prop in e) { | |
1063 if (/^[A-Z_]+_ERR$/.test(prop) && e[prop] == e.code) { | |
1064 return prop; | |
1065 } | |
1066 } | |
1067 | |
1068 throw "Exception seems to not be a DOMException? " + e; | |
1069 } | |
1070 | |
1071 /** | |
1072 * Given an array of endpoint data [start container, start offset, end | |
1073 * container, end offset], returns a Range with those endpoints. | |
1074 */ | |
1075 function rangeFromEndpoints(endpoints) { | |
1076 // If we just use document instead of the ownerDocument of endpoints[0], | |
1077 // WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in | |
1078 // range, not selection, so we don't want to fail anything for it. | |
1079 var range = ownerDocument(endpoints[0]).createRange(); | |
1080 range.setStart(endpoints[0], endpoints[1]); | |
1081 range.setEnd(endpoints[2], endpoints[3]); | |
1082 return range; | |
1083 } | |
OLD | NEW |