| /* vim: set sw=4 sts=4 : */ | |
| (function () { | |
| var estraverse = require('estraverse'); | |
| var parser = require('./parser'); | |
| var isArray = Array.isArray || function isArray(array) { | |
| return {}.toString.call(array) === '[object Array]'; | |
| }; | |
| var LEFT_SIDE = {}; | |
| var RIGHT_SIDE = {}; | |
| function esqueryModule() { | |
| /** | |
| * Get the value of a property which may be multiple levels down in the object. | |
| */ | |
| function getPath(obj, key) { | |
| var i, keys = key.split("."); | |
| for (i = 0; i < keys.length; i++) { | |
| if (obj == null) { return obj; } | |
| obj = obj[keys[i]]; | |
| } | |
| return obj; | |
| } | |
| /** | |
| * Determine whether `node` can be reached by following `path`, starting at `ancestor`. | |
| */ | |
| function inPath(node, ancestor, path) { | |
| var field, remainingPath, i; | |
| if (path.length === 0) { return node === ancestor; } | |
| if (ancestor == null) { return false; } | |
| field = ancestor[path[0]]; | |
| remainingPath = path.slice(1); | |
| if (isArray(field)) { | |
| for (i = 0, l = field.length; i < l; ++i) { | |
| if (inPath(node, field[i], remainingPath)) { return true; } | |
| } | |
| return false; | |
| } else { | |
| return inPath(node, field, remainingPath); | |
| } | |
| } | |
| /** | |
| * Given a `node` and its ancestors, determine if `node` is matched by `selector`. | |
| */ | |
| function matches(node, selector, ancestry) { | |
| var path, ancestor, i, l, p; | |
| if (!selector) { return true; } | |
| if (!node) { return false; } | |
| if (!ancestry) { ancestry = []; } | |
| switch(selector.type) { | |
| case 'wildcard': | |
| return true; | |
| case 'identifier': | |
| return selector.value.toLowerCase() === node.type.toLowerCase(); | |
| case 'field': | |
| path = selector.name.split('.'); | |
| ancestor = ancestry[path.length - 1]; | |
| return inPath(node, ancestor, path); | |
| case 'matches': | |
| for (i = 0, l = selector.selectors.length; i < l; ++i) { | |
| if (matches(node, selector.selectors[i], ancestry)) { return true; } | |
| } | |
| return false; | |
| case 'compound': | |
| for (i = 0, l = selector.selectors.length; i < l; ++i) { | |
| if (!matches(node, selector.selectors[i], ancestry)) { return false; } | |
| } | |
| return true; | |
| case 'not': | |
| for (i = 0, l = selector.selectors.length; i < l; ++i) { | |
| if (matches(node, selector.selectors[i], ancestry)) { return false; } | |
| } | |
| return true; | |
| case 'has': | |
| var a, collector = []; | |
| for (i = 0, l = selector.selectors.length; i < l; ++i) { | |
| a = []; | |
| estraverse.traverse(node, { | |
| enter: function (node, parent) { | |
| if (parent != null) { a.unshift(parent); } | |
| if (matches(node, selector.selectors[i], a)) { | |
| collector.push(node); | |
| } | |
| }, | |
| leave: function () { a.shift(); } | |
| }); | |
| } | |
| return collector.length !== 0; | |
| case 'child': | |
| if (matches(node, selector.right, ancestry)) { | |
| return matches(ancestry[0], selector.left, ancestry.slice(1)); | |
| } | |
| return false; | |
| case 'descendant': | |
| if (matches(node, selector.right, ancestry)) { | |
| for (i = 0, l = ancestry.length; i < l; ++i) { | |
| if (matches(ancestry[i], selector.left, ancestry.slice(i + 1))) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| case 'attribute': | |
| p = getPath(node, selector.name); | |
| switch (selector.operator) { | |
| case null: | |
| case void 0: | |
| return p != null; | |
| case '=': | |
| switch (selector.value.type) { | |
| case 'regexp': return typeof p === 'string' && selector.value.value.test(p); | |
| case 'literal': return '' + selector.value.value === '' + p; | |
| case 'type': return selector.value.value === typeof p; | |
| } | |
| case '!=': | |
| switch (selector.value.type) { | |
| case 'regexp': return !selector.value.value.test(p); | |
| case 'literal': return '' + selector.value.value !== '' + p; | |
| case 'type': return selector.value.value !== typeof p; | |
| } | |
| case '<=': return p <= selector.value.value; | |
| case '<': return p < selector.value.value; | |
| case '>': return p > selector.value.value; | |
| case '>=': return p >= selector.value.value; | |
| } | |
| case 'sibling': | |
| return matches(node, selector.right, ancestry) && | |
| sibling(node, selector.left, ancestry, LEFT_SIDE) || | |
| selector.left.subject && | |
| matches(node, selector.left, ancestry) && | |
| sibling(node, selector.right, ancestry, RIGHT_SIDE); | |
| case 'adjacent': | |
| return matches(node, selector.right, ancestry) && | |
| adjacent(node, selector.left, ancestry, LEFT_SIDE) || | |
| selector.right.subject && | |
| matches(node, selector.left, ancestry) && | |
| adjacent(node, selector.right, ancestry, RIGHT_SIDE); | |
| case 'nth-child': | |
| return matches(node, selector.right, ancestry) && | |
| nthChild(node, ancestry, function (length) { | |
| return selector.index.value - 1; | |
| }); | |
| case 'nth-last-child': | |
| return matches(node, selector.right, ancestry) && | |
| nthChild(node, ancestry, function (length) { | |
| return length - selector.index.value; | |
| }); | |
| case 'class': | |
| if(!node.type) return false; | |
| switch(selector.name.toLowerCase()){ | |
| case 'statement': | |
| if(node.type.slice(-9) === 'Statement') return true; | |
| // fallthrough: interface Declaration <: Statement { } | |
| case 'declaration': | |
| return node.type.slice(-11) === 'Declaration'; | |
| case 'pattern': | |
| if(node.type.slice(-7) === 'Pattern') return true; | |
| // fallthrough: interface Expression <: Node, Pattern { } | |
| case 'expression': | |
| return node.type.slice(-10) === 'Expression' || | |
| node.type.slice(-7) === 'Literal' || | |
| ( | |
| node.type === 'Identifier' && | |
| (ancestry.length === 0 || ancestry[0].type !== 'MetaProperty') | |
| ) || | |
| node.type === 'MetaProperty'; | |
| case 'function': | |
| return node.type.slice(0, 8) === 'Function' || | |
| node.type === 'ArrowFunctionExpression'; | |
| } | |
| throw new Error('Unknown class name: ' + selector.name); | |
| } | |
| throw new Error('Unknown selector type: ' + selector.type); | |
| } | |
| /* | |
| * Determines if the given node has a sibling that matches the given selector. | |
| */ | |
| function sibling(node, selector, ancestry, side) { | |
| var parent = ancestry[0], listProp, startIndex, keys, i, l, k, lowerBound, upperBound; | |
| if (!parent) { return false; } | |
| keys = estraverse.VisitorKeys[parent.type]; | |
| for (i = 0, l = keys.length; i < l; ++i) { | |
| listProp = parent[keys[i]]; | |
| if (isArray(listProp)) { | |
| startIndex = listProp.indexOf(node); | |
| if (startIndex < 0) { continue; } | |
| if (side === LEFT_SIDE) { | |
| lowerBound = 0; | |
| upperBound = startIndex; | |
| } else { | |
| lowerBound = startIndex + 1; | |
| upperBound = listProp.length; | |
| } | |
| for (k = lowerBound; k < upperBound; ++k) { | |
| if (matches(listProp[k], selector, ancestry)) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| /* | |
| * Determines if the given node has an asjacent sibling that matches the given selector. | |
| */ | |
| function adjacent(node, selector, ancestry, side) { | |
| var parent = ancestry[0], listProp, keys, i, l, idx; | |
| if (!parent) { return false; } | |
| keys = estraverse.VisitorKeys[parent.type]; | |
| for (i = 0, l = keys.length; i < l; ++i) { | |
| listProp = parent[keys[i]]; | |
| if (isArray(listProp)) { | |
| idx = listProp.indexOf(node); | |
| if (idx < 0) { continue; } | |
| if (side === LEFT_SIDE && idx > 0 && matches(listProp[idx - 1], selector, ancestry)) { | |
| return true; | |
| } | |
| if (side === RIGHT_SIDE && idx < listProp.length - 1 && matches(listProp[idx + 1], selector, ancestry)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| /* | |
| * Determines if the given node is the nth child, determined by idxFn, which is given the containing list's length. | |
| */ | |
| function nthChild(node, ancestry, idxFn) { | |
| var parent = ancestry[0], listProp, keys, i, l, idx; | |
| if (!parent) { return false; } | |
| keys = estraverse.VisitorKeys[parent.type]; | |
| for (i = 0, l = keys.length; i < l; ++i) { | |
| listProp = parent[keys[i]]; | |
| if (isArray(listProp)) { | |
| idx = listProp.indexOf(node); | |
| if (idx >= 0 && idx === idxFn(listProp.length)) { return true; } | |
| } | |
| } | |
| return false; | |
| } | |
| /* | |
| * For each selector node marked as a subject, find the portion of the selector that the subject must match. | |
| */ | |
| function subjects(selector, ancestor) { | |
| var results, p; | |
| if (selector == null || typeof selector != 'object') { return []; } | |
| if (ancestor == null) { ancestor = selector; } | |
| results = selector.subject ? [ancestor] : []; | |
| for(p in selector) { | |
| if(!{}.hasOwnProperty.call(selector, p)) { continue; } | |
| [].push.apply(results, subjects(selector[p], p === 'left' ? selector[p] : ancestor)); | |
| } | |
| return results; | |
| } | |
| /** | |
| * From a JS AST and a selector AST, collect all JS AST nodes that match the selector. | |
| */ | |
| function match(ast, selector) { | |
| var ancestry = [], results = [], altSubjects, i, l, k, m; | |
| if (!selector) { return results; } | |
| altSubjects = subjects(selector); | |
| estraverse.traverse(ast, { | |
| enter: function (node, parent) { | |
| if (parent != null) { ancestry.unshift(parent); } | |
| if (matches(node, selector, ancestry)) { | |
| if (altSubjects.length) { | |
| for (i = 0, l = altSubjects.length; i < l; ++i) { | |
| if (matches(node, altSubjects[i], ancestry)) { results.push(node); } | |
| for (k = 0, m = ancestry.length; k < m; ++k) { | |
| if (matches(ancestry[k], altSubjects[i], ancestry.slice(k + 1))) { | |
| results.push(ancestry[k]); | |
| } | |
| } | |
| } | |
| } else { | |
| results.push(node); | |
| } | |
| } | |
| }, | |
| leave: function () { ancestry.shift(); } | |
| }); | |
| return results; | |
| } | |
| /** | |
| * Parse a selector string and return its AST. | |
| */ | |
| function parse(selector) { | |
| return parser.parse(selector); | |
| } | |
| /** | |
| * Query the code AST using the selector string. | |
| */ | |
| function query(ast, selector) { | |
| return match(ast, parse(selector)); | |
| } | |
| query.parse = parse; | |
| query.match = match; | |
| query.matches = matches; | |
| return query.query = query; | |
| } | |
| if (typeof define === "function" && define.amd) { | |
| define(esqueryModule); | |
| } else if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = esqueryModule(); | |
| } else { | |
| this.esquery = esqueryModule(); | |
| } | |
| })(); |