UNPKG

shift-query

Version:

A query library for Shift AST using a CSS selector like query language.

444 lines (411 loc) 12.6 kB
(function () { "use strict"; var traverse = require("shift-traverser"); 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, l; 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 = []; traverse.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 for FunctionDeclaration, VariableDeclaration case "declaration": return node.type.slice(-11) === "Declaration" && node.type.substr(0, 8) !== 'Variable'; case "target": return node.type.slice(-6) === "Target"; case "expression": return node.type.slice(-10) === "Expression"; case "function": return ( node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowExpression" ); } 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 = traverse.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 = traverse.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 = traverse.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); traverse.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(); } })();