jsdom-jscore-rn
Version:
A fork of brentvatne/jsdom-jscore to run within react-native
1,749 lines (1,480 loc) • 58.2 kB
JavaScript
/*
ServerJS Javascript DOM Level 1
*/
var inheritFrom = require("../utils").inheritFrom;
// utility functions
var attachId = function(id,elm,doc) {
if (id && elm && doc) {
if (!doc._ids[id]) {
doc._ids[id] = [];
}
doc._ids[id].push(elm);
}
};
var detachId = function(id,elm,doc) {
var elms, i;
if (id && elm && doc) {
if (doc._ids && doc._ids[id]) {
elms = doc._ids[id];
for (i=0;i<elms.length;i++) {
if (elms[i] === elm) {
elms.splice(i,1);
i--;
}
}
if (elms.length === 0) {
delete doc._ids[id];
}
}
}
};
var core = {
mapper: function(parent, filter, recursive) {
return function() {
return core.mapDOMNodes(parent, recursive !== false, filter);
};
},
// Returns Array
mapDOMNodes : function(parent, recursive, callback) {
function visit(parent, result) {
return Array.prototype.reduce.call(parent.childNodes, reducer, result);
}
function reducer(array, child) {
if (callback(child)) {
array.push(child);
}
if (recursive && child._childNodes) {
visit(child, array);
}
return array;
}
return visit(parent, []);
},
visitTree: function(root, callback) {
var cur = root; // TODO: Unroll this.
function visit(el) {
if (el) {
callback(el);
if (el._childNodes) {
var i = 0,
children = el._childNodes,
l = children.length;
for (i; i<l; i++) {
visit(children[i]);
}
}
}
}
visit(root);
},
markTreeReadonly: function(node) {
function markLevel(el) {
el._readonly = true;
// also mark attributes and their children read-only
if (el.attributes) {
var attributes = el.attributes, l = attributes.length, i=0;
attributes._readonly = true;
for (i; i<l; i++) {
core.visitTree(attributes[i], markLevel);
}
}
}
core.visitTree(node, markLevel);
}
};
// ExceptionCode
var INDEX_SIZE_ERR = core.INDEX_SIZE_ERR = 1,
DOMSTRING_SIZE_ERR = core.DOMSTRING_SIZE_ERR = 2,
HIERARCHY_REQUEST_ERR = core.HIERARCHY_REQUEST_ERR = 3,
WRONG_DOCUMENT_ERR = core.WRONG_DOCUMENT_ERR = 4,
INVALID_CHARACTER_ERR = core.INVALID_CHARACTER_ERR = 5,
NO_DATA_ALLOWED_ERR = core.NO_DATA_ALLOWED_ERR = 6,
NO_MODIFICATION_ALLOWED_ERR = core.NO_MODIFICATION_ALLOWED_ERR = 7,
NOT_FOUND_ERR = core.NOT_FOUND_ERR = 8,
NOT_SUPPORTED_ERR = core.NOT_SUPPORTED_ERR = 9,
INUSE_ATTRIBUTE_ERR = core.INUSE_ATTRIBUTE_ERR = 10,
// Node Types
ELEMENT_NODE = 1,
ATTRIBUTE_NODE = 2,
TEXT_NODE = 3,
CDATA_SECTION_NODE = 4,
ENTITY_REFERENCE_NODE = 5,
ENTITY_NODE = 6,
PROCESSING_INSTRUCTION_NODE = 7,
COMMENT_NODE = 8,
DOCUMENT_NODE = 9,
DOCUMENT_TYPE_NODE = 10,
DOCUMENT_FRAGMENT_NODE = 11,
NOTATION_NODE = 12;
var messages = core.exceptionMessages = { };
messages[INDEX_SIZE_ERR] = "Index size error";
messages[DOMSTRING_SIZE_ERR] = "DOMString size error";
messages[HIERARCHY_REQUEST_ERR] = "Hierarchy request error";
messages[WRONG_DOCUMENT_ERR] = "Wrong document";
messages[INVALID_CHARACTER_ERR] = "Invalid character";
messages[NO_DATA_ALLOWED_ERR] = "No data allowed";
messages[NO_MODIFICATION_ALLOWED_ERR] = "No modification allowed";
messages[NOT_FOUND_ERR] = "Not found";
messages[NOT_SUPPORTED_ERR] = "Not supported";
messages[INUSE_ATTRIBUTE_ERR] = "Attribute in use";
core.DOMException = function(code, message) {
this.code = code;
Error.call(this, core.exceptionMessages[code]);
this.message = core.exceptionMessages[code];
if(message) this.message = this.message + ": " + message;
if(Error.captureStackTrace) Error.captureStackTrace(this, core.DOMException);
};
core.DOMException.INDEX_SIZE_ERR = INDEX_SIZE_ERR;
core.DOMException.DOMSTRING_SIZE_ERR = DOMSTRING_SIZE_ERR;
core.DOMException.HIERARCHY_REQUEST_ERR = HIERARCHY_REQUEST_ERR;
core.DOMException.WRONG_DOCUMENT_ERR = WRONG_DOCUMENT_ERR;
core.DOMException.INVALID_CHARACTER_ERR = INVALID_CHARACTER_ERR;
core.DOMException.NO_DATA_ALLOWED_ERR = NO_DATA_ALLOWED_ERR;
core.DOMException.NO_MODIFICATION_ALLOWED_ERR = NO_MODIFICATION_ALLOWED_ERR;
core.DOMException.NOT_FOUND_ERR = NOT_FOUND_ERR;
core.DOMException.NOT_SUPPORTED_ERR = NOT_SUPPORTED_ERR;
core.DOMException.INUSE_ATTRIBUTE_ERR = INUSE_ATTRIBUTE_ERR;
inheritFrom(Error, core.DOMException, {
INDEX_SIZE_ERR : INDEX_SIZE_ERR,
DOMSTRING_SIZE_ERR : DOMSTRING_SIZE_ERR,
HIERARCHY_REQUEST_ERR : HIERARCHY_REQUEST_ERR,
WRONG_DOCUMENT_ERR : WRONG_DOCUMENT_ERR,
INVALID_CHARACTER_ERR : INVALID_CHARACTER_ERR,
NO_DATA_ALLOWED_ERR : NO_DATA_ALLOWED_ERR,
NO_MODIFICATION_ALLOWED_ERR : NO_MODIFICATION_ALLOWED_ERR,
NOT_FOUND_ERR : NOT_FOUND_ERR,
NOT_SUPPORTED_ERR : NOT_SUPPORTED_ERR,
INUSE_ATTRIBUTE_ERR : INUSE_ATTRIBUTE_ERR
});
core.NodeList = function NodeList(element, query) {
if (!query) {
// Non-live NodeList
if (Array.isArray(element)) {
Array.prototype.push.apply(this, element);
}
Object.defineProperties(this, {
_length: {value: element ? element.length : 0, writable:true}
});
} else {
Object.defineProperties(this, {
_element: {value: element},
_query: {value: query},
_snapshot: {writable: true},
_length: {value: 0, writable: true},
_version: {value: -1, writable: true}
});
this._update();
}
};
function lengthFromProperties(arrayLike) {
var max = -1;
for (var i in arrayLike) {
var asNumber = +i;
if (!isNaN(asNumber) && asNumber > max) {
max = asNumber;
}
}
return max + 1;
}
core.NodeList.prototype = {
_update: function() {
var i;
if (!this._element) {
this._length = lengthFromProperties(this);
} else {
if (this._version < this._element._version) {
var nodes = this._snapshot = this._query();
this._resetTo(nodes);
this._version = this._element._version;
}
}
},
_resetTo: function(array) {
var startingLength = lengthFromProperties(this);
for (var i = 0; i < startingLength; ++i) {
delete this[i];
}
for (var j = 0; j < array.length; ++j) {
this[j] = array[j];
}
this._length = array.length;
},
_toArray: function() {
if (this._element) {
this._update();
return this._snapshot;
}
return Array.prototype.slice.call(this);
},
get length() {
this._update();
return this._length || 0;
},
set length(length) {
return this._length;
},
item: function(index) {
this._update();
return this[index] || null;
},
toString: function() {
return '[ jsdom NodeList ]: contains ' + this.length + ' items';
}
};
Object.defineProperty(core.NodeList.prototype, 'constructor', {
value: core.NodeList,
writable: true,
configurable: true
});
core.DOMImplementation = function DOMImplementation(document, /* Object */ features) {
this._ownerDocument = document;
this._features = {};
if (features) {
for (var feature in features) {
if (features.hasOwnProperty(feature)) {
this.addFeature(feature.toLowerCase(), features[feature]);
}
}
}
};
core.DOMImplementation.prototype = {
get ownerDocument() { return this._ownerDocument },
removeFeature : function(feature, version) {
feature = feature.toLowerCase();
if (this._features[feature]) {
if (version) {
var j = 0,
versions = this._features[feature],
l = versions.length;
for (j; j<l; j++) {
if (versions[j] === version) {
versions.splice(j,1);
return;
}
}
} else {
delete this._features[feature];
}
}
},
addFeature: function(feature, version) {
feature = feature.toLowerCase();
if (version) {
if (!this._features[feature]) {
this._features[feature] = [];
}
if (version instanceof Array) {
Array.prototype.push.apply(this._features[feature], version);
} else {
this._features[feature].push(version);
}
}
},
hasFeature: function(/* string */ feature, /* string */ version) {
feature = (feature) ? feature.toLowerCase() : '';
var versions = (this._features[feature]) ?
this._features[feature] :
false;
if (!version && versions.length && versions.length > 0) {
return true;
} else if (typeof versions === 'string') {
return versions === version;
} else if (versions.indexOf && versions.length > 0) {
for (var i = 0; i < versions.length; i++) {
var found = versions[i] instanceof RegExp ?
versions[i].test(version) :
versions[i] === version;
if (found) { return true; }
}
return false;
} else {
return false;
}
}
};
var attrCopy = function(src, dest, fn) {
if (src.attributes) {
var attrs = src.attributes, i, l = attrs.length, attr, copied;
for (i=0;i<l;i++) {
attr = attrs[i];
// skip over default attributes
if (!attr.specified) {
continue;
}
// TODO: consider duplicating this code and moving it into level2/core
if (attr.namespaceURI) {
dest.setAttributeNS(attr.namespaceURI,
attr.nodeName,
attr.nodeValue);
var localName = attr.nodeName.split(':').pop();
copied = dest.getAttributeNodeNS(attr.namespaceURI, localName);
} else {
dest.setAttribute(attr.nodeName, attr.nodeValue);
copied = dest.getAttributeNode(attr.nodeName);
}
if (typeof fn == "function") {
fn(attr, copied);
}
}
}
return dest;
};
core.Node = function Node(ownerDocument) {
this._childNodes = new core.NodeList();
this._ownerDocument = ownerDocument;
this._attributes = new AttributeList(ownerDocument, this);
this._nodeName = null;
this._childrenList = null;
this._version = 0;
this._nodeValue = null;
this._parentNode = null;
this._nodeName = null;
this._readonly = false;
};
core.Node.ELEMENT_NODE = ELEMENT_NODE;
core.Node.ATTRIBUTE_NODE = ATTRIBUTE_NODE;
core.Node.TEXT_NODE = TEXT_NODE;
core.Node.CDATA_SECTION_NODE = CDATA_SECTION_NODE;
core.Node.ENTITY_REFERENCE_NODE = ENTITY_REFERENCE_NODE;
core.Node.ENTITY_NODE = ENTITY_NODE;
core.Node.PROCESSING_INSTRUCTION_NODE = PROCESSING_INSTRUCTION_NODE;
core.Node.COMMENT_NODE = COMMENT_NODE;
core.Node.DOCUMENT_NODE = DOCUMENT_NODE;
core.Node.DOCUMENT_TYPE_NODE = DOCUMENT_TYPE_NODE;
core.Node.DOCUMENT_FRAGMENT_NODE = DOCUMENT_FRAGMENT_NODE;
core.Node.NOTATION_NODE = NOTATION_NODE;
core.Node.prototype = {
ELEMENT_NODE : ELEMENT_NODE,
ATTRIBUTE_NODE : ATTRIBUTE_NODE,
TEXT_NODE : TEXT_NODE,
CDATA_SECTION_NODE : CDATA_SECTION_NODE,
ENTITY_REFERENCE_NODE : ENTITY_REFERENCE_NODE,
ENTITY_NODE : ENTITY_NODE,
PROCESSING_INSTRUCTION_NODE : PROCESSING_INSTRUCTION_NODE,
COMMENT_NODE : COMMENT_NODE,
DOCUMENT_NODE : DOCUMENT_NODE,
DOCUMENT_TYPE_NODE : DOCUMENT_TYPE_NODE,
DOCUMENT_FRAGMENT_NODE : DOCUMENT_FRAGMENT_NODE,
NOTATION_NODE : NOTATION_NODE,
get children() {
if (!this._childrenList) {
var self = this;
this._childrenList = new core.NodeList(this, function() {
return Array.prototype.filter.call(self._childNodes, function(node) {
return node.tagName;
});
});
}
return this._childrenList;
},
get nodeValue() {
return this._nodeValue;
},
set nodeValue(value) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR, 'Attempting to modify a read-only node');
}
this._nodeValue = value;
},
get parentNode() { return this._parentNode;},
get nodeName() {
var name = this._nodeName || this._tagName;
if (this.nodeType === ELEMENT_NODE &&
this._ownerDocument &&
this._ownerDocument._doctype &&
this._ownerDocument._doctype.name.toLowerCase().indexOf("html") !== -1)
{
return name.toUpperCase();
}
return name;
},
set nodeName(unused) { throw new core.DOMException();},
get attributes() { return this._attributes;},
get firstChild() {
return this._childNodes.length > 0 ? this._childNodes[0] : null;
},
set firstChild(unused) { throw new core.DOMException();},
get ownerDocument() { return this._ownerDocument;},
get readonly() { return this._readonly;},
get lastChild() {
var len = this._childNodes.length;
return len > 0 ? this._childNodes[len -1] : null;
},
set lastChild(unused) { throw new core.DOMException();},
get childNodes() {
return this._childNodes;
},
set childNodes(unused) { throw new core.DOMException();},
_indexOf: function(/*Node*/ child) {
if (!this._childNodes ||
!this._childNodes.length) {
return -1;
}
var currentNode, index = 0, children = this._childNodes;
while ((currentNode = children[index])) {
if (currentNode == child) {
break;
}
index++;
}
if (currentNode == child) {
return index;
}
return -1;
},
get nextSibling() {
// find this node's index in the parentNode, add one and call it a day
if (!this._parentNode || !this._parentNode._indexOf) {
return null;
}
var index = this._parentNode._indexOf(this);
if (index == -1 || index+1 >= this._parentNode._childNodes.length) {
return null;
}
return this._parentNode._childNodes[index+1] || null;
},
set nextSibling(unused) { throw new core.DOMException();},
get previousSibling() {
if (!this._parentNode || !this._parentNode._indexOf) {
return null;
}
var index = this._parentNode._indexOf(this);
if (index == -1 || index-1 < 0) {
return null;
}
return this._parentNode._childNodes[index-1] || null;
},
set previousSibling(unused) { throw new core.DOMException();},
/* returns Node */
insertBefore : function(/* Node */ newChild, /* Node*/ refChild) {
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR, 'Attempting to modify a read-only node');
}
// Adopt unowned children, for weird nodes like DocumentType
if (!newChild._ownerDocument) newChild._ownerDocument = this._ownerDocument;
// TODO - if (!newChild) then?
if (newChild._ownerDocument !== this._ownerDocument) {
throw new core.DOMException(WRONG_DOCUMENT_ERR);
}
if (newChild.nodeType && newChild.nodeType === ATTRIBUTE_NODE) {
throw new core.DOMException(HIERARCHY_REQUEST_ERR);
}
// search for parents matching the newChild
var current = this;
do {
if (current === newChild) {
throw new core.DOMException(HIERARCHY_REQUEST_ERR);
}
} while((current = current._parentNode));
// fragments are merged into the element
if (newChild.nodeType === DOCUMENT_FRAGMENT_NODE) {
var tmpNode, i = newChild._childNodes.length;
while (i-- > 0) {
tmpNode = newChild.removeChild(newChild.firstChild);
this.insertBefore(tmpNode, refChild);
}
} else if (newChild === refChild) {
return newChild;
} else {
// if the newChild is already in the tree elsewhere, remove it first
if (newChild._parentNode) {
newChild._parentNode.removeChild(newChild);
}
if (refChild == null) {
var refChildIndex = this._childNodes.length;
} else {
var refChildIndex = this._indexOf(refChild);
if (refChildIndex == -1) {
throw new core.DOMException(NOT_FOUND_ERR);
}
}
Array.prototype.splice.call(this._childNodes, refChildIndex, 0, newChild);
newChild._parentNode = this;
if (this._attached && newChild._attach) {
newChild._attach();
}
this._modified();
}
return newChild;
}, // raises(DOMException);
_modified: function() {
this._version++;
if (this._ownerDocument) {
this._ownerDocument._version++;
}
if (this._childrenList) {
this._childrenList._update();
}
},
_attrModified: function(name, value, oldValue) {
if (name == 'id' && this._attached) {
var doc = this._ownerDocument;
detachId(oldValue,this,doc);
attachId(value,this,doc);
}
},
/* returns Node */
replaceChild : function(/* Node */ newChild, /* Node */ oldChild){
this.insertBefore(newChild, oldChild);
return this.removeChild(oldChild);
}, //raises(DOMException);
/* returns void */
_attach : function() {
this._attached = true;
if (this.id) {
attachId(this.id,this,this._ownerDocument);
}
for (var i=0,len=this._childNodes.length;i<len;i++) {
if (this._childNodes[i]._attach) {
this._childNodes[i]._attach();
}
}
},
/* returns void */
_detach : function() {
var i, elms;
this._attached = false;
if (this.id) {
detachId(this.id,this,this._ownerDocument);
}
for (var i=0,len=this._childNodes.length;i<len;i++) {
this._childNodes[i]._detach();
}
},
/* returns Node */
removeChild : function(/* Node */ oldChild){
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
// TODO - if (!oldChild) then?
var oldChildIndex = this._indexOf(oldChild);
if (oldChildIndex == -1) {
throw new core.DOMException(NOT_FOUND_ERR);
}
Array.prototype.splice.call(this._childNodes, oldChildIndex, 1);
oldChild._parentNode = null;
this._modified();
oldChild._detach();
return oldChild;
}, // raises(DOMException);
/* returns Node */
appendChild : function(/* Node */ newChild) {
return this.insertBefore(newChild, null);
}, // raises(DOMException);
/* returns boolean */
hasChildNodes : function() {
return this._childNodes.length > 0;
},
/* returns Node */
cloneNode : function(/* bool */ deep, fn) {
var object = null;
switch (this.nodeType) {
case this.ELEMENT_NODE:
object = attrCopy(this,this._ownerDocument.createElement(this.tagName), fn);
break;
case this.TEXT_NODE:
object = attrCopy(this,this._ownerDocument.createTextNode(this.tagName));
object.nodeValue = this.nodeValue;
break;
case this.CDATA_SECTION_NODE:
object = this._ownerDocument.createCDATASection(this.tagName);
object.nodeValue = this.nodeValue;
break;
case this.ENTITY_REFERENCE_NODE:
var name = (this._entity) ? this._entity.name : this._entityName,
ref = this._ownerDocument.createEntityReference(name);
object = attrCopy(this, ref);
object.nodeValue = this.nodeValue;
break;
case this.ATTRIBUTE_NODE:
object = this._ownerDocument.createAttribute(this.name);
break;
case this.ENTITY_NODE:
var entity = this._ownerDocument.createEntityNode(this.name);
object = attrCopy(this, entity);
object.nodeValue = this.nodeValue;
object._publicId = this._publicId;
object._systemId = this._systemId;
object._notationName = this.notationName;
break;
case this.PROCESSING_INSTRUCTION_NODE:
var pi = this._ownerDocument.createProcessingInstruction(this._target,
this._data);
object = attrCopy(this, pi);
object.nodeValue = this.nodeValue;
break;
case this.COMMENT_NODE:
object = this._ownerDocument.createComment(this.tagName);
object.nodeValue = this.nodeValue;
break;
case this.DOCUMENT_NODE:
object = attrCopy(this, new core.Document());
// TODO: clone the doctype/entities/notations/etc?
break;
case this.DOCUMENT_TYPE_NODE:
object = attrCopy(this, new core.DocumentType());
object.nodeValue = this.nodeValue;
break;
case this.DOCUMENT_FRAGMENT_NODE:
object = this._ownerDocument.createDocumentFragment();
break;
case this.NOTATION_NODE:
object = this._ownerDocument.createNotationNode(this._name,
this._publicId,
this._systemId);
object = attrCopy(this,object);
object.nodeValue = this.nodeValue;
break;
default:
throw new core.DOMException(NOT_FOUND_ERR);
break;
}
if (typeof fn === "function") {
fn(this, object);
}
if (deep || this.nodeType === ATTRIBUTE_NODE) {
var clone = null;
for (var i=0,len=this._childNodes.length;i<len;i++)
{
clone = this._childNodes[i].cloneNode(true);
if (clone.nodeType === ATTRIBUTE_NODE) {
object.setAttributeNode(clone);
} else {
var readonly = object._readonly;
object._readonly = false;
object.appendChild(clone);
object._readonly = readonly;
}
}
}
return object;
},
/* returns void */
normalize: function() {
var prevChild, child, attr,i;
if (this._attributes && this._attributes.length) {
for (i=0;i<this._attributes.length;i++)
{
if (this._attributes[i]) {
attr = this._attributes[i].normalize();
}
}
}
for (i=0;i<this._childNodes.length;i++)
{
child = this._childNodes[i];
if (child.normalize) {
child.normalize();
}
// Level2/core clean off empty nodes
if (child.nodeValue === "") {
this.removeChild(child);
i--;
continue;
}
if (i>0) {
prevChild = this._childNodes[i-1];
if (child.nodeType === TEXT_NODE &&
prevChild.nodeType === TEXT_NODE)
{
// remove the child and decrement i
prevChild.appendData(child.nodeValue);
this.removeChild(child);
i--;
}
}
}
},
toString: function() {
var id = '';
if (this.id) {
id = '#' + this.id;
}
if (this.className) {
var classes = this.className.split(/\s+/);
for (var i = 0, len = classes.length; i < len; i++) {
id += '.' + classes[i];
}
}
return '[ ' + this.tagName + id + ' ]';
},
raise: function(type, message, data) {
var text = type + ": " + message;
if (data) {
if (data.exception) {
text = data.exception.stack;
} else {
text += ' - More:\n' + data;
}
}
if (type === "error") {
if (!this.errors) {
this.errors = [];
}
// TODO: consider using actual `Error` objects or `DOMException`s even..
var err = {
type : type,
message : message || "No message",
data : data || null
};
this.errors.push(err);
if (this._ownerDocument &&
this._ownerDocument.raise &&
this !== this._ownerDocument)
{
this._ownerDocument.raise(type, message, data);
}
}
}
};
core.NamedNodeMap = function NamedNodeMap(document) {
this._nodes = Object.create(null);
this._nsStore = {};
this.length = 0;
this._ownerDocument = document;
this._readonly = false;
};
core.NamedNodeMap.prototype = {
get readonly() { return this._readonly;},
get ownerDocument() { this._ownerDocument;},
exists : function(name) {
return (this._nodes[name] || this._nodes[name] === null) ? true : false;
},
/* returns Node */
getNamedItem: function(/* string */ name) {
return this._nodes[name] || null;
},
/* returns Node */
setNamedItem: function(/* Node */ arg) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
// arg is from a different document
if (arg && arg._ownerDocument !== this._ownerDocument) {
throw new core.DOMException(WRONG_DOCUMENT_ERR);
}
// if this argument is already in use..
if (arg && arg._ownerElement) {
throw new core.DOMException(INUSE_ATTRIBUTE_ERR);
}
var name = arg.name || arg.tagName;
var ret = this._nodes[name];
if (!ret) {
this.length++;
ret = null;
}
arg._specified = true;
this._nodes[name] = arg;
// Avoid overwriting prototype methods etc.:
if (this.hasOwnProperty(name) || !(name in this)) {
this[name] = arg;
}
return ret;
}, // raises: function(DOMException) {},
/* returns Node */
removeNamedItem: function(/* string */ name) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
if (!this._nodes[name]) {
throw new core.DOMException(NOT_FOUND_ERR);
}
var prev = this._nodes[name] || null;
delete this._nodes[name];
delete this[name];
this.length--;
return prev;
}, // raises: function(DOMException) {},
/* returns Node */
item: function(/* int */ index) {
var current = 0;
for (var member in this._nodes) {
if (current === index && this._nodes[member]) {
return this._nodes[member];
}
current++;
}
return null;
}
};
//
// For historical reasons, AttributeList objects must allow accessing
// attributes as if the object were an associative array. For
// instance, if `attributes` is an AttributeList object then
// `attributes.x` should evaluate to the attribute named `x` (which is
// not in any namespace). The AttributeList class uses the dollar
// symbol ($) to reduce the possibility of a clash between its field
// names and possible attribute names. For instance, if the method
// currently named `$set` were instead named `set` then it would not
// be possible to access an attribute named `set` through
// `attributes.set`. The dollar symbol is not valid in attribute names
// so `$set` cannot clash.
//
// Some fields do not get the $ because:
//
// * They are part of the API (e.g. `setNamedItem`, `length`), so they
// must be visible under a specific name.
//
// * Jsdom's code which traverses the DOM tree expects regularly named
// fields (e.g. `_parentNode`).
//
function AttributeList(document, parentNode) {
this._ownerDocument = document;
this._parentNode = parentNode;
this._readonly = false;
this._$ns_to_attrs = Object.create(null);
this._$name_to_attrs = Object.create(null);
this.length = 0;
}
AttributeList.prototype = {
_$reserved: [], // Initialized later
//
// Code internal to jsdom and which manipulates an AttributeList
// object should use the following methods rather than the methods
// that provide the NamedNodeMap interface.
//
// This method *ignores* namespaces. This is *not* the same thing as
// requesting an attribute with a null namespace.
$getNoNS: function (name) {
var attrs = this._$name_to_attrs[name];
if (!attrs) {
return null;
}
return attrs[0] || null;
},
$getNode: function (namespace, localName) {
var attrs = this._$ns_to_attrs[namespace];
if (!attrs) {
return null;
}
var ret = attrs[localName];
if (!ret) {
return null;
}
return ret;
},
// This method *ignores* namespaces. This is *not* the same thing as
// requesting an attribute with a null namespace.
$setNoNS: function (name, value) {
var attr = this.$getNoNS(name);
if (!attr) {
this.$set(name, value);
return;
}
var prev_val = attr.value;
attr.value = value;
attr._specified = true;
this._parentNode._attrModified(attr.name, attr.value, prev_val);
this._parentNode._modified();
},
$set: function (localName, value, name, prefix, namespace) {
if (this._readonly) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
if (name === undefined) {
name = localName;
}
if (prefix === undefined) {
prefix = null;
}
if (namespace === undefined) {
namespace = null;
}
var prev_attr = this.$getNode(namespace, localName);
var attr;
var prev_val = null;
if (prev_attr) {
prev_val = prev_attr.value;
prev_attr._prefix = prefix;
prev_attr.value = value;
attr = prev_attr;
attr._specified = true;
this._parentNode._attrModified(attr.name, attr.value, prev_val);
this._parentNode._modified();
}
else {
attr = this._ownerDocument.createAttribute(name);
attr._ownerElement = this._parentNode;
attr.value = value;
attr._namespaceURI = namespace;
attr._prefix = prefix;
attr._localName = localName;
attr._parentNode = this._parentNode;
attr._created = true;
this.$setNode(attr);
// $setNode calls the parent node methods.
}
},
$setNode: function (attr) {
if (this._readonly) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
if (attr.nodeType !== ATTRIBUTE_NODE) {
throw new core.DOMException(HIERARCHY_REQUEST_ERR);
}
if (attr._ownerDocument !== this._ownerDocument) {
throw new core.DOMException(WRONG_DOCUMENT_ERR);
}
if (attr._parentNode && attr._parentNode !== this._parentNode) {
throw new core.DOMException(INUSE_ATTRIBUTE_ERR);
}
var localName = attr._localName;
var name = attr.name;
var prefix = attr._prefix;
var namespace = attr._namespaceURI;
if (name === undefined) {
name = localName;
}
if (prefix === undefined) {
prefix = null;
}
if (namespace === undefined) {
namespace = null;
}
var prev_attr = this.$getNode(namespace, localName);
var prev_val = null;
if (prev_attr) {
prev_val = prev_attr.value;
// Remove the old attribute
this._$onlyRemoveNode(prev_attr);
}
attr._parentNode = this._parentNode;
attr._ownerElement = this._parentNode;
attr._specified = true;
var attrs = this._$ns_to_attrs[namespace];
if (!attrs) {
attrs = this._$ns_to_attrs[namespace] = Object.create(null);
}
attrs[localName] = attr;
attrs = this._$name_to_attrs[name];
if (!attrs) {
attrs = this._$name_to_attrs[name] = [attr];
}
else {
attrs.push(attr);
}
// Only attributes in the null namespace can be set this way.
if (namespace === null) {
// Make the node a field on this object but ONLY if it does not
// clash with the reserved names.
if (this._$reserved.indexOf(name) === -1) {
this[name] = attr;
}
}
this[this.length] = attr;
this.length++;
this._parentNode._attrModified(attr.name, attr.value, prev_val);
this._parentNode._modified();
return prev_attr;
},
// This method *ignores* namespaces. This is *not* the same thing as
// requesting an attribute with a null namespace.
$removeNoNS: function (name) {
var attr = this.$getNoNS(name);
return attr ? this.$removeNode(attr) : null;
},
$remove: function (namespace, localName) {
var attr = this.$getNode(namespace, localName);
return attr ? this.$removeNode(attr) : null;
},
/* Only removes the node, and does not add a default value. */
_$onlyRemoveNode: function (attr) {
var namespace = attr._namespaceURI;
var localName = attr._localName;
var attrs = this._$ns_to_attrs[namespace];
if (!attrs) {
return null;
}
var found_attr = attrs[localName];
if (found_attr !== attr) {
return null;
}
if (this._readonly) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
attr._ownerElement = null;
attr._parentNode = null;
delete attrs[localName];
attrs = this._$name_to_attrs[attr.name];
attrs.splice(attrs.indexOf(attr), 1);
var ix = Array.prototype.indexOf.call(this, attr);
// Splice also modifies length.
Array.prototype.splice.call(this, ix, 1);
if (this[attr.name] === attr) {
delete this[attr.name];
}
this._parentNode._attrModified(attr.name);
this._parentNode._modified();
return attr;
},
$removeNode: function (attr) {
if (!this._$onlyRemoveNode(attr)) {
return null;
}
// set default value if available
var doc = this._ownerDocument;
if (doc && doc._doctype && doc._doctype.name.toLowerCase() !== "html") {
var elem =
doc._doctype._attributes.getNamedItem(this._parentNode.nodeName);
if (elem) {
var defaultValue = elem.attributes.getNamedItemNS(attr._namespaceURI,
attr._localName);
if (defaultValue) {
this.$set(attr._localName, defaultValue.value, attr.name, attr._prefix,
attr._namespaceURI);
var new_attr = this.$getNode(attr._namespaceURI, attr._localName);
new_attr._specified = false;
}
}
}
return attr;
},
// Although http://dom.spec.whatwg.org/#concept-element-attribute
// does not specify that the attributes field on an Element should
// support NamedNodeMap, in practice browsers still support this
// interface so we should support it for compatibility.
getNamedItem: function (name) {
return this.getNamedItemNS(null, name);
},
removeNamedItem: function (name) {
return this.removeNamedItemNS(null, name);
},
item: function (i) {
return this[i];
},
getNamedItemNS: function (namespaceURI, localName) {
if (namespaceURI === "") {
namespaceURI = null;
}
return this.$getNode(namespaceURI, localName);
},
removeNamedItemNS: function (namespaceURI, localName) {
var ret = this.$remove(namespaceURI, localName);
if (ret === null) {
throw new core.DOMException(NOT_FOUND_ERR);
}
return ret;
}
};
// Alias these methods.
AttributeList.prototype.setNamedItem = AttributeList.prototype.$setNode;
AttributeList.prototype.setNamedItemNS = AttributeList.prototype.$setNode;
(function () {
// Construct the list of reserved attribute names from a temporarily
// created AttributeList and from the chain of prototypes. We need
// this because JavaScript code running an a browser expects to be
// able to do el.attributes.x to get the value of the attribute "x"
// on an element. Unfortunately, JavaScript *currently* does not
// allow us to elegantly provide such functionality without risking
// a clash with the fields and methods set on the AttributeList
// object. Hence we need a list of reserved field names.
var reserved = Object.keys(new AttributeList());
var prototype = AttributeList.prototype;
while (prototype) {
reserved = reserved.concat(Object.getOwnPropertyNames(prototype));
prototype = Object.getPrototypeOf(prototype);
}
AttributeList.prototype._$reserved = reserved;
})();
core.AttributeList = AttributeList;
core.NotationNodeMap = function NotationNodeMap(document) {
core.NamedNodeMap.call(this, document);
this._readonly = false;
for (var i=1;i<arguments.length;i++) {
this.setNamedItem(arguments[i]);
}
this._readonly = true;
};
inheritFrom(core.NamedNodeMap, core.NotationNodeMap);
core.EntityNodeMap = function EntityNodeMap(document) {
core.NamedNodeMap.call(this,document);
this._readonly = false;
var i = 1, l = arguments.length;
for (i=1; i<l; i++) {
this.setNamedItem(arguments[i]);
}
core.markTreeReadonly(this);
};
inheritFrom(core.NamedNodeMap, core.EntityNodeMap);
core.Element = function Element(document, tagName) {
this._ownerDocument = document;
core.Node.call(this, document);
this._nodeName = tagName;
this._tagName = tagName;
};
inheritFrom(core.Node, core.Element, {
get nodeValue() { return null;},
set nodeValue(value) { /* do nothing */ },
get tagName() {
if (this.nodeType === ELEMENT_NODE &&
this._ownerDocument &&
this._ownerDocument._doctype &&
this._ownerDocument._doctype.name.toLowerCase().indexOf("html") !== -1)
{
return this.nodeName.toUpperCase();
}
return this.nodeName;
},
nodeType : ELEMENT_NODE,
get attributes() {
return this._attributes;
},
/* returns string */
getAttribute: function(/* string */ name) {
var attribute = this._attributes.$getNode(null, name);
if (attribute) {
return attribute.value;
}
return null;
},
setAttribute: function(/* string */ name, /* string */ value) {
if (this._ownerDocument) {
var attr = this._ownerDocument.createAttribute(name);
attr.value = value;
attr._ownerElement = this;
attr._created = true;
this._attributes.$setNode(attr);
}
}, //raises: function(DOMException) {},
removeAttribute: function(/* string */ name) {
this._attributes.$remove(null, name);
}, // raises: function(DOMException) {},
/* returns Attr */
getAttributeNode: function(/* string */ name) {
return this._attributes.$getNode(null, name);
},
/* returns Attr */
setAttributeNode: function(/* Attr */ newAttr) {
var prevNode = this._attributes.$getNode(null, newAttr.name);
if (prevNode) {
prevNode._ownerElement = null;
}
newAttr._ownerElement = this;
this._attributes.$setNode(newAttr);
return (prevNode && prevNode.specified) ? prevNode : null;
}, // raises: function(DOMException) {},
/* returns Attr */
removeAttributeNode: function(/* Attr */ oldAttr) {
var ret = this._attributes.$removeNode(oldAttr);
if (ret !== null) {
return ret;
}
throw new core.DOMException(NOT_FOUND_ERR);
}, //raises: function(DOMException) {},
/* returns NodeList */
getElementsByTagName: function(/* string */ name) {
name = name.toLowerCase();
function filterByTagName(child) {
child = (child.nodeType === ENTITY_REFERENCE_NODE) ?
child._entity :
child;
if (child.nodeName && child.nodeType === ELEMENT_NODE) {
return name === "*" || (child.nodeName.toLowerCase() === name);
}
return false;
}
return new core.NodeList(this._ownerDocument || this, core.mapper(this, filterByTagName, true));
},
});
core.DocumentFragment = function DocumentFragment(document) {
core.Node.call(this, document);
this._nodeName = this._tagName = "#document-fragment";
};
inheritFrom(core.Node, core.DocumentFragment, {
nodeType : DOCUMENT_FRAGMENT_NODE,
get nodeValue() { return null;},
set nodeValue(unused) { /* do nothing */ },
get attributes() { return null;}
});
core.ProcessingInstruction = function ProcessingInstruction(document, target, data) {
this._ownerDocument = document;
core.Node.call(this, document);
this._nodeName = target;
this._tagName = target;
this._target = target;
this._nodeValue = data;
}
inheritFrom(core.Node, core.ProcessingInstruction, {
nodeType : PROCESSING_INSTRUCTION_NODE,
get target() { return this._target;},
set target(value) { throw new core.DOMException(1);},
get nodeValue() { return this._nodeValue;},
set nodeValue(value) { this._nodeValue = value},
get data() { return this._nodeValue;},
set data(unused) { throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);},
get attributes() { return null;}
});
core.Document = function Document(options) {
if (!options) {
options = {};
}
else if (typeof options == 'string') {
options = {
name: options
};
}
core.Node.call(this, "#document");
this._nodeName = this._tagName = "#document";
this._contentType = options.contentType || "text/xml";
this._doctype = options._doctype;
this._implementation = options.implementation || new (core.DOMImplementation)();
this._documentElement = null;
this._ids = Object.create(null);
this._attached = true;
this._ownerDocument = this;
this._readonly = false;
};
var tagRegEx = /[^\w:\d_\.-]+/i;
var entRegEx = /[^\w\d_\-&;]+/;
var invalidAttrRegEx = /[\s"'>/=\u0000-\u001A]/;
inheritFrom(core.Node, core.Document, {
nodeType : DOCUMENT_NODE,
_elementBuilders : { },
_defaultElementBuilder: function(document, tagName) {
return new core.Element(document, tagName);
},
get contentType() { return this._contentType;},
get doctype() { return this._doctype || null;},
set doctype(doctype) { this._doctype = doctype;},
get documentElement() {
if (this._documentElement) {
return this._documentElement;
} else {
var children = this._childNodes, len = this._childNodes.length, i=0;
for (i;i<len;i++) {
if (children[i].nodeType === ELEMENT_NODE) {
this._documentElement = children[i];
return children[i];
}
}
return null;
}
},
get implementation() { return this._implementation;},
set implementation(implementation) { this._implementation = implementation;},
get nodeName() { return '#document'; },
get tagName() {
return null;
},
get nodeValue() { return null; },
set nodeValue(unused) { /* noop */ },
get attributes() { return null;},
get ownerDocument() { return null;},
get readonly() { return this._readonly;},
/* returns Element */
_createElementNoTagNameValidation: function(/*string*/ tagName) {
var lower = tagName.toLowerCase();
var element = (this._elementBuilders[lower] || this._defaultElementBuilder)(this, tagName);
// Check for and introduce default elements
if (this._doctype && this._doctype._attributes && this._doctype.name.toLowerCase() !== "html") {
var attrElement = this._doctype._attributes.getNamedItem(tagName);
if (attrElement && attrElement._childNodes) {
var attrs = attrElement.attributes;
var attr, len = attrs.length, defaultAttr;
for (var i = 0; i < len; i++) {
defaultAttr = attrs[i];
if (defaultAttr) {
attr = this.createAttribute(defaultAttr.name);
attr.value = defaultAttr.value;
element.setAttributeNode(attr);
attr._specified = false;
attr._created = true;
}
}
}
}
element._created = true;
return element;
},
/* returns Element */
createElement: function(/* string */ tagName) {
tagName = String(tagName);
var c = [];
if (tagName.length === 0 || (c = tagName.match(tagRegEx))) {
throw new core.DOMException(INVALID_CHARACTER_ERR, 'Invalid character in tag name: ' + c.pop());
}
return this._createElementNoTagNameValidation(tagName);
}, //raises: function(DOMException) {},
/* returns DocumentFragment */
createDocumentFragment: function() {
return new core.DocumentFragment(this);
},
/* returns Text */
createTextNode: function(/* string */ data) {
return new core.Text(this,data);
},
/* returns Comment */
createComment: function(/* string */ data) {
return new core.Comment(this,data);
},
/* returns CDATASection */
createCDATASection: function(/* string */ data) {
if (this._doctype && this._doctype.name === "html") {
throw new core.DOMException(NOT_SUPPORTED_ERR);
}
return new core.CDATASection(this,data);
}, // raises: function(DOMException) {},
/* returns ProcessingInstruction */
createProcessingInstruction: function(/* string */ target,/* string */ data) {
if (this._doctype && this._doctype.name === "html") {
throw new core.DOMException(NOT_SUPPORTED_ERR);
}
if (target.match(tagRegEx) || !target || !target.length) {
throw new core.DOMException(INVALID_CHARACTER_ERR);
}
return new core.ProcessingInstruction(this, target, data);
}, // raises: function(DOMException) {},
/* returns Attr */
createAttribute: function(/* string */ name) {
if (!name || !name.length || name.match(invalidAttrRegEx) ) {
throw new core.DOMException(INVALID_CHARACTER_ERR, "attribute name: " + name);
}
return new core.Attr(this, name,false);
}, // raises: function(DOMException) {},
/* returns EntityReference */
createEntityReference: function(/* string */ name) {
if (this._doctype && this._doctype.name === "html") {
throw new core.DOMException(NOT_SUPPORTED_ERR);
}
name = name.replace(/[&;]/g,"");
if (!name || !name.length) {
throw new core.DOMException(INVALID_CHARACTER_ERR);
}
if (name.match(tagRegEx)) {
throw new core.DOMException(INVALID_CHARACTER_ERR);
}
var entity;
if (this._doctype && this._doctype.entities) {
entity = this._doctype.entities.getNamedItem(name);
} else {
entity = null;
}
var ref = new core.EntityReference(this, entity);
ref._entityName = name;
return ref;
}, //raises: function(DOMException) {},
/* returns Entity */
createEntityNode : function(/* string */ name)
{
if (name.match(entRegEx) || !name || !name.length) {
throw new core.DOMException(INVALID_CHARACTER_ERR);
}
var ret = new core.Entity(this, name);
ret._readonly = false;// TODO: fix me please.
for (var i=1;i<arguments.length;i++)
{
ret.appendChild(arguments[i]);
}
core.markTreeReadonly(ret);
return ret;
},
/* returns Notation */
createNotationNode : function(/* string */ name,/* string */ publicId,/* string */ systemId)
{
if (name.match(entRegEx) || !name || !name.length) {
throw new core.DOMException(INVALID_CHARACTER_ERR);
}
var ret = new core.Notation(this, name, publicId, systemId);
ret._readonly = false;// TODO: fix me please.
for (var i=3;i<arguments.length;i++)
{
ret.appendChild(arguments[i]);
}
core.markTreeReadonly(ret);
return ret;
},
appendChild : function(/* Node */ arg) {
if (this.documentElement && arg.nodeType == ELEMENT_NODE) {
throw new core.DOMException(HIERARCHY_REQUEST_ERR);
}
return core.Node.prototype.appendChild.call(this, arg);
},
removeChild : function(/* Node */ arg) {
var ret = core.Node.prototype.removeChild.call(this, arg);
if (arg == this._documentElement) {
this._documentElement = null;// force a recalculation
}
return ret;
},
/* returns NodeList */
getElementsByTagName: function(/* string */ name) {
function filterByTagName(child) {
if (child.nodeType && child.nodeType === ENTITY_REFERENCE_NODE)
{
child = child._entity;
}
if (child.nodeName && child.nodeType === ELEMENT_NODE)
{
if (name === "*") {
return true;
// case insensitivity for html
} else if (child._ownerDocument && child._ownerDocument._doctype &&
//child._ownerDocument._doctype.name === "html" &&
child.nodeName.toLowerCase() === name.toLowerCase())
{
return true;
} else if (child.nodeName.toLowerCase() === name.toLowerCase()) {
return true;
}
}
return false;
}
return new core.NodeList(this.documentElement || this, core.mapper(this, filterByTagName, true));
}
});
core.CharacterData = function CharacterData(document, value) {
core.Node.call(this, document);
this._nodeValue = value + "";
};
inheritFrom(core.Node, core.CharacterData, {
get data() { return this._nodeValue;},
set data(data) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
this._nodeValue = data;
},
/* returns int */
get length() { return this._nodeValue.length || 0;},
/* returns string */
substringData: function(/* int */ offset, /* int */ count) {
if (count < 0 || offset < 0 || offset > this._nodeValue.length) {
throw new core.DOMException(INDEX_SIZE_ERR);
}
return (this._nodeValue.length < offset + count) ?
this._nodeValue.substring(offset) :
this._nodeValue.substring(offset, offset+count);
}, // raises: function(DOMException) {},
/* returns string */
appendData: function(/* string */ arg) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
this._nodeValue+=arg;
return this._nodeValue;
}, // raises: function(DOMException) {},
/* returns string */
insertData: function(/* int */ offset, /* string */ arg) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
if (offset < 0 || offset > this._nodeValue.length) {
throw new core.DOMException(INDEX_SIZE_ERR);
}
var start = this._nodeValue.substring(0,offset);
var end = this._nodeValue.substring(offset);
this._nodeValue = start + arg + end;
}, //raises: function(DOMException) {},
/* returns void */
deleteData: function(/* int */ offset, /* int */ count) {
// readonly
if (this._readonly === true) {
throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
}
if (offset < 0