diff --git a/README.md b/README.md index a3ee334..b029583 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,14 @@ The following selectors are supported: * [subject indicator](http://dev.w3.org/csswg/selectors4/#subject): `!IfStatement > [name="foo"]` * class of AST node: `:statement`, `:expression`, `:declaration`, `:function`, or `:pattern` +For JSX-Support use: + +`var esquery = require("esquery/jsx");` + +instead of: + +`var esquery = require("esquery");` + +(Note the '/jsx' part) + [![Build Status](https://travis-ci.org/estools/esquery.png?branch=master)](https://travis-ci.org/estools/esquery) diff --git a/esquery.js b/esquery.js index d4d9fea..19c0d08 100644 --- a/esquery.js +++ b/esquery.js @@ -1,336 +1,16 @@ /* 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 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 === 'Literal' || - node.type === 'Identifier'; - 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; - } + var esquery = require("./esquery_init"); + var estraverse = require("estraverse"); if (typeof define === "function" && define.amd) { - define(esqueryModule); + define(function(){return esquery(estraverse);}); } else if (typeof module !== 'undefined' && module.exports) { - module.exports = esqueryModule(); + module.exports = esquery(estraverse); } else { - this.esquery = esqueryModule(); + this.esquery = esquery(estraverse); } })(); diff --git a/esquery_init.js b/esquery_init.js new file mode 100644 index 0000000..4fb7411 --- /dev/null +++ b/esquery_init.js @@ -0,0 +1,335 @@ +/* vim: set sw=4 sts=4 : */ +(function () { + + function esqueryModule(estraverse) { + + var parser = require('./parser'); + + var isArray = Array.isArray || function isArray(array) { + return {}.toString.call(array) === '[object Array]'; + }; + + var LEFT_SIDE = {}; + var RIGHT_SIDE = {}; + + /** + * 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 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 === 'Literal' || + node.type === 'Identifier'; + 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(function(){return esqueryModule;}); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = esqueryModule; + } else { + this.esquery = esqueryModule; + } + +})(); diff --git a/jsx.js b/jsx.js new file mode 100644 index 0000000..d249850 --- /dev/null +++ b/jsx.js @@ -0,0 +1,16 @@ +/* vim: set sw=4 sts=4 : */ +(function () { + + var esquery = require("./esquery_init"); + var estraverse_jsx = require("estraverse-fb"); + + + if (typeof define === "function" && define.amd) { + define(function(){return esquery(estraverse_jsx);}); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = esquery(estraverse_jsx); + } else { + this.esquery = esquery(estraverse_jsx); + } + +})(); diff --git a/package.json b/package.json index 2e5644d..b4626a8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "description": "A query library for ECMAScript AST using a CSS selector like query language.", "main": "esquery.js", "files": [ + "esquery_init.js", "esquery.js", + "jsx.js", "parser.js", "license.txt", "README.md" @@ -35,6 +37,7 @@ "node": ">=0.6" }, "dependencies": { - "estraverse": "^4.0.0" + "estraverse": "^4.0.0", + "estraverse-fb": "*" } }