Here is the enhanced getEBCS. As mentioned, the only pseudo-selectors
supported are first-child and last-child. The others should be
trivial to add without any severe impact on performance. As no
attempt is made to validate selectors, passing unsupported types will
result in anything from the wrong results to script errors.
Tested with the MooTools "SlickSpeed" page in IE7, FireFox, Opera 9
and Windows Safari Beta. That last one threw a monkey wrench into the
works in that it doesn't support the child pseudo-selectors properly
with XPath. As the design is split between browsers that support
XPath and those that don't, XPath is completely disabled when this
"feature" is detected. It would be worth changing it to choose based
on the selector as Safari appears to have the fastest XPath
implementation. Luckily, it is relatively fast with the DOM too.
Certainly there are more optimizations that could be made. I don't
see any urgency there as even IE wins most of the tests as it sits.
All tests were done with the most inefficient implementations of gEBI
and gEBTN. All of the code was wrapped in an anonymous function so
that global variables would not be part of the equation (I have heard
that some browsers are slower to access those.)
I successfully tested quite a few selector combinations, but I have no
illusions that this is a pat hand.
Lots of lines will wrap. I don't have time to format it for the ng.
If you want to help with testing and don't feel like extricating the
code from this post, send me an email and I will send my test page. I
can't send the hacked MooTools test page as it has a copyright notice,
but I can offer instructions on how to create a local copy and add a
fourth column to it.
I think I included everything required that is not currently in the
repository. IMO, some of the support functions should be discussed
for the project in the near future.
var $; // Declare globally for automated testing
// Put the rest in an anonymous function.
var doc = this.document;
var html = getAnElement();
var getEBCN, getEBXP, resolve, selectByXPath,
xPathChildSelectorsBad;
var attributeAliases = {'for':'htmlFor', accesskey:'accessKey',
maxlength:'maxLength', 'class':'className', readonly:'readOnly'};
var attributesBad = (html && html.getAttribute &&
html.getAttribute('style') && typeof(html.getAttribute('style')) ==
'object');
var reCamel = new RegExp('([^-]*)-(.)(.*)');
// Used to convert array-like host objects to arrays
// IIRC, Array.prototype.slice didn't work with node lists
function toArray(o) {
var a = [];
var l = o.length;
while (l--) { a[l] = o[l]; }
return a;
}
if (isFeaturedMethod(doc, 'evaluate')) {
resolve = function() { return '
http://www.w3.org/1999/xhtml'; };
getEBXP = function(s, d) {
d = d || doc;
var i, q = [], r, docNode = (d.nodeType == 9)?d:
(d.ownerDocument);
r = docNode.evaluate(s, d,
(xmlParseMode(docNode))?resolve:null,
global.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null);
i = r.snapshotLength;
while (i--) { q
= r.snapshotItem(i); }
return q;
};
}
function elementDocument(el) {
if (el.ownerDocument) {
return el.ownerDocument;
}
while (el.parentNode) {
el = el.parentNode;
}
return el;
}
var hasAttribute = (function() {
if (isFeaturedMethod(html, 'hasAttribute')) {
return function(el, name) { return el.hasAttribute(name); };
}
if (isFeaturedMethod(html, 'attributes')) {
return function(el, name) { return !!(el.attributes[name] &&
el.attributes[name].specified); };
}
})();
function camelize(name) {
var m = name.match(reCamel);
return (m)?([m[1], m[2].toUpperCase(), m[3]].join('')):name;
}
var getAttribute = (function() {
var att, alias, nameC, nn, reEvent, reNewLine, reFunction,
reBoolean, reURI;
if (html && html.getAttribute) {
if (attributesBad) {
reEvent = new RegExp('^on');
reNewLine = new RegExp('[\\n\\r]', 'g');
reFunction = new RegExp('^function anonymous\\(\\) *{(.*)}$');
reBoolean = new RegExp('checked|selected|disabled|multiple');
reURI = new RegExp('href|src|longdesc');
return function(el, name) {
if (!hasAttribute || hasAttribute(el, name)) {
if ((elementDocument(el)).selectNodes) { return
el.getAttribute(name, 2); } // HTML embedded in an XML document
name = name.toLowerCase();
alias = attributeAliases[name];
if (!alias) {
if (name == 'style') { return (el.style)?
(el.style.cssText || null):null; }
if (reBoolean.test(name)) { return (el[name])?
name:null; }
if (reURI.test(name)) { return el.getAttribute(name,
2); }
if (reEvent.test(name) && el[name]) {
att = el[name].toString();
if (att) {
att = att.replace(reNewLine, '');
if (reFunction.test(att)) { return
att.replace(reFunction, '$1'); }
}
return null;
}
nn = el.tagName;
if (nn == 'select' && name == 'type') { return null; }
if (nn == 'form' && el.getAttributeNode) {
att = el.getAttributeNode(name);
return (att && att.nodeValue)?att.nodeValue:null;
}
}
nameC = camelize(alias || name);
if (typeof(el[nameC]) == 'unknown') {
return '[unknown]';
}
else {
return ((typeof(el[nameC]) != 'string' && typeof(el[nameC]) !=
'undefined' && el[nameC] !== null && el[nameC].toString)?
el[nameC].toString():el[nameC]) || null;
}
}
return null;
};
}
return function(el, name) { return el.getAttribute(name); };
}
})();
var getChildren = (function() {
if (isFeaturedMethod(html, 'children')) {
return function(el) {
return el.children;
};
}
if (isFeaturedMethod(html, 'childNodes')) {
return function(el) {
// Should use XPath here when possible
// Doesn't matter for getEBCS as XPath branch never calls this
var nl = el.childNodes, r = [];
var i = nl.length;
while (i--) {
// Code duplicated for performance
if ((nl.nodeType == 1 && nl.tagName != '!') || (!
nl.nodeType && nl.tagName)) {
r.push(nl);
}
}
return r.reverse();
//return filter(toArray(el.childNodes), elementFilter);
};
}
})();
function parseAtom(s) {
var ai, m, mv, ml;
var o = {};
s = s.replace(/\x00/g, ' '); // Change nulls back to spaces
m = s.match(/^([>\+~])/);
if (m) {
o.combinator = m[1];
s = s.substring(1);
}
m = s.match(/^([^#\.\[:]+)/);
o.tag = m ? m[1] : '*';
m = s.match(/#([^\.]+)/);
o.id = m ? m[1] : null;
m = s.match(/\.([^\[\:]+)/);
o.cls = m ? m[1] : null;
m = s.match(/.+)$/);
o.pseudo = m ? m[1] : null;
m = s.match(/\[[^\]]+\]/g);
if (m) {
ml = m.length;
o.attributes = [];
o.attributeValues = [];
o.attributeOperators = [];
for (ai = 0; ai < ml; ai++) {
o.attributes[ai] = m[ai].substring(1, m[ai].length - 1);
m[ai] = m[ai].replace(/^%/, '');
mv = m[ai].match(/(~|!)?="*([^"\]]*)"*/);
if (mv) {
o.attributeOperators[ai] = mv[1];
o.attributeValues[ai] = mv[2];
o.attributes[ai] = o.attributes[ai].replace(/(~|!)?=.*/,
'');
}
}
}
return o;
}
if (typeof(getEBXP) != 'undefined') {
selectByXPath = function(d, a) {
var atts, m, o, r, s;
var docNode = (d.nodeType == 9)?d:elementDocument(d);
var i = a.length;
while (i--) {
o = parseAtom(a);
if (s) {
if (o.combinator) {
s += (o.combinator == '>')?'/'o.combinator == '~')?'/
preceding-sibling::':'/following-sibling::';
}
else {
s += '//';
}
}
else {
s = './/';
}
s = [s, ((xmlParseMode(docNode))?'html:':''),
(o.pseudo)?'*'.tag, ((o.cls)?"[contains(concat(' ', @class, ' '), '
" + o.cls + " ')]":'')].join('');
if (o.pseudo) {
s += ((o.pseudo == 'last-child')?'[last()]':'[1]') +
'[self::' + o.tag + ']';
}
if (o.id) {
s += ['[@id="', o.id, '"]'].join('');
}
if (o.attributes) {
atts = [];
m = o.attributes.length;
while (m--) {
switch(o.attributeOperators[m]) {
case '~':
atts.push(['contains(@', o.attributes[m], ',"',
o.attributeValues[m], '")'].join(''));
break;
case '!':
atts.push(['not(@', o.attributes[m], '="',
o.attributeValues[m], '")'].join(''));
break;
default:
atts.push((o.attributeValues[m])?['@', o.attributes[m],
'="', o.attributeValues[m], '"'].join(''):['@',
o.attributes[m]].join(''));
}
}
s = [s, '[', atts.join(' and '), ']'].join('');
}
}
return getEBXP(s, d);
};
}
var getEBCS = (function() {
var els, // candidate elements for return
ns, // elements to return
o, // selector atom object
docNode,
cache = {}, // cached select functions
aCache = {}, // cached select atom functions
qid = 0, // query id (marks branches as traversed)
bAll; // indicates if "all" object is featured for elements
function getDocNode(d) {
return (d.nodeType == 9 || (!d.nodeType && !d.tagName))?
d:elementDocument(d);
}
bAll = (isFeaturedMethod(html, 'all'));
var previousAtom; // adjacent selectors check this to determine
comparison (currently only checking for tag)
var selectAtomFactory = function(id, tag, cls, combinator,
attributes, attributeValues, attributeOperators, pseudo) {
var ai, al, att, b, c, d, el, i, j, k, m, r, sibling;
return function(a, docNode) {
if (attributes) { al = attributes.length; }
r = [];
k = a.length;
qid++;
while (k--) {
d = a[k];
if (id) {
if (!d.tagName || (combinator && combinator != '>')) {
els = (el = getEBI(id, docNode)) ? [el] : [];
}
else {
els = (bAll && (el = d.all[id]))?[el](combinator ==
'>')?getChildren(d):getEBTN(d, tag));
}
}
else {
els = (combinator == '>')?getChildren(d):getEBTN(d, tag);
}
i = els.length;
while (i--) {
el = els;
b = ((!cls || ((m = el.className) &&
(' ' + m + ' ').indexOf(cls) > -1)) &&
(!id || el.id == id)
);
if (b) {
switch (combinator) {
case '~':
case '+':
sibling = el;
do {
sibling = (combinator == '~')?
sibling.nextSibling:sibling.previousSibling;
}
while (sibling && sibling.nodeType != 1);
b = b && (sibling && ((!previousAtom.id ||
previousAtom.id == sibling.id) && (previousAtom.tag == '*' ||
sibling.tagName.toLowerCase() == previousAtom.tag) && (!
previousAtom.cls || ((m = sibling.className) && (' ' + m + '
').indexOf(previousAtom.cls) > -1))));
break;
default:
b = b && (tag == '*' || (!combinator && !id) ||
el.tagName.toLowerCase() == tag);
}
if (pseudo && el.parentNode) {
c = getChildren(el.parentNode);
b = b && (c[(pseudo == 'first-child')?0:c.length - 1]
== el);
}
if (attributes) {
ai = al;
while (ai-- && b) {
switch(attributeOperators[ai]) {
case '~':
att = getAttribute(el, attributes[ai]);
b = b && att && att.indexOf(attributeValues[ai]) !
= -1;
break;
case '!':
b = b && getAttribute(el, attributes[ai]) !=
attributeValues[ai];
break;
default:
b = b && (attributeValues[ai])?getAttribute(el,
attributes[ai]) == attributeValues[ai]!hasAttribute &&
getAttribute(el, attributes[ai])) || hasAttribute(el, attributes[ai]);
}
}
}
if (b && el._qid != qid) { r[r.length] = el; el._qid =
qid; if (id) { break; } }
}
}
}
return r;
};
};
var selectFactory = function(a) {
var i, j;
return function(d) {
i = a.length;
j = 1;
docNode = getDocNode(d);
ns = [[d]];
while (i--) {
o = parseAtom(a);
if (!aCache['_' + a]) {
aCache['_' + a] = selectAtomFactory(o.id,
o.tag.toLowerCase(), (o.cls)?' ' + o.cls + ' ':null, o.combinator,
o.attributes, o.attributeValues, o.attributeOperators, o.pseudo);
}
ns[j] = aCache['_' + a](ns[j - 1], docNode);
previousAtom = o;
j++;
}
return ns[j - 1].reverse();
};
};
var get = (function() {
var el, getD, r;
if (typeof getEBI != 'undefined' &&
typeof getEBTN != 'undefined' &&
typeof getChildren != 'undefined' &&
typeof getAttribute != 'undefined') {
getD = function(d, a, s, qid) {
if (a.length == 1) {
o = parseAtom(a[0]);
if (!o.pseudo && !o.attributes) {
if (o.id && !o.pseudo && !o.cls && !o.attributes) {
// Optimization for #foo
el = getEBI(o.id, getDocNode(d));
return (el && (o.tag == '*' || o.tag ==
el.tagName.toLowerCase()))?[el]:[];
}
if (!o.id && !o.cls) {
// Optimization for foo
r = getEBTN(d, o.tag);
return (typeof(r.reverse) == 'function')?r:toArray(r);
}
}
}
s = '_' + s;
if (!cache) { // avoid toString conflict
cache = selectFactory(a);
}
return cache(d, qid);
};
}
if (getD) {
return function(d, a, s, qid) {
// Really only need to disable XPath for specific selectors
if (selectByXPath && !xPathChildSelectorsBad) {
return (get = selectByXPath)(d, a, s);
}
else {
return (get = getD)(d, a, s, qid);
}
};
}
})();
if (get) {
return function(s, d) {
var a = [], aSel = [], chr, i, inQuotes, r = [], used = {};
d = d || doc;
s = s.replace(/^\s+/,'').replace(/\s+$/,''); // trim
s = s.replace(/\s+,/g, ',').replace(/,\s+/g, ','); // remove
spaces before and after commas
i = s.length;
while (i--) {
chr = s.charAt(i);
switch (chr) {
case ',':
if (inQuotes) {
aSel[aSel.length] = chr;
}
else {
a[a.length] = aSel.reverse().join('');
aSel = [];
}
break;
case ' ':
// change quoted spaces to nulls temporarily
// changed back in parseAtom
aSel[aSel.length] = (inQuotes)?'\x00':' ';
break;
case '"':
inQuotes = !inQuotes;
aSel[aSel.length] = chr;
break;
default:
aSel[aSel.length] = chr;
}
}
if (aSel.length) { a[a.length] = aSel.reverse().join(''); }
a.reverse();
i = a.length;
while (i--) {
a = a.replace(/\s+/g, ' '); // collapse multiple spaces
a = a.replace(/([^\s])([>\+])/g, '$1 $2');
a = a.replace(/([^\s])([~])[^=]/g, '$1 $2');
a = a.replace(/([>\+~])\s/g, '$1');
if (!used['_' + a]) { // prevent dupes (e.g. div, div,
div)
r = r.concat(get(d, a.split(' ').reverse(), a));
}
used['_' + a] = 1;
}
return r;
};
}
})();
if (getEBCS) {
$ = getEBCS;
getEBCN = function(s, d) {
return getEBCS('.' + s, d);
};
}
// Safari 3 bug test (tested Windows Beta version)
// This logic needs to go in a central DOMContentLoaded wrapper
if (isFeaturedMethod(this, 'addEventListener')) {
this.addEventListener('load', function() {
if (getEBXP) {
xPathChildSelectorsBad = !!getEBXP('.//*[1]
[self::body]').length;
}
}, false);
}