mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
953 lines
33 KiB
JavaScript
953 lines
33 KiB
JavaScript
|
"use strict";
|
|||
|
// TODO: iframes, contenteditable/designMode
|
|||
|
|
|||
|
// Everything is done in functions in this test harness, so we have to declare
|
|||
|
// all the variables before use to make sure they can be reused.
|
|||
|
var selection;
|
|||
|
var testDiv, paras, detachedDiv, detachedPara1, detachedPara2,
|
|||
|
foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement,
|
|||
|
detachedXmlElement, detachedTextNode, foreignTextNode,
|
|||
|
detachedForeignTextNode, xmlTextNode, detachedXmlTextNode,
|
|||
|
processingInstruction, detachedProcessingInstruction, comment,
|
|||
|
detachedComment, foreignComment, detachedForeignComment, xmlComment,
|
|||
|
detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype,
|
|||
|
foreignDoctype, xmlDoctype;
|
|||
|
var testRanges, testPoints, testNodes;
|
|||
|
|
|||
|
function setupRangeTests() {
|
|||
|
selection = getSelection();
|
|||
|
testDiv = document.querySelector("#test");
|
|||
|
if (testDiv) {
|
|||
|
testDiv.parentNode.removeChild(testDiv);
|
|||
|
}
|
|||
|
testDiv = document.createElement("div");
|
|||
|
testDiv.id = "test";
|
|||
|
document.body.insertBefore(testDiv, document.body.firstChild);
|
|||
|
// Test some diacritics, to make sure browsers are using code units here
|
|||
|
// and not something like grapheme clusters.
|
|||
|
testDiv.innerHTML = "<p id=a>Äb̈c̈d̈ëf̈g̈ḧ\n"
|
|||
|
+ "<p id=b style=display:none>Ijklmnop\n"
|
|||
|
+ "<p id=c>Qrstuvwx"
|
|||
|
+ "<p id=d style=display:none>Yzabcdef"
|
|||
|
+ "<p id=e style=display:none>Ghijklmn";
|
|||
|
paras = testDiv.querySelectorAll("p");
|
|||
|
|
|||
|
detachedDiv = document.createElement("div");
|
|||
|
detachedPara1 = document.createElement("p");
|
|||
|
detachedPara1.appendChild(document.createTextNode("Opqrstuv"));
|
|||
|
detachedPara2 = document.createElement("p");
|
|||
|
detachedPara2.appendChild(document.createTextNode("Wxyzabcd"));
|
|||
|
detachedDiv.appendChild(detachedPara1);
|
|||
|
detachedDiv.appendChild(detachedPara2);
|
|||
|
|
|||
|
// Opera doesn't automatically create a doctype for a new HTML document,
|
|||
|
// contrary to spec. It also doesn't let you add doctypes to documents
|
|||
|
// after the fact through any means I've tried. So foreignDoc in Opera
|
|||
|
// will have no doctype, foreignDoctype will be null, and Opera will fail
|
|||
|
// some tests somewhat mysteriously as a result.
|
|||
|
foreignDoc = document.implementation.createHTMLDocument("");
|
|||
|
foreignPara1 = foreignDoc.createElement("p");
|
|||
|
foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl"));
|
|||
|
foreignPara2 = foreignDoc.createElement("p");
|
|||
|
foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst"));
|
|||
|
foreignDoc.body.appendChild(foreignPara1);
|
|||
|
foreignDoc.body.appendChild(foreignPara2);
|
|||
|
|
|||
|
// Now we get to do really silly stuff, which nobody in the universe is
|
|||
|
// ever going to actually do, but the spec defines behavior, so too bad.
|
|||
|
// Testing is fun!
|
|||
|
xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y");
|
|||
|
xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
|
|||
|
detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names");
|
|||
|
detachedTextNode = document.createTextNode("Uvwxyzab");
|
|||
|
detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij");
|
|||
|
detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr");
|
|||
|
// PIs only exist in XML documents, so don't bother with document or
|
|||
|
// foreignDoc.
|
|||
|
detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp");
|
|||
|
detachedComment = document.createComment("Stuvwxyz");
|
|||
|
// Hurrah, we finally got to "z" at the end!
|
|||
|
detachedForeignComment = foreignDoc.createComment("אריה יהודה");
|
|||
|
detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר");
|
|||
|
|
|||
|
// We should also test with document fragments that actually contain stuff
|
|||
|
// . . . but, maybe later.
|
|||
|
docfrag = document.createDocumentFragment();
|
|||
|
foreignDocfrag = foreignDoc.createDocumentFragment();
|
|||
|
xmlDocfrag = xmlDoc.createDocumentFragment();
|
|||
|
|
|||
|
xmlElement = xmlDoc.createElement("igiveuponcreativenames");
|
|||
|
xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti");
|
|||
|
xmlElement.appendChild(xmlTextNode);
|
|||
|
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?');
|
|||
|
xmlDoc.appendChild(xmlElement);
|
|||
|
xmlDoc.appendChild(processingInstruction);
|
|||
|
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");
|
|||
|
xmlDoc.appendChild(xmlComment);
|
|||
|
|
|||
|
comment = document.createComment("Alphabet soup?");
|
|||
|
testDiv.appendChild(comment);
|
|||
|
|
|||
|
foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things. I\'ve seen non-native speakers trip up on this.');
|
|||
|
foreignDoc.appendChild(foreignComment);
|
|||
|
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.");
|
|||
|
foreignDoc.body.appendChild(foreignTextNode);
|
|||
|
|
|||
|
doctype = document.doctype;
|
|||
|
foreignDoctype = foreignDoc.doctype;
|
|||
|
|
|||
|
testRanges = [
|
|||
|
// Various ranges within the text node children of different
|
|||
|
// paragraphs. All should be valid.
|
|||
|
"[paras[0].firstChild, 0, paras[0].firstChild, 0]",
|
|||
|
"[paras[0].firstChild, 0, paras[0].firstChild, 1]",
|
|||
|
"[paras[0].firstChild, 2, paras[0].firstChild, 8]",
|
|||
|
"[paras[0].firstChild, 2, paras[0].firstChild, 9]",
|
|||
|
"[paras[1].firstChild, 0, paras[1].firstChild, 0]",
|
|||
|
"[paras[1].firstChild, 0, paras[1].firstChild, 1]",
|
|||
|
"[paras[1].firstChild, 2, paras[1].firstChild, 8]",
|
|||
|
"[paras[1].firstChild, 2, paras[1].firstChild, 9]",
|
|||
|
"[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]",
|
|||
|
"[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]",
|
|||
|
"[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]",
|
|||
|
"[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]",
|
|||
|
"[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]",
|
|||
|
"[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]",
|
|||
|
// Now try testing some elements, not just text nodes.
|
|||
|
"[document.documentElement, 0, document.documentElement, 1]",
|
|||
|
"[document.documentElement, 0, document.documentElement, 2]",
|
|||
|
"[document.documentElement, 1, document.documentElement, 2]",
|
|||
|
"[document.head, 1, document.head, 1]",
|
|||
|
"[document.body, 0, document.body, 1]",
|
|||
|
"[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]",
|
|||
|
"[foreignDoc.head, 1, foreignDoc.head, 1]",
|
|||
|
"[foreignDoc.body, 0, foreignDoc.body, 0]",
|
|||
|
"[paras[0], 0, paras[0], 0]",
|
|||
|
"[paras[0], 0, paras[0], 1]",
|
|||
|
"[detachedPara1, 0, detachedPara1, 0]",
|
|||
|
"[detachedPara1, 0, detachedPara1, 1]",
|
|||
|
// Now try some ranges that span elements.
|
|||
|
"[paras[0].firstChild, 0, paras[1].firstChild, 0]",
|
|||
|
"[paras[0].firstChild, 0, paras[1].firstChild, 8]",
|
|||
|
"[paras[0].firstChild, 3, paras[3], 1]",
|
|||
|
// How about something that spans a node and its descendant?
|
|||
|
"[paras[0], 0, paras[0].firstChild, 7]",
|
|||
|
"[testDiv, 2, paras[4], 1]",
|
|||
|
"[testDiv, 1, paras[2].firstChild, 5]",
|
|||
|
"[document.documentElement, 1, document.body, 0]",
|
|||
|
"[foreignDoc.documentElement, 1, foreignDoc.body, 0]",
|
|||
|
// Then a few more interesting things just for good measure.
|
|||
|
"[document, 0, document, 1]",
|
|||
|
"[document, 0, document, 2]",
|
|||
|
"[document, 1, document, 2]",
|
|||
|
"[testDiv, 0, comment, 5]",
|
|||
|
"[paras[2].firstChild, 4, comment, 2]",
|
|||
|
"[paras[3], 1, comment, 8]",
|
|||
|
"[foreignDoc, 0, foreignDoc, 0]",
|
|||
|
"[foreignDoc, 1, foreignComment, 2]",
|
|||
|
"[foreignDoc.body, 0, foreignTextNode, 36]",
|
|||
|
"[xmlDoc, 0, xmlDoc, 0]",
|
|||
|
// Opera 11 crashes if you extractContents() a range that ends at offset
|
|||
|
// zero in a comment. Comment out this line to run the tests successfully.
|
|||
|
"[xmlDoc, 1, xmlComment, 0]",
|
|||
|
"[detachedTextNode, 0, detachedTextNode, 8]",
|
|||
|
"[detachedForeignTextNode, 7, detachedForeignTextNode, 7]",
|
|||
|
"[detachedForeignTextNode, 0, detachedForeignTextNode, 8]",
|
|||
|
"[detachedXmlTextNode, 7, detachedXmlTextNode, 7]",
|
|||
|
"[detachedXmlTextNode, 0, detachedXmlTextNode, 8]",
|
|||
|
"[detachedComment, 3, detachedComment, 4]",
|
|||
|
"[detachedComment, 5, detachedComment, 5]",
|
|||
|
"[detachedForeignComment, 0, detachedForeignComment, 1]",
|
|||
|
"[detachedForeignComment, 4, detachedForeignComment, 4]",
|
|||
|
"[detachedXmlComment, 2, detachedXmlComment, 6]",
|
|||
|
"[docfrag, 0, docfrag, 0]",
|
|||
|
"[foreignDocfrag, 0, foreignDocfrag, 0]",
|
|||
|
"[xmlDocfrag, 0, xmlDocfrag, 0]",
|
|||
|
];
|
|||
|
|
|||
|
testPoints = [
|
|||
|
// Various positions within the page, some invalid. Remember that
|
|||
|
// paras[0] is visible, and paras[1] is display: none.
|
|||
|
"[paras[0].firstChild, -1]",
|
|||
|
"[paras[0].firstChild, 0]",
|
|||
|
"[paras[0].firstChild, 1]",
|
|||
|
"[paras[0].firstChild, 2]",
|
|||
|
"[paras[0].firstChild, 8]",
|
|||
|
"[paras[0].firstChild, 9]",
|
|||
|
"[paras[0].firstChild, 10]",
|
|||
|
"[paras[0].firstChild, 65535]",
|
|||
|
"[paras[1].firstChild, -1]",
|
|||
|
"[paras[1].firstChild, 0]",
|
|||
|
"[paras[1].firstChild, 1]",
|
|||
|
"[paras[1].firstChild, 2]",
|
|||
|
"[paras[1].firstChild, 8]",
|
|||
|
"[paras[1].firstChild, 9]",
|
|||
|
"[paras[1].firstChild, 10]",
|
|||
|
"[paras[1].firstChild, 65535]",
|
|||
|
"[detachedPara1.firstChild, 0]",
|
|||
|
"[detachedPara1.firstChild, 1]",
|
|||
|
"[detachedPara1.firstChild, 8]",
|
|||
|
"[detachedPara1.firstChild, 9]",
|
|||
|
"[foreignPara1.firstChild, 0]",
|
|||
|
"[foreignPara1.firstChild, 1]",
|
|||
|
"[foreignPara1.firstChild, 8]",
|
|||
|
"[foreignPara1.firstChild, 9]",
|
|||
|
// Now try testing some elements, not just text nodes.
|
|||
|
"[document.documentElement, -1]",
|
|||
|
"[document.documentElement, 0]",
|
|||
|
"[document.documentElement, 1]",
|
|||
|
"[document.documentElement, 2]",
|
|||
|
"[document.documentElement, 7]",
|
|||
|
"[document.head, 1]",
|
|||
|
"[document.body, 3]",
|
|||
|
"[foreignDoc.documentElement, 0]",
|
|||
|
"[foreignDoc.documentElement, 1]",
|
|||
|
"[foreignDoc.head, 0]",
|
|||
|
"[foreignDoc.body, 1]",
|
|||
|
"[paras[0], 0]",
|
|||
|
"[paras[0], 1]",
|
|||
|
"[paras[0], 2]",
|
|||
|
"[paras[1], 0]",
|
|||
|
"[paras[1], 1]",
|
|||
|
"[paras[1], 2]",
|
|||
|
"[detachedPara1, 0]",
|
|||
|
"[detachedPara1, 1]",
|
|||
|
"[testDiv, 0]",
|
|||
|
"[testDiv, 3]",
|
|||
|
// Then a few more interesting things just for good measure.
|
|||
|
"[document, -1]",
|
|||
|
"[document, 0]",
|
|||
|
"[document, 1]",
|
|||
|
"[document, 2]",
|
|||
|
"[document, 3]",
|
|||
|
"[comment, -1]",
|
|||
|
"[comment, 0]",
|
|||
|
"[comment, 4]",
|
|||
|
"[comment, 96]",
|
|||
|
"[foreignDoc, 0]",
|
|||
|
"[foreignDoc, 1]",
|
|||
|
"[foreignComment, 2]",
|
|||
|
"[foreignTextNode, 0]",
|
|||
|
"[foreignTextNode, 36]",
|
|||
|
"[xmlDoc, -1]",
|
|||
|
"[xmlDoc, 0]",
|
|||
|
"[xmlDoc, 1]",
|
|||
|
"[xmlDoc, 5]",
|
|||
|
"[xmlComment, 0]",
|
|||
|
"[xmlComment, 4]",
|
|||
|
"[processingInstruction, 0]",
|
|||
|
"[processingInstruction, 5]",
|
|||
|
"[processingInstruction, 9]",
|
|||
|
"[detachedTextNode, 0]",
|
|||
|
"[detachedTextNode, 8]",
|
|||
|
"[detachedForeignTextNode, 0]",
|
|||
|
"[detachedForeignTextNode, 8]",
|
|||
|
"[detachedXmlTextNode, 0]",
|
|||
|
"[detachedXmlTextNode, 8]",
|
|||
|
"[detachedProcessingInstruction, 12]",
|
|||
|
"[detachedComment, 3]",
|
|||
|
"[detachedComment, 5]",
|
|||
|
"[detachedForeignComment, 0]",
|
|||
|
"[detachedForeignComment, 4]",
|
|||
|
"[detachedXmlComment, 2]",
|
|||
|
"[docfrag, 0]",
|
|||
|
"[foreignDocfrag, 0]",
|
|||
|
"[xmlDocfrag, 0]",
|
|||
|
"[doctype, 0]",
|
|||
|
"[doctype, -17]",
|
|||
|
"[doctype, 1]",
|
|||
|
"[foreignDoctype, 0]",
|
|||
|
"[xmlDoctype, 0]",
|
|||
|
];
|
|||
|
|
|||
|
testNodes = [
|
|||
|
"paras[0]",
|
|||
|
"paras[0].firstChild",
|
|||
|
"paras[1]",
|
|||
|
"paras[1].firstChild",
|
|||
|
"foreignPara1",
|
|||
|
"foreignPara1.firstChild",
|
|||
|
"detachedPara1",
|
|||
|
"detachedPara1.firstChild",
|
|||
|
"detachedPara1",
|
|||
|
"detachedPara1.firstChild",
|
|||
|
"testDiv",
|
|||
|
"document",
|
|||
|
"detachedDiv",
|
|||
|
"detachedPara2",
|
|||
|
"foreignDoc",
|
|||
|
"foreignPara2",
|
|||
|
"xmlDoc",
|
|||
|
"xmlElement",
|
|||
|
"detachedXmlElement",
|
|||
|
"detachedTextNode",
|
|||
|
"foreignTextNode",
|
|||
|
"detachedForeignTextNode",
|
|||
|
"xmlTextNode",
|
|||
|
"detachedXmlTextNode",
|
|||
|
"processingInstruction",
|
|||
|
"detachedProcessingInstruction",
|
|||
|
"comment",
|
|||
|
"detachedComment",
|
|||
|
"foreignComment",
|
|||
|
"detachedForeignComment",
|
|||
|
"xmlComment",
|
|||
|
"detachedXmlComment",
|
|||
|
"docfrag",
|
|||
|
"foreignDocfrag",
|
|||
|
"xmlDocfrag",
|
|||
|
"doctype",
|
|||
|
"foreignDoctype",
|
|||
|
"xmlDoctype",
|
|||
|
];
|
|||
|
}
|
|||
|
if ("setup" in window) {
|
|||
|
setup(setupRangeTests);
|
|||
|
} else {
|
|||
|
// Presumably we're running from within an iframe or something
|
|||
|
setupRangeTests();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the length of a node as specified in DOM Range.
|
|||
|
*/
|
|||
|
function getNodeLength(node) {
|
|||
|
if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
|
|||
|
return 0;
|
|||
|
}
|
|||
|
if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) {
|
|||
|
return node.length;
|
|||
|
}
|
|||
|
return node.childNodes.length;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the furthest ancestor of a Node as defined by the spec.
|
|||
|
*/
|
|||
|
function furthestAncestor(node) {
|
|||
|
var root = node;
|
|||
|
while (root.parentNode != null) {
|
|||
|
root = root.parentNode;
|
|||
|
}
|
|||
|
return root;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* "The ancestor containers of a Node are the Node itself and all its
|
|||
|
* ancestors."
|
|||
|
*
|
|||
|
* Is node1 an ancestor container of node2?
|
|||
|
*/
|
|||
|
function isAncestorContainer(node1, node2) {
|
|||
|
return node1 == node2 ||
|
|||
|
(node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the first Node that's after node in tree order, or null if node is
|
|||
|
* the last Node.
|
|||
|
*/
|
|||
|
function nextNode(node) {
|
|||
|
if (node.hasChildNodes()) {
|
|||
|
return node.firstChild;
|
|||
|
}
|
|||
|
return nextNodeDescendants(node);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the last Node that's before node in tree order, or null if node is
|
|||
|
* the first Node.
|
|||
|
*/
|
|||
|
function previousNode(node) {
|
|||
|
if (node.previousSibling) {
|
|||
|
node = node.previousSibling;
|
|||
|
while (node.hasChildNodes()) {
|
|||
|
node = node.lastChild;
|
|||
|
}
|
|||
|
return node;
|
|||
|
}
|
|||
|
return node.parentNode;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the next Node that's after node and all its descendants in tree
|
|||
|
* order, or null if node is the last Node or an ancestor of it.
|
|||
|
*/
|
|||
|
function nextNodeDescendants(node) {
|
|||
|
while (node && !node.nextSibling) {
|
|||
|
node = node.parentNode;
|
|||
|
}
|
|||
|
if (!node) {
|
|||
|
return null;
|
|||
|
}
|
|||
|
return node.nextSibling;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the ownerDocument of the Node, or the Node itself if it's a
|
|||
|
* Document.
|
|||
|
*/
|
|||
|
function ownerDocument(node) {
|
|||
|
return node.nodeType == Node.DOCUMENT_NODE
|
|||
|
? node
|
|||
|
: node.ownerDocument;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns true if ancestor is an ancestor of descendant, false otherwise.
|
|||
|
*/
|
|||
|
function isAncestor(ancestor, descendant) {
|
|||
|
if (!ancestor || !descendant) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
while (descendant && descendant != ancestor) {
|
|||
|
descendant = descendant.parentNode;
|
|||
|
}
|
|||
|
return descendant == ancestor;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns true if descendant is a descendant of ancestor, false otherwise.
|
|||
|
*/
|
|||
|
function isDescendant(descendant, ancestor) {
|
|||
|
return isAncestor(ancestor, descendant);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* The position of two boundary points relative to one another, as defined by
|
|||
|
* the spec.
|
|||
|
*/
|
|||
|
function getPosition(nodeA, offsetA, nodeB, offsetB) {
|
|||
|
// "If node A is the same as node B, return equal if offset A equals offset
|
|||
|
// B, before if offset A is less than offset B, and after if offset A is
|
|||
|
// greater than offset B."
|
|||
|
if (nodeA == nodeB) {
|
|||
|
if (offsetA == offsetB) {
|
|||
|
return "equal";
|
|||
|
}
|
|||
|
if (offsetA < offsetB) {
|
|||
|
return "before";
|
|||
|
}
|
|||
|
if (offsetA > offsetB) {
|
|||
|
return "after";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "If node A is after node B in tree order, compute the position of (node
|
|||
|
// B, offset B) relative to (node A, offset A). If it is before, return
|
|||
|
// after. If it is after, return before."
|
|||
|
if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|||
|
var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
|
|||
|
if (pos == "before") {
|
|||
|
return "after";
|
|||
|
}
|
|||
|
if (pos == "after") {
|
|||
|
return "before";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "If node A is an ancestor of node B:"
|
|||
|
if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
|
|||
|
// "Let child equal node B."
|
|||
|
var child = nodeB;
|
|||
|
|
|||
|
// "While child is not a child of node A, set child to its parent."
|
|||
|
while (child.parentNode != nodeA) {
|
|||
|
child = child.parentNode;
|
|||
|
}
|
|||
|
|
|||
|
// "If the index of child is less than offset A, return after."
|
|||
|
if (indexOf(child) < offsetA) {
|
|||
|
return "after";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "Return before."
|
|||
|
return "before";
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* "contained" as defined by DOM Range: "A Node node is contained in a range
|
|||
|
* range if node's furthest ancestor is the same as range's root, and (node, 0)
|
|||
|
* is after range's start, and (node, length of node) is before range's end."
|
|||
|
*/
|
|||
|
function isContained(node, range) {
|
|||
|
var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
|
|||
|
var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
|
|||
|
|
|||
|
return furthestAncestor(node) == furthestAncestor(range.startContainer)
|
|||
|
&& pos1 == "after"
|
|||
|
&& pos2 == "before";
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* "partially contained" as defined by DOM Range: "A Node is partially
|
|||
|
* contained in a range if it is an ancestor container of the range's start but
|
|||
|
* not its end, or vice versa."
|
|||
|
*/
|
|||
|
function isPartiallyContained(node, range) {
|
|||
|
var cond1 = isAncestorContainer(node, range.startContainer);
|
|||
|
var cond2 = isAncestorContainer(node, range.endContainer);
|
|||
|
return (cond1 && !cond2) || (cond2 && !cond1);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Index of a node as defined by the spec.
|
|||
|
*/
|
|||
|
function indexOf(node) {
|
|||
|
if (!node.parentNode) {
|
|||
|
// No preceding sibling nodes, right?
|
|||
|
return 0;
|
|||
|
}
|
|||
|
var i = 0;
|
|||
|
while (node != node.parentNode.childNodes[i]) {
|
|||
|
i++;
|
|||
|
}
|
|||
|
return i;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* extractContents() implementation, following the spec. If an exception is
|
|||
|
* supposed to be thrown, will return a string with the name (e.g.,
|
|||
|
* "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also
|
|||
|
* return an arbitrary human-readable string if a condition is hit that implies
|
|||
|
* a spec bug.
|
|||
|
*/
|
|||
|
function myExtractContents(range) {
|
|||
|
// "If the context object's detached flag is set, raise an
|
|||
|
// INVALID_STATE_ERR exception and abort these steps."
|
|||
|
try {
|
|||
|
range.collapsed;
|
|||
|
} catch (e) {
|
|||
|
return "INVALID_STATE_ERR";
|
|||
|
}
|
|||
|
|
|||
|
// "Let frag be a new DocumentFragment whose ownerDocument is the same as
|
|||
|
// the ownerDocument of the context object's start node."
|
|||
|
var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
|
|||
|
? range.startContainer
|
|||
|
: range.startContainer.ownerDocument;
|
|||
|
var frag = ownerDoc.createDocumentFragment();
|
|||
|
|
|||
|
// "If the context object's start and end are the same, abort this method,
|
|||
|
// returning frag."
|
|||
|
if (range.startContainer == range.endContainer
|
|||
|
&& range.startOffset == range.endOffset) {
|
|||
|
return frag;
|
|||
|
}
|
|||
|
|
|||
|
// "Let original start node, original start offset, original end node, and
|
|||
|
// original end offset be the context object's start and end nodes and
|
|||
|
// offsets, respectively."
|
|||
|
var originalStartNode = range.startContainer;
|
|||
|
var originalStartOffset = range.startOffset;
|
|||
|
var originalEndNode = range.endContainer;
|
|||
|
var originalEndOffset = range.endOffset;
|
|||
|
|
|||
|
// "If original start node and original end node are the same, and they are
|
|||
|
// a Text or Comment node:"
|
|||
|
if (range.startContainer == range.endContainer
|
|||
|
&& (range.startContainer.nodeType == Node.TEXT_NODE
|
|||
|
|| range.startContainer.nodeType == Node.COMMENT_NODE)) {
|
|||
|
// "Let clone be the result of calling cloneNode(false) on original
|
|||
|
// start node."
|
|||
|
var clone = originalStartNode.cloneNode(false);
|
|||
|
|
|||
|
// "Set the data of clone to the result of calling
|
|||
|
// substringData(original start offset, original end offset − original
|
|||
|
// start offset) on original start node."
|
|||
|
clone.data = originalStartNode.substringData(originalStartOffset,
|
|||
|
originalEndOffset - originalStartOffset);
|
|||
|
|
|||
|
// "Append clone as the last child of frag."
|
|||
|
frag.appendChild(clone);
|
|||
|
|
|||
|
// "Call deleteData(original start offset, original end offset −
|
|||
|
// original start offset) on original start node."
|
|||
|
originalStartNode.deleteData(originalStartOffset,
|
|||
|
originalEndOffset - originalStartOffset);
|
|||
|
|
|||
|
// "Abort this method, returning frag."
|
|||
|
return frag;
|
|||
|
}
|
|||
|
|
|||
|
// "Let common ancestor equal original start node."
|
|||
|
var commonAncestor = originalStartNode;
|
|||
|
|
|||
|
// "While common ancestor is not an ancestor container of original end
|
|||
|
// node, set common ancestor to its own parent."
|
|||
|
while (!isAncestorContainer(commonAncestor, originalEndNode)) {
|
|||
|
commonAncestor = commonAncestor.parentNode;
|
|||
|
}
|
|||
|
|
|||
|
// "If original start node is an ancestor container of original end node,
|
|||
|
// let first partially contained child be null."
|
|||
|
var firstPartiallyContainedChild;
|
|||
|
if (isAncestorContainer(originalStartNode, originalEndNode)) {
|
|||
|
firstPartiallyContainedChild = null;
|
|||
|
// "Otherwise, let first partially contained child be the first child of
|
|||
|
// common ancestor that is partially contained in the context object."
|
|||
|
} else {
|
|||
|
for (var i = 0; i < commonAncestor.childNodes.length; i++) {
|
|||
|
if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
|
|||
|
firstPartiallyContainedChild = commonAncestor.childNodes[i];
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (!firstPartiallyContainedChild) {
|
|||
|
throw "Spec bug: no first partially contained child!";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "If original end node is an ancestor container of original start node,
|
|||
|
// let last partially contained child be null."
|
|||
|
var lastPartiallyContainedChild;
|
|||
|
if (isAncestorContainer(originalEndNode, originalStartNode)) {
|
|||
|
lastPartiallyContainedChild = null;
|
|||
|
// "Otherwise, let last partially contained child be the last child of
|
|||
|
// common ancestor that is partially contained in the context object."
|
|||
|
} else {
|
|||
|
for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
|
|||
|
if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
|
|||
|
lastPartiallyContainedChild = commonAncestor.childNodes[i];
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (!lastPartiallyContainedChild) {
|
|||
|
throw "Spec bug: no last partially contained child!";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "Let contained children be a list of all children of common ancestor
|
|||
|
// that are contained in the context object, in tree order."
|
|||
|
//
|
|||
|
// "If any member of contained children is a DocumentType, raise a
|
|||
|
// HIERARCHY_REQUEST_ERR exception and abort these steps."
|
|||
|
var containedChildren = [];
|
|||
|
for (var i = 0; i < commonAncestor.childNodes.length; i++) {
|
|||
|
if (isContained(commonAncestor.childNodes[i], range)) {
|
|||
|
if (commonAncestor.childNodes[i].nodeType
|
|||
|
== Node.DOCUMENT_TYPE_NODE) {
|
|||
|
return "HIERARCHY_REQUEST_ERR";
|
|||
|
}
|
|||
|
containedChildren.push(commonAncestor.childNodes[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "If original start node is an ancestor container of original end node,
|
|||
|
// set new node to original start node and new offset to original start
|
|||
|
// offset."
|
|||
|
var newNode, newOffset;
|
|||
|
if (isAncestorContainer(originalStartNode, originalEndNode)) {
|
|||
|
newNode = originalStartNode;
|
|||
|
newOffset = originalStartOffset;
|
|||
|
// "Otherwise:"
|
|||
|
} else {
|
|||
|
// "Let reference node equal original start node."
|
|||
|
var referenceNode = originalStartNode;
|
|||
|
|
|||
|
// "While reference node's parent is not null and is not an ancestor
|
|||
|
// container of original end node, set reference node to its parent."
|
|||
|
while (referenceNode.parentNode
|
|||
|
&& !isAncestorContainer(referenceNode.parentNode, originalEndNode)) {
|
|||
|
referenceNode = referenceNode.parentNode;
|
|||
|
}
|
|||
|
|
|||
|
// "Set new node to the parent of reference node, and new offset to one
|
|||
|
// plus the index of reference node."
|
|||
|
newNode = referenceNode.parentNode;
|
|||
|
newOffset = 1 + indexOf(referenceNode);
|
|||
|
}
|
|||
|
|
|||
|
// "If first partially contained child is a Text or Comment node:"
|
|||
|
if (firstPartiallyContainedChild
|
|||
|
&& (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
|
|||
|
|| firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
|
|||
|
// "Let clone be the result of calling cloneNode(false) on original
|
|||
|
// start node."
|
|||
|
var clone = originalStartNode.cloneNode(false);
|
|||
|
|
|||
|
// "Set the data of clone to the result of calling substringData() on
|
|||
|
// original start node, with original start offset as the first
|
|||
|
// argument and (length of original start node − original start offset)
|
|||
|
// as the second."
|
|||
|
clone.data = originalStartNode.substringData(originalStartOffset,
|
|||
|
getNodeLength(originalStartNode) - originalStartOffset);
|
|||
|
|
|||
|
// "Append clone as the last child of frag."
|
|||
|
frag.appendChild(clone);
|
|||
|
|
|||
|
// "Call deleteData() on original start node, with original start
|
|||
|
// offset as the first argument and (length of original start node −
|
|||
|
// original start offset) as the second."
|
|||
|
originalStartNode.deleteData(originalStartOffset,
|
|||
|
getNodeLength(originalStartNode) - originalStartOffset);
|
|||
|
// "Otherwise, if first partially contained child is not null:"
|
|||
|
} else if (firstPartiallyContainedChild) {
|
|||
|
// "Let clone be the result of calling cloneNode(false) on first
|
|||
|
// partially contained child."
|
|||
|
var clone = firstPartiallyContainedChild.cloneNode(false);
|
|||
|
|
|||
|
// "Append clone as the last child of frag."
|
|||
|
frag.appendChild(clone);
|
|||
|
|
|||
|
// "Let subrange be a new Range whose start is (original start node,
|
|||
|
// original start offset) and whose end is (first partially contained
|
|||
|
// child, length of first partially contained child)."
|
|||
|
var subrange = ownerDoc.createRange();
|
|||
|
subrange.setStart(originalStartNode, originalStartOffset);
|
|||
|
subrange.setEnd(firstPartiallyContainedChild,
|
|||
|
getNodeLength(firstPartiallyContainedChild));
|
|||
|
|
|||
|
// "Let subfrag be the result of calling extractContents() on
|
|||
|
// subrange."
|
|||
|
var subfrag = myExtractContents(subrange);
|
|||
|
|
|||
|
// "For each child of subfrag, in order, append that child to clone as
|
|||
|
// its last child."
|
|||
|
for (var i = 0; i < subfrag.childNodes.length; i++) {
|
|||
|
clone.appendChild(subfrag.childNodes[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "For each contained child in contained children, append contained child
|
|||
|
// as the last child of frag."
|
|||
|
for (var i = 0; i < containedChildren.length; i++) {
|
|||
|
frag.appendChild(containedChildren[i]);
|
|||
|
}
|
|||
|
|
|||
|
// "If last partially contained child is a Text or Comment node:"
|
|||
|
if (lastPartiallyContainedChild
|
|||
|
&& (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
|
|||
|
|| lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
|
|||
|
// "Let clone be the result of calling cloneNode(false) on original
|
|||
|
// end node."
|
|||
|
var clone = originalEndNode.cloneNode(false);
|
|||
|
|
|||
|
// "Set the data of clone to the result of calling substringData(0,
|
|||
|
// original end offset) on original end node."
|
|||
|
clone.data = originalEndNode.substringData(0, originalEndOffset);
|
|||
|
|
|||
|
// "Append clone as the last child of frag."
|
|||
|
frag.appendChild(clone);
|
|||
|
|
|||
|
// "Call deleteData(0, original end offset) on original end node."
|
|||
|
originalEndNode.deleteData(0, originalEndOffset);
|
|||
|
// "Otherwise, if last partially contained child is not null:"
|
|||
|
} else if (lastPartiallyContainedChild) {
|
|||
|
// "Let clone be the result of calling cloneNode(false) on last
|
|||
|
// partially contained child."
|
|||
|
var clone = lastPartiallyContainedChild.cloneNode(false);
|
|||
|
|
|||
|
// "Append clone as the last child of frag."
|
|||
|
frag.appendChild(clone);
|
|||
|
|
|||
|
// "Let subrange be a new Range whose start is (last partially
|
|||
|
// contained child, 0) and whose end is (original end node, original
|
|||
|
// end offset)."
|
|||
|
var subrange = ownerDoc.createRange();
|
|||
|
subrange.setStart(lastPartiallyContainedChild, 0);
|
|||
|
subrange.setEnd(originalEndNode, originalEndOffset);
|
|||
|
|
|||
|
// "Let subfrag be the result of calling extractContents() on
|
|||
|
// subrange."
|
|||
|
var subfrag = myExtractContents(subrange);
|
|||
|
|
|||
|
// "For each child of subfrag, in order, append that child to clone as
|
|||
|
// its last child."
|
|||
|
for (var i = 0; i < subfrag.childNodes.length; i++) {
|
|||
|
clone.appendChild(subfrag.childNodes[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "Set the context object's start and end to (new node, new offset)."
|
|||
|
range.setStart(newNode, newOffset);
|
|||
|
range.setEnd(newNode, newOffset);
|
|||
|
|
|||
|
// "Return frag."
|
|||
|
return frag;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* insertNode() implementation, following the spec. If an exception is
|
|||
|
* supposed to be thrown, will return a string with the name (e.g.,
|
|||
|
* "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also
|
|||
|
* return an arbitrary human-readable string if a condition is hit that implies
|
|||
|
* a spec bug.
|
|||
|
*/
|
|||
|
function myInsertNode(range, newNode) {
|
|||
|
// "If the context object's detached flag is set, raise an
|
|||
|
// INVALID_STATE_ERR exception and abort these steps."
|
|||
|
//
|
|||
|
// Assume that if accessing collapsed throws, it's detached.
|
|||
|
try {
|
|||
|
range.collapsed;
|
|||
|
} catch (e) {
|
|||
|
return "INVALID_STATE_ERR";
|
|||
|
}
|
|||
|
|
|||
|
// "If the context object's start node is a Text or Comment node and its
|
|||
|
// parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these
|
|||
|
// steps."
|
|||
|
if ((range.startContainer.nodeType == Node.TEXT_NODE
|
|||
|
|| range.startContainer.nodeType == Node.COMMENT_NODE)
|
|||
|
&& !range.startContainer.parentNode) {
|
|||
|
return "HIERARCHY_REQUEST_ERR";
|
|||
|
}
|
|||
|
|
|||
|
// "If the context object's start node is a Text node, run splitText() on
|
|||
|
// it with the context object's start offset as its argument, and let
|
|||
|
// reference node be the result."
|
|||
|
var referenceNode;
|
|||
|
if (range.startContainer.nodeType == Node.TEXT_NODE) {
|
|||
|
// We aren't testing how ranges vary under mutations, and browsers vary
|
|||
|
// in how they mutate for splitText, so let's just force the correct
|
|||
|
// way.
|
|||
|
var start = [range.startContainer, range.startOffset];
|
|||
|
var end = [range.endContainer, range.endOffset];
|
|||
|
|
|||
|
referenceNode = range.startContainer.splitText(range.startOffset);
|
|||
|
|
|||
|
if (start[0] == end[0]
|
|||
|
&& end[1] > start[1]) {
|
|||
|
end[0] = referenceNode;
|
|||
|
end[1] -= start[1];
|
|||
|
} else if (end[0] == start[0].parentNode
|
|||
|
&& end[1] > indexOf(referenceNode)) {
|
|||
|
end[1]++;
|
|||
|
}
|
|||
|
range.setStart(start[0], start[1]);
|
|||
|
range.setEnd(end[0], end[1]);
|
|||
|
// "Otherwise, if the context object's start node is a Comment, let
|
|||
|
// reference node be the context object's start node."
|
|||
|
} else if (range.startContainer.nodeType == Node.COMMENT_NODE) {
|
|||
|
referenceNode = range.startContainer;
|
|||
|
// "Otherwise, let reference node be the child of the context object's
|
|||
|
// start node with index equal to the context object's start offset, or
|
|||
|
// null if there is no such child."
|
|||
|
} else {
|
|||
|
referenceNode = range.startContainer.childNodes[range.startOffset];
|
|||
|
if (typeof referenceNode == "undefined") {
|
|||
|
referenceNode = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// "If reference node is null, let parent node be the context object's
|
|||
|
// start node."
|
|||
|
var parentNode;
|
|||
|
if (!referenceNode) {
|
|||
|
parentNode = range.startContainer;
|
|||
|
// "Otherwise, let parent node be the parent of reference node."
|
|||
|
} else {
|
|||
|
parentNode = referenceNode.parentNode;
|
|||
|
}
|
|||
|
|
|||
|
// "Call insertBefore(newNode, reference node) on parent node, re-raising
|
|||
|
// any exceptions that call raised."
|
|||
|
try {
|
|||
|
parentNode.insertBefore(newNode, referenceNode);
|
|||
|
} catch (e) {
|
|||
|
return getDomExceptionName(e);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Asserts that two nodes are equal, in the sense of isEqualNode(). If they
|
|||
|
* aren't, tries to print a relatively informative reason why not. TODO: Move
|
|||
|
* this to testharness.js?
|
|||
|
*/
|
|||
|
function assertNodesEqual(actual, expected, msg) {
|
|||
|
if (!actual.isEqualNode(expected)) {
|
|||
|
msg = "Actual and expected mismatch for " + msg + ". ";
|
|||
|
|
|||
|
while (actual && expected) {
|
|||
|
assert_true(actual.nodeType === expected.nodeType
|
|||
|
&& actual.nodeName === expected.nodeName
|
|||
|
&& actual.nodeValue === expected.nodeValue
|
|||
|
&& actual.childNodes.length === expected.childNodes.length,
|
|||
|
"First differing node: expected " + format_value(expected)
|
|||
|
+ ", got " + format_value(actual));
|
|||
|
actual = nextNode(actual);
|
|||
|
expected = nextNode(expected);
|
|||
|
}
|
|||
|
|
|||
|
assert_unreached("DOMs were not equal but we couldn't figure out why");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR"). In
|
|||
|
* theory this should be just e.name, but in practice it's not. So I could
|
|||
|
* legitimately just return e.name, but then every engine but WebKit would fail
|
|||
|
* every test, since no one seems to care much for standardizing DOMExceptions.
|
|||
|
* Instead I mangle it to account for browser bugs, so as not to fail
|
|||
|
* insertNode() tests (for instance) for insertBefore() bugs. Of course, a
|
|||
|
* standards-compliant browser will work right in any event.
|
|||
|
*
|
|||
|
* If the exception has no string property called "name" or "message", we just
|
|||
|
* re-throw it.
|
|||
|
*/
|
|||
|
function getDomExceptionName(e) {
|
|||
|
if (typeof e.name == "string"
|
|||
|
&& /^[A-Z_]+_ERR$/.test(e.name)) {
|
|||
|
// Either following the standard, or prefixing NS_ERROR_DOM (I'm
|
|||
|
// looking at you, Gecko).
|
|||
|
return e.name.replace(/^NS_ERROR_DOM_/, "");
|
|||
|
}
|
|||
|
|
|||
|
if (typeof e.message == "string"
|
|||
|
&& /^[A-Z_]+_ERR$/.test(e.message)) {
|
|||
|
// Opera
|
|||
|
return e.message;
|
|||
|
}
|
|||
|
|
|||
|
if (typeof e.message == "string"
|
|||
|
&& /^DOM Exception:/.test(e.message)) {
|
|||
|
// IE
|
|||
|
return /[A-Z_]+_ERR/.exec(e.message)[0];
|
|||
|
}
|
|||
|
|
|||
|
throw e;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Given an array of endpoint data [start container, start offset, end
|
|||
|
* container, end offset], returns a Range with those endpoints.
|
|||
|
*/
|
|||
|
function rangeFromEndpoints(endpoints) {
|
|||
|
// If we just use document instead of the ownerDocument of endpoints[0],
|
|||
|
// WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in
|
|||
|
// range, not selection, so we don't want to fail anything for it.
|
|||
|
var range = ownerDocument(endpoints[0]).createRange();
|
|||
|
range.setStart(endpoints[0], endpoints[1]);
|
|||
|
range.setEnd(endpoints[2], endpoints[3]);
|
|||
|
return range;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Given an array of endpoint data [start container, start offset, end
|
|||
|
* container, end offset], sets the selection to have those endpoints. Uses
|
|||
|
* addRange, so the range will be forwards. Accepts an empty array for
|
|||
|
* endpoints, in which case the selection will just be emptied.
|
|||
|
*/
|
|||
|
function setSelectionForwards(endpoints) {
|
|||
|
selection.removeAllRanges();
|
|||
|
if (endpoints.length) {
|
|||
|
selection.addRange(rangeFromEndpoints(endpoints));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Given an array of endpoint data [start container, start offset, end
|
|||
|
* container, end offset], sets the selection to have those endpoints, with the
|
|||
|
* direction backwards. Uses extend, so it will throw in IE. Accepts an empty
|
|||
|
* array for endpoints, in which case the selection will just be emptied.
|
|||
|
*/
|
|||
|
function setSelectionBackwards(endpoints) {
|
|||
|
selection.removeAllRanges();
|
|||
|
if (endpoints.length) {
|
|||
|
selection.collapse(endpoints[2], endpoints[3]);
|
|||
|
selection.extend(endpoints[0], endpoints[1]);
|
|||
|
}
|
|||
|
}
|