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