ref: c43dc872397e19d8d23a5296b661cc7eced8223c
dir: /domino-lib/select.js/
"use strict"; /* jshint eqnull: true */ /** * Zest (https://github.com/chjj/zest) * A css selector engine. * Copyright (c) 2011-2012, Christopher Jeffrey. (MIT Licensed) * Domino version based on Zest v0.1.3 with bugfixes applied. */ /** * Helpers */ var window = Object.create(null, { location: { get: function() { throw new Error('window.location is not supported.'); } } }); var compareDocumentPosition = function(a, b) { return a.compareDocumentPosition(b); }; var order = function(a, b) { /* jshint bitwise: false */ return compareDocumentPosition(a, b) & 2 ? 1 : -1; }; var next = function(el) { while ((el = el.nextSibling) && el.nodeType !== 1); return el; }; var prev = function(el) { while ((el = el.previousSibling) && el.nodeType !== 1); return el; }; var child = function(el) { /*jshint -W084 */ if (el = el.firstChild) { while (el.nodeType !== 1 && (el = el.nextSibling)); } return el; }; var lastChild = function(el) { /*jshint -W084 */ if (el = el.lastChild) { while (el.nodeType !== 1 && (el = el.previousSibling)); } return el; }; var parentIsElement = function(n) { if (!n.parentNode) { return false; } var nodeType = n.parentNode.nodeType; // The root `html` element can be a first- or last-child, too. return nodeType === 1 || nodeType === 9; }; var unquote = function(str) { if (!str) return str; var ch = str[0]; if (ch === '"' || ch === '\'') { if (str[str.length-1] === ch) { str = str.slice(1, -1); } else { // bad string. str = str.slice(1); } return str.replace(rules.str_escape, function(s) { var m = /^\\(?:([0-9A-Fa-f]+)|([\r\n\f]+))/.exec(s); if (!m) { return s.slice(1); } if (m[2]) { return ''; /* escaped newlines are ignored in strings. */ } var cp = parseInt(m[1], 16); return String.fromCodePoint ? String.fromCodePoint(cp) : // Not all JavaScript implementations have String.fromCodePoint yet. String.fromCharCode(cp); }); } else if (rules.ident.test(str)) { return decodeid(str); } else { // NUMBER, PERCENTAGE, DIMENSION, etc return str; } }; var decodeid = function(str) { return str.replace(rules.escape, function(s) { var m = /^\\([0-9A-Fa-f]+)/.exec(s); if (!m) { return s[1]; } var cp = parseInt(m[1], 16); return String.fromCodePoint ? String.fromCodePoint(cp) : // Not all JavaScript implementations have String.fromCodePoint yet. String.fromCharCode(cp); }); }; var indexOf = (function() { if (Array.prototype.indexOf) { return Array.prototype.indexOf; } return function(obj, item) { var i = this.length; while (i--) { if (this[i] === item) return i; } return -1; }; })(); var makeInside = function(start, end) { var regex = rules.inside.source .replace(/</g, start) .replace(/>/g, end); return new RegExp(regex); }; var replace = function(regex, name, val) { regex = regex.source; regex = regex.replace(name, val.source || val); return new RegExp(regex); }; var truncateUrl = function(url, num) { return url .replace(/^(?:\w+:\/\/|\/+)/, '') .replace(/(?:\/+|\/*#.*?)$/, '') .split('/', num) .join('/'); }; /** * Handle `nth` Selectors */ var parseNth = function(param_, test) { var param = param_.replace(/\s+/g, '') , cap; if (param === 'even') { param = '2n+0'; } else if (param === 'odd') { param = '2n+1'; } else if (param.indexOf('n') === -1) { param = '0n' + param; } cap = /^([+-])?(\d+)?n([+-])?(\d+)?$/.exec(param); return { group: cap[1] === '-' ? -(cap[2] || 1) : +(cap[2] || 1), offset: cap[4] ? (cap[3] === '-' ? -cap[4] : +cap[4]) : 0 }; }; var nth = function(param_, test, last) { var param = parseNth(param_) , group = param.group , offset = param.offset , find = !last ? child : lastChild , advance = !last ? next : prev; return function(el) { if (!parentIsElement(el)) return; var rel = find(el.parentNode) , pos = 0; while (rel) { if (test(rel, el)) pos++; if (rel === el) { pos -= offset; return group && pos ? (pos % group) === 0 && (pos < 0 === group < 0) : !pos; } rel = advance(rel); } }; }; /** * Simple Selectors */ var selectors = { '*': (function() { if (false/*function() { var el = document.createElement('div'); el.appendChild(document.createComment('')); return !!el.getElementsByTagName('*')[0]; }()*/) { return function(el) { if (el.nodeType === 1) return true; }; } return function() { return true; }; })(), 'type': function(type) { type = type.toLowerCase(); return function(el) { return el.nodeName.toLowerCase() === type; }; }, 'attr': function(key, op, val, i) { op = operators[op]; return function(el) { var attr; switch (key) { case 'for': attr = el.htmlFor; break; case 'class': // className is '' when non-existent // getAttribute('class') is null attr = el.className; if (attr === '' && el.getAttribute('class') == null) { attr = null; } break; case 'href': case 'src': attr = el.getAttribute(key, 2); break; case 'title': // getAttribute('title') can be '' when non-existent sometimes? attr = el.getAttribute('title') || null; break; // careful with attributes with special getter functions case 'id': case 'lang': case 'dir': case 'accessKey': case 'hidden': case 'tabIndex': case 'style': if (el.getAttribute) { attr = el.getAttribute(key); break; } /* falls through */ default: if (el.hasAttribute && !el.hasAttribute(key)) { break; } attr = el[key] != null ? el[key] : el.getAttribute && el.getAttribute(key); break; } if (attr == null) return; attr = attr + ''; if (i) { attr = attr.toLowerCase(); val = val.toLowerCase(); } return op(attr, val); }; }, ':first-child': function(el) { return !prev(el) && parentIsElement(el); }, ':last-child': function(el) { return !next(el) && parentIsElement(el); }, ':only-child': function(el) { return !prev(el) && !next(el) && parentIsElement(el); }, ':nth-child': function(param, last) { return nth(param, function() { return true; }, last); }, ':nth-last-child': function(param) { return selectors[':nth-child'](param, true); }, ':root': function(el) { return el.ownerDocument.documentElement === el; }, ':empty': function(el) { return !el.firstChild; }, ':not': function(sel) { var test = compileGroup(sel); return function(el) { return !test(el); }; }, ':first-of-type': function(el) { if (!parentIsElement(el)) return; var type = el.nodeName; /*jshint -W084 */ while (el = prev(el)) { if (el.nodeName === type) return; } return true; }, ':last-of-type': function(el) { if (!parentIsElement(el)) return; var type = el.nodeName; /*jshint -W084 */ while (el = next(el)) { if (el.nodeName === type) return; } return true; }, ':only-of-type': function(el) { return selectors[':first-of-type'](el) && selectors[':last-of-type'](el); }, ':nth-of-type': function(param, last) { return nth(param, function(rel, el) { return rel.nodeName === el.nodeName; }, last); }, ':nth-last-of-type': function(param) { return selectors[':nth-of-type'](param, true); }, ':checked': function(el) { return !!(el.checked || el.selected); }, ':indeterminate': function(el) { return !selectors[':checked'](el); }, ':enabled': function(el) { return !el.disabled && el.type !== 'hidden'; }, ':disabled': function(el) { return !!el.disabled; }, ':target': function(el) { return el.id === window.location.hash.substring(1); }, ':focus': function(el) { return el === el.ownerDocument.activeElement; }, ':is': function(sel) { return compileGroup(sel); }, // :matches is an older name for :is; see // https://github.com/w3c/csswg-drafts/issues/3258 ':matches': function(sel) { return selectors[':is'](sel); }, ':nth-match': function(param, last) { var args = param.split(/\s*,\s*/) , arg = args.shift() , test = compileGroup(args.join(',')); return nth(arg, test, last); }, ':nth-last-match': function(param) { return selectors[':nth-match'](param, true); }, ':links-here': function(el) { return el + '' === window.location + ''; }, ':lang': function(param) { return function(el) { while (el) { if (el.lang) return el.lang.indexOf(param) === 0; el = el.parentNode; } }; }, ':dir': function(param) { return function(el) { while (el) { if (el.dir) return el.dir === param; el = el.parentNode; } }; }, ':scope': function(el, con) { var context = con || el.ownerDocument; if (context.nodeType === 9) { return el === context.documentElement; } return el === context; }, ':any-link': function(el) { return typeof el.href === 'string'; }, ':local-link': function(el) { if (el.nodeName) { return el.href && el.host === window.location.host; } var param = +el + 1; return function(el) { if (!el.href) return; var url = window.location + '' , href = el + ''; return truncateUrl(url, param) === truncateUrl(href, param); }; }, ':default': function(el) { return !!el.defaultSelected; }, ':valid': function(el) { return el.willValidate || (el.validity && el.validity.valid); }, ':invalid': function(el) { return !selectors[':valid'](el); }, ':in-range': function(el) { return el.value > el.min && el.value <= el.max; }, ':out-of-range': function(el) { return !selectors[':in-range'](el); }, ':required': function(el) { return !!el.required; }, ':optional': function(el) { return !el.required; }, ':read-only': function(el) { if (el.readOnly) return true; var attr = el.getAttribute('contenteditable') , prop = el.contentEditable , name = el.nodeName.toLowerCase(); name = name !== 'input' && name !== 'textarea'; return (name || el.disabled) && attr == null && prop !== 'true'; }, ':read-write': function(el) { return !selectors[':read-only'](el); }, ':hover': function() { throw new Error(':hover is not supported.'); }, ':active': function() { throw new Error(':active is not supported.'); }, ':link': function() { throw new Error(':link is not supported.'); }, ':visited': function() { throw new Error(':visited is not supported.'); }, ':column': function() { throw new Error(':column is not supported.'); }, ':nth-column': function() { throw new Error(':nth-column is not supported.'); }, ':nth-last-column': function() { throw new Error(':nth-last-column is not supported.'); }, ':current': function() { throw new Error(':current is not supported.'); }, ':past': function() { throw new Error(':past is not supported.'); }, ':future': function() { throw new Error(':future is not supported.'); }, // Non-standard, for compatibility purposes. ':contains': function(param) { return function(el) { var text = el.innerText || el.textContent || el.value || ''; return text.indexOf(param) !== -1; }; }, ':has': function(param) { return function(el) { return find(param, el).length > 0; }; } // Potentially add more pseudo selectors for // compatibility with sizzle and most other // selector engines (?). }; /** * Attribute Operators */ var operators = { '-': function() { return true; }, '=': function(attr, val) { return attr === val; }, '*=': function(attr, val) { return attr.indexOf(val) !== -1; }, '~=': function(attr, val) { var i , s , f , l; for (s = 0; true; s = i + 1) { i = attr.indexOf(val, s); if (i === -1) return false; f = attr[i - 1]; l = attr[i + val.length]; if ((!f || f === ' ') && (!l || l === ' ')) return true; } }, '|=': function(attr, val) { var i = attr.indexOf(val) , l; if (i !== 0) return; l = attr[i + val.length]; return l === '-' || !l; }, '^=': function(attr, val) { return attr.indexOf(val) === 0; }, '$=': function(attr, val) { var i = attr.lastIndexOf(val); return i !== -1 && i + val.length === attr.length; }, // non-standard '!=': function(attr, val) { return attr !== val; } }; /** * Combinator Logic */ var combinators = { ' ': function(test) { return function(el) { /*jshint -W084 */ while (el = el.parentNode) { if (test(el)) return el; } }; }, '>': function(test) { return function(el) { /*jshint -W084 */ if (el = el.parentNode) { return test(el) && el; } }; }, '+': function(test) { return function(el) { /*jshint -W084 */ if (el = prev(el)) { return test(el) && el; } }; }, '~': function(test) { return function(el) { /*jshint -W084 */ while (el = prev(el)) { if (test(el)) return el; } }; }, 'noop': function(test) { return function(el) { return test(el) && el; }; }, 'ref': function(test, name) { var node; function ref(el) { var doc = el.ownerDocument , nodes = doc.getElementsByTagName('*') , i = nodes.length; while (i--) { node = nodes[i]; if (ref.test(el)) { node = null; return true; } } node = null; } ref.combinator = function(el) { if (!node || !node.getAttribute) return; var attr = node.getAttribute(name) || ''; if (attr[0] === '#') attr = attr.substring(1); if (attr === el.id && test(node)) { return node; } }; return ref; } }; /** * Grammar */ var rules = { escape: /\\(?:[^0-9A-Fa-f\r\n]|[0-9A-Fa-f]{1,6}[\r\n\t ]?)/g, str_escape: /(escape)|\\(\n|\r\n?|\f)/g, nonascii: /[\u00A0-\uFFFF]/, cssid: /(?:(?!-?[0-9])(?:escape|nonascii|[-_a-zA-Z0-9])+)/, qname: /^ *(cssid|\*)/, simple: /^(?:([.#]cssid)|pseudo|attr)/, ref: /^ *\/(cssid)\/ */, combinator: /^(?: +([^ \w*.#\\]) +|( )+|([^ \w*.#\\]))(?! *$)/, attr: /^\[(cssid)(?:([^\w]?=)(inside))?\]/, pseudo: /^(:cssid)(?:\((inside)\))?/, inside: /(?:"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|<[^"'>]*>|\\["'>]|[^"'>])*/, ident: /^(cssid)$/ }; rules.cssid = replace(rules.cssid, 'nonascii', rules.nonascii); rules.cssid = replace(rules.cssid, 'escape', rules.escape); rules.qname = replace(rules.qname, 'cssid', rules.cssid); rules.simple = replace(rules.simple, 'cssid', rules.cssid); rules.ref = replace(rules.ref, 'cssid', rules.cssid); rules.attr = replace(rules.attr, 'cssid', rules.cssid); rules.pseudo = replace(rules.pseudo, 'cssid', rules.cssid); rules.inside = replace(rules.inside, '[^"\'>]*', rules.inside); rules.attr = replace(rules.attr, 'inside', makeInside('\\[', '\\]')); rules.pseudo = replace(rules.pseudo, 'inside', makeInside('\\(', '\\)')); rules.simple = replace(rules.simple, 'pseudo', rules.pseudo); rules.simple = replace(rules.simple, 'attr', rules.attr); rules.ident = replace(rules.ident, 'cssid', rules.cssid); rules.str_escape = replace(rules.str_escape, 'escape', rules.escape); /** * Compiling */ var compile = function(sel_) { var sel = sel_.replace(/^\s+|\s+$/g, '') , test , filter = [] , buff = [] , subject , qname , cap , op , ref; /*jshint -W084 */ while (sel) { if (cap = rules.qname.exec(sel)) { sel = sel.substring(cap[0].length); qname = decodeid(cap[1]); buff.push(tok(qname, true)); } else if (cap = rules.simple.exec(sel)) { sel = sel.substring(cap[0].length); qname = '*'; buff.push(tok(qname, true)); buff.push(tok(cap)); } else { throw new SyntaxError('Invalid selector.'); } while (cap = rules.simple.exec(sel)) { sel = sel.substring(cap[0].length); buff.push(tok(cap)); } if (sel[0] === '!') { sel = sel.substring(1); subject = makeSubject(); subject.qname = qname; buff.push(subject.simple); } if (cap = rules.ref.exec(sel)) { sel = sel.substring(cap[0].length); ref = combinators.ref(makeSimple(buff), decodeid(cap[1])); filter.push(ref.combinator); buff = []; continue; } if (cap = rules.combinator.exec(sel)) { sel = sel.substring(cap[0].length); op = cap[1] || cap[2] || cap[3]; if (op === ',') { filter.push(combinators.noop(makeSimple(buff))); break; } } else { op = 'noop'; } if (!combinators[op]) { throw new SyntaxError('Bad combinator.'); } filter.push(combinators[op](makeSimple(buff))); buff = []; } test = makeTest(filter); test.qname = qname; test.sel = sel; if (subject) { subject.lname = test.qname; subject.test = test; subject.qname = subject.qname; subject.sel = test.sel; test = subject; } if (ref) { ref.test = test; ref.qname = test.qname; ref.sel = test.sel; test = ref; } return test; }; var tok = function(cap, qname) { // qname if (qname) { return cap === '*' ? selectors['*'] : selectors.type(cap); } // class/id if (cap[1]) { return cap[1][0] === '.' // XXX unescape here? or in attr? ? selectors.attr('class', '~=', decodeid(cap[1].substring(1)), false) : selectors.attr('id', '=', decodeid(cap[1].substring(1)), false); } // pseudo-name // inside-pseudo if (cap[2]) { return cap[3] ? selectors[decodeid(cap[2])](unquote(cap[3])) : selectors[decodeid(cap[2])]; } // attr name // attr op // attr value if (cap[4]) { var value = cap[6]; var i = /["'\s]\s*I$/i.test(value); if (i) { value = value.replace(/\s*I$/i, ''); } return selectors.attr(decodeid(cap[4]), cap[5] || '-', unquote(value), i); } throw new SyntaxError('Unknown Selector.'); }; var makeSimple = function(func) { var l = func.length , i; // Potentially make sure // `el` is truthy. if (l < 2) return func[0]; return function(el) { if (!el) return; for (i = 0; i < l; i++) { if (!func[i](el)) return; } return true; }; }; var makeTest = function(func) { if (func.length < 2) { return function(el) { return !!func[0](el); }; } return function(el) { var i = func.length; while (i--) { if (!(el = func[i](el))) return; } return true; }; }; var makeSubject = function() { var target; function subject(el) { var node = el.ownerDocument , scope = node.getElementsByTagName(subject.lname) , i = scope.length; while (i--) { if (subject.test(scope[i]) && target === el) { target = null; return true; } } target = null; } subject.simple = function(el) { target = el; return true; }; return subject; }; var compileGroup = function(sel) { var test = compile(sel) , tests = [ test ]; while (test.sel) { test = compile(test.sel); tests.push(test); } if (tests.length < 2) return test; return function(el) { var l = tests.length , i = 0; for (; i < l; i++) { if (tests[i](el)) return true; } }; }; /** * Selection */ var find = function(sel, node) { var results = [] , test = compile(sel) , scope = node.getElementsByTagName(test.qname) , i = 0 , el; /*jshint -W084 */ while (el = scope[i++]) { if (test(el)) results.push(el); } if (test.sel) { while (test.sel) { test = compile(test.sel); scope = node.getElementsByTagName(test.qname); i = 0; /*jshint -W084 */ while (el = scope[i++]) { if (test(el) && indexOf.call(results, el) === -1) { results.push(el); } } } results.sort(order); } return results; }; /** * Expose */ module.exports = exports = function(sel, context) { /* when context isn't a DocumentFragment and the selector is simple: */ var id, r; if (context.nodeType !== 11 && sel.indexOf(' ') === -1) { if (sel[0] === '#' && context.rooted && /^#[A-Z_][-A-Z0-9_]*$/i.test(sel)) { if (context.doc._hasMultipleElementsWithId) { id = sel.substring(1); if (!context.doc._hasMultipleElementsWithId(id)) { r = context.doc.getElementById(id); return r ? [r] : []; } } } if (sel[0] === '.' && /^\.\w+$/.test(sel)) { return context.getElementsByClassName(sel.substring(1)); } if (/^\w+$/.test(sel)) { return context.getElementsByTagName(sel); } } /* do things the hard/slow way */ return find(sel, context); }; exports.selectors = selectors; exports.operators = operators; exports.combinators = combinators; exports.matches = function(el, sel) { var test = { sel: sel }; do { test = compile(test.sel); if (test(el)) { return true; } } while (test.sel); return false; };