sub
Version:
A subset of the DOM environment for running Rule.js on the server
483 lines (461 loc) • 16.6 kB
JavaScript
;
var Node = require('./Node').Node;
var initializeNode = require('./Node').initializeNode;
var DOMException = require('./DOMException').DOMException;
var createDOMException = require('./DOMException').createDOMException;
var Text = require('./Text').Text;
// Helpers
/* As specified at:
* http://www.w3.org/TR/2008/REC-xml-20081126/#d0e804
*/
var attributeNameStartChar, attributeNameChar, attributeNameRegExp, attributeString;
attributeNameStartChar = "([:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD])";
attributeNameChar = "(" + attributeNameStartChar + "|[\\-\\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040])";
attributeNameRegExp = new RegExp("^" + attributeNameStartChar + attributeNameChar + "*$");
attributeString = function (string) {
string = String(string);
if (!attributeNameRegExp.test(string)) {throw createDOMException(DOMException.INVALID_CHARACTER_ERR); }
return string.toLowerCase();
};
var tagNameStartChar, tagNameChar, tagNameRegExp, tagString;
tagNameStartChar = "([:A-Z_a-z])";
tagNameChar = "(" + tagNameStartChar + "|[-.0-9])";
tagNameRegExp = new RegExp("^" + tagNameStartChar + tagNameChar + "*$");
tagString = function (string) {
string = String(string);
if (!tagNameRegExp.test(string)) {throw createDOMException(DOMException.INVALID_CHARACTER_ERR); }
return string.toUpperCase();
};
var asHTML, voidElements, escape, escapeAttr, escapeScript;
escape = function (text) {
return text
.replace(/</g, '<')
.replace(/>/g, '>');
};
escapeAttr = function (text) {
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
escapeScript = function (text) {
return text
.replace(/<\//g, '\\u003C/');
};
// List of elements that are not allowed to have children
voidElements = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
asHTML = function (node) {
var key;
if (node instanceof Text) {
if (node.parentElement && node.parentElement.tagName === 'SCRIPT') {
return escapeScript(node.data);
} else {
return escape(node.data);
}
}
if (node instanceof Element) {
var tag, attributes, attribute, index;
tag = node.tagName.toLowerCase();
attributes = '';
for (index = 0; index < node.attributes.length; index++) {
attribute = node.attributes[index];
attributes += ' '+attribute.name+'="'+escapeAttr(attribute.value)+'"';
}
if (voidElements.indexOf(tag) !== -1 && node.childNodes.length === 0) {
return '<'+tag+attributes+'/>';
} else {
return '<'+tag+attributes+'>'+node.innerHTML+'</'+tag+'>';
}
}
};
/*
* Element
*/
var Element, initializeElement, createElement;
Element = (function () {
// Extension
(function (child, parent) {
function Element() {
this.constructor = child;
}
Element.prototype = parent.prototype;
child.prototype = new Element();
}(Element, Node));
// Prototype
(function () {
// These properties within defineProperties should not be in the element
// prototype, this is a speed optimization, as the call to defineProperties
// is expensive when called on every element initialization.
// On the actual DOM, these properties belong to the object itself,
// not the prototype.
Object.defineProperties(Element.prototype, {
'firstElementChild': {
configurable: true,
get: function () {
var index, child;
for (index = 0; index < this.childNodes.length; index++) {
child = this.childNodes[index];
if (child instanceof Element) {
return child;
}
}
return null;
}
},
'nextElementSibling': {
configurable: true,
get: function () {
var index, sibling, siblings;
if (this.parentNode) {
siblings = this.parentNode.childNodes;
for (index = siblings.indexOf(this) + 1; index < siblings.length; index++) {
sibling = siblings[index];
if (sibling instanceof Element) {
return sibling;
}
}
}
return null;
}
},
'innerHTML': {
configurable: true,
get: function () {
var index, html;
html = '';
for (index = 0; index < this.childNodes.length; index++) {
html += asHTML(this.childNodes[index]);
}
return html;
},
set: function (string) {
while (this.firstChild) {
this.removeChild(this.firstChild);
}
parser(string).forEach(
function (element) {
this.insertBefore(element);
}, this);
return string;
}
},
'outerHTML': {
configurable: true,
get: function () {
return asHTML(this);
},
set: function (string) {
parser(string).forEach(
function (element) {
this.parentElement.insertBefore(element, this);
}, this);
this.parentElement.removeChild(this);
return string;
}
},
'className': {
configurable: true,
get: function () {
return this.getAttribute('class') || '';
},
set: function (string) {
return this.setAttribute('class', string);
}
},
'id': {
configurable: true,
get: function () {
return this.getAttribute('id') || '';
},
set: function (string) {
return this.setAttribute('id', string);
}
}
});
Element.prototype.hasAttributes = function () {
return this.attributes.length > 0;
};
Element.prototype.getAttribute = function (key) {
var attribute, index;
if (key === null || key === undefined) { return null; }
for (index = 0; index < this.attributes.length; index++) {
attribute = this.attributes[index];
if (attribute.name === key){
return attribute.value;
}
}
return null;
};
Element.prototype.setAttribute = function (key, value) {
var attribute, index;
value = String(value);
key = attributeString(key);
for (index = 0; index < this.attributes.length; index++) {
attribute = this.attributes[index];
if (attribute.name === key){
attribute.value = value;
return;
}
}
this.attributes.push({
name: key,
value: value
});
};
Element.prototype.removeAttribute = function (key) {
var attribute, index;
key = attributeString(key);
for (index = 0; index < this.attributes.length; index++) {
attribute = this.attributes[index];
if (attribute.name === key){
this.attributes.splice(index, 1);
return;
}
}
};
Element.prototype.getElementsByClassName = function (string) {
var child, index, childClassName, classes, hasClasses, tree, matches;
hasClasses = function (childClasses, matchClasses) {
var index;
for (index = 0; index < matchClasses.length; index++) {
if (childClasses.indexOf(matchClasses[index]) === -1) {
return false;
}
}
return true;
};
// Initialization
classes = string.split(' ');
matches = [];
tree = [];
// Flatten node tree before filtering
Array.prototype.push.apply(tree, this.childNodes);
for (index = 0; index < tree.length; index++) {
child = tree[index];
if (!(child instanceof Element)) {
continue;
}
Array.prototype.push.apply(tree, child.childNodes);
childClassName = child.getAttribute('class');
if (childClassName && hasClasses(childClassName.split(' '), classes)) {
matches.push(child);
}
}
return matches;
};
Element.prototype.getElementsByTagName = function (string) {
var child, index, tag, tree, matches;
// Initialization
tag = string.toUpperCase();
matches = [];
tree = [];
// Flatten node tree before filtering
Array.prototype.push.apply(tree, this.childNodes);
for (index = 0; index < tree.length; index++) {
child = tree[index];
if (!(child instanceof Element)) {
continue;
}
Array.prototype.push.apply(tree, child.childNodes);
if (child instanceof Element && child.tagName === tag) {
matches.push(child);
}
}
return matches;
};
Element.prototype.querySelector = function (string) {
return this.querySelectorAll(string)[0];
};
Element.prototype.querySelectorAll = function (string) {
var elements, queries, trim,
parseSelector, parseSelectors, parseQuery, parseQueries,
checkTag, checkIDs, checkClasses, checkAttributes, check,
selectChildren, select;
trim = function (string) {
return string.trim();
};
parseSelector = function (string) {
var nameMatch, selector, nextMatch, match;
nameMatch = "([-]?[_a-zA-Z]+[_a-zA-Z0-9-]*)";
selector = {
combinator: '',
tag: '',
ids: [],
classes: [],
attributes: []
};
// This is a side-effecty helper function to reduce duplicate code.
nextMatch = function (regExp) {
var match;
if (regExp.test(string)) {
match = regExp.exec(string).slice(1);
string = string.replace(regExp, '');
}
return match;
};
// Combinator and tag must come first so they are checked before the loop
if (match = nextMatch(new RegExp("^([+~> ])\\s*"))) {
selector.combinator = match[0];
}
if (match = nextMatch(new RegExp("^"+nameMatch))) {
selector.tag = match[0];
}
// Within each selector, parse out classes, ids, and attributes
while (string !== '') {
if (match = nextMatch(new RegExp("^#"+nameMatch)))
{ selector.ids.push(match[0]); } else
if (match = nextMatch(new RegExp("^\\."+nameMatch)))
{ selector.classes.push(match[0]); } else
if (match = nextMatch(new RegExp("^\\["+nameMatch+"(?:([~|^$*]?[=])['\"]?"+nameMatch+"['\"]?)?\\]")))
{ selector.attributes.push({key: match[0], comparator: match[1], value: match[2]}); } else
{ throw createDOMException(DOMException.SYNTAX_ERR); }
}
return selector;
};
parseSelectors = function (strings) {
return strings.reduce(function (selectors, string) {
selectors.push(parseSelector(string));
return selectors;
}, []);
};
parseQuery = function (string) {
// Split selectors so there is a combinator, then selector
return parseSelectors(
string
.match(/\s*([+~> ]?\s*[^+~> ]+)/g)
.map(trim)
);
};
parseQueries = function (strings) {
return strings.reduce(function (queries, string) {
queries.push(parseQuery(string));
return queries;
}, []);
};
checkTag = function(selector, element) {
return !(selector.tag && selector.tag.toUpperCase() !== element.tagName);
};
checkIDs = function(selector, element) {
return !(selector.ids.length > 1 || (selector.ids.length === 1 && selector.ids[0] !== element.getAttribute('id')));
};
checkClasses = function(selector, element) {
var classes, index;
classes = (element.getAttribute('class') || '').split(' ').filter(function(string){return string;});
for (index = 0; index < selector.classes.length; index++) {
if (classes.indexOf(selector.classes[index]) === -1) { return false; }
}
return true;
};
checkAttributes = function(selector, element) {
var attribute, value, index;
for (index = 0; index < selector.attributes.length; index++) {
attribute = selector.attributes[index];
value = element.getAttribute(attribute.key);
if (value === null) { return false; }
switch (attribute.comparator) {
// Exactly equal to query value
case '=':
if (attribute.value !== value) { return false; }
break;
// One of space seperated words is equal to query value
case '~=':
if (value.split(' ').indexOf(attribute.value) === -1) { return false; }
break;
// One of hyphen seperated words is equal to query value
case '|=':
if (value.split('-').indexOf(attribute.value) === -1) { return false; }
break;
// Starts with query value
case '^=':
if (value.indexOf(attribute.value) !== 0) { return false; }
break;
// Ends with query value
case '$=':
if (value.indexOf(attribute.value)+attribute.value.length-value.length !== 0) { return false; }
break;
// Has query value somewhere
case '*=':
if (value.indexOf(attribute.value) === -1) { return false; }
break;
}
}
return true;
};
check = function (selector, element) {
if (element instanceof Element) {
return (checkTag(selector, element) && checkIDs(selector, element) && checkClasses(selector, element) && checkAttributes(selector, element));
}
};
// Given a selector and an element, find new elements that match the selector from the current element
selectChildren = function (selector, element) {
var selected, childIndex, child;
selected = [];
if (selector.combinator === '' || selector.combinator === '>') {
for (childIndex = 0; childIndex < element.childNodes.length; childIndex++) {
child = element.childNodes[childIndex];
if (check(selector, child)) {
selected.push(child);
}
if (selector.combinator === '' && child.childNodes.length) {
selected.push.apply(selected, selectChildren(selector, child));
}
}
} else if (element.nextSibling && (selector.combinator === '+' || selector.combinator === '~')) {
child = element.nextSibling;
if (check(selector, child)) {
selected.push(child);
}
if (selector.combinator === '~' || child.nodeType === Node.TEXT_NODE) {
selected.push.apply(selected, selectChildren(selector, child));
}
}
return selected;
};
// Given a query of selectors, apply selectors against elements and find matching children
select = function (query, elements) {
var selector, selected;
selector = query[0];
selected = elements.reduce(function (selected, element) {
return selected.concat(selectChildren(selector, element));
}, []);
if (query.length > 1) {
selected = select(query.slice(1), selected);
}
return selected;
};
string = String(string);
if (!string) { throw createDOMException(DOMException.SYNTAX_ERR); }
queries = parseQueries(
string
.split(',')
.map(trim)
);
elements = [this];
// Iterate through each query and return unique results
return queries.reduce(function (selected, query) {
return selected.concat(select(query, elements).filter(
function (element) { return selected.indexOf(element) === -1; }
));
}, []);
};
}());
//Constructor
function Element() {
throw new TypeError('Illegal constructor');
}
return Element;
}());
var parser;
initializeElement = function(tagName) {
// Required here because it would cause infinite loop if required above
if (!parser) {
parser = require('../utils/parser');
}
initializeNode.apply(this, arguments);
this.attributes = [];
this.nodeType = Node.ELEMENT_NODE;
this.tagName = tagString(tagName);
};
exports.Element = Element;
exports.initializeElement = initializeElement;