// node-jpath.js - is a library that allows filtering of JSON data based on pattern-like expressions (function(Array, undef) { var TRUE = !0, FALSE = !1, STRING = "string", FUNCTION = "function", PERIOD = ".", EMPTY = '', NULL = null, rxTokens = /([A-Za-z0-9_\*@\$\(\)]+(?:\[.+?\])?)/g, rxIndex = /^(\S+)\((\d+)\)$/, rxPairs = /(\(+)?([\w\.\(\)\$\_]+)(?:\s*)([\=\^\!\*\~\>\<\?\$]{1,2})\s*(?:\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^' \&\|\)\(]+)\s*(\)+)?/g, rxCondition = /(\S+)\[(.+)\]/, rxEscQuote = /\\('|")/g, app = Array.prototype.push, apc = Array.prototype.concat, /** * Private API * @type {Object} */ hidden = { /** * Function that strips wrapping quotes * @param {String} s String that contains quotes around a word * @return {String} Word without quotes */ qtrim: function(s) { return((!s.indexOf("'") || !s.indexOf('"')) && (s.slice(-1) === "'" || s.slice(-1) === '"')) ? s.slice(1, -1) : s; }, /** * Converts an object into an Array if it isn't * @param {Object} o Any type of object * @return {Array} Array of an object or an empty Array */ toArray: function(o) { return o instanceof Array ? o : (o === undef || o === NULL) ? [] : [o]; }, /** * Recursive function that walks through an object, extracting pattern matches * @param {String} pattern jPath expression * @param {Function} cfn Callback function used to run a custom comparisson * @param {Object|Array} obj An object or an Array that will be scanned for matches * @return {Array} Matching results */ traverse: function(pattern, cfn, obj) { var out, data = (obj || this.data), temp, tokens, token, idxToken, index, expToken, condition, tail, self = arguments.callee, found, i, j, l; if(data && typeof(pattern) === STRING) { tokens = pattern.match(rxTokens); //dot notation splitter //Get first token token = tokens[0]; //Trailing tokens tail = tokens.slice(1).join(PERIOD); if(data instanceof Array) { temp = []; for(i = 0, j; (j = data[i]) != NULL; i++) { found = self.call(this, token, cfn, j); if(((found instanceof Array) && found.length) || found !== undef) { app.call(temp, found); } } if(temp.length) { return tail ? self.call(this, tail, cfn, temp) : temp; } else { return; } } else if(token === "*") { return tail ? self.call(this, tail, cfn, data) : data; } else if(data[token] !== undef) { return tail ? self.call(this, tail, cfn, data[token]) : data[token]; } else if(rxIndex.test(token)) { idxToken = token.match(rxIndex); token = idxToken[1]; index = +idxToken[2]; temp = data[token]; return tail ? self.call(this, tail, cfn, (temp && temp.length) ? temp[index] : temp) : (temp && temp.length) ? temp[index] : temp; } else if(rxCondition.test(token)) { expToken = token.match(rxCondition); token = expToken[1]; condition = expToken[2]; var evalStr, isMatch, subset = token === "*" ? data : data[token], elem; if(subset instanceof Array) { temp = []; //Second loop here is faster than recursive call for(i = 0; (elem = subset[i]) != NULL; i++) { //Convert condition pairs to booleans evalStr = condition.replace(rxPairs, function(match, pl, left, operator, right, pr) { return [pl, hidden.testPairs.call(elem, left, right, operator, cfn), pr].join(EMPTY); }); //Evaluate expression isMatch = eval(evalStr); if(isMatch) { app.call(temp, elem); } } if(temp.length) { return tail ? self.call(this, tail, cfn, temp) : temp; } else { return; } } else { elem = subset; //Convert condition pairs to booleans evalStr = condition.replace(rxPairs, function(match, pl, left, operator, right, pr) { return [pl, hidden.testPairs.call(elem, left, right, operator, cfn), pr].join(EMPTY); }); //Evaluate expression isMatch = eval(evalStr); if(isMatch) { return tail ? self.call(this, tail, cfn, elem) : elem; } } } } return out; }, //Matches type of a to b matchTypes: function(a, b) { var _a, _b; switch(typeof(a)) { case STRING: _b = b + EMPTY; break; case "boolean": _b = b === "true" ? TRUE : FALSE; break; case "number": _b = +b; break; case "date": _b = new Date(b).valueOf(); _a = a.valueOf(); break; default: _b = b; } if(b === "null") { _b = NULL; } if(b === "undefined") { _b = undef; } return { left: (_a || a), right: _b }; }, //Condition functions testPairs: (function() { var conditions = { "=": function(l, r) { return l === r; }, "==": function(l, r) { return l === r; }, "!=": function(l, r) { return l !== r; }, "<": function(l, r) { return l < r; }, "<=": function(l, r) { return l <= r; }, ">": function(l, r) { return l > r; }, ">=": function(l, r) { return l >= r; }, "~=": function(l, r) { return(l + EMPTY).toLowerCase() === (r + EMPTY).toLowerCase(); }, "^=": function(l, r) { return !((l + EMPTY).indexOf(r)); }, "$=": function(l, r) { return(r + EMPTY) === (l + EMPTY).slice(-(r + EMPTY).length); }, "*=": function(l, r) { return(l + EMPTY).toLowerCase().indexOf((r + EMPTY).toLowerCase()) !== -1; } }; return function(left, right, operator, fn) { var out = FALSE, leftVal = left.indexOf(PERIOD) >= 0 ? hidden.traverse(left, NULL, this) : this[left], //We clean up r to remove wrapping quotes and escaped quotes (both single/dbl) pairs = hidden.matchTypes(leftVal, hidden.qtrim(right).trim().replace(rxEscQuote, '$1')); if(operator === "?") { if(typeof(fn) === FUNCTION) { out = fn.call(this, pairs.left, right); } } else { out = conditions[operator](pairs.left, pairs.right); } return out; }; })(), /** * Merges results of sibling nodes into a single Array * @param {String} pattern String pattern or results * @return {Array} Concatinated results */ merge: function(pattern) { var out = [], temp = hidden.toArray(pattern ? hidden.traverse.apply(this, arguments) : this.selection); out = apc.apply([], temp); return out; } }; /** * JPath Class * @param {Object|Array} obj Search subject */ function JPath(obj) { if(!(this instanceof JPath)) { return new JPath(obj); } this.data = obj || NULL; this.selection = []; } JPath.prototype = { /** * Sets search subject (source of data) * @param {Object|Array} obj Search subject * @return {this} */ from: function(obj) { this.data = obj; return this; }, /** * Returns a first match element * @return {Var} Any type of object located in the first element of the result Array */ first: function() { return this.selection.length ? this.selection[0] : NULL; }, /** * Returns a last match element * @return {Var} Any type of object located in the last element of the result Array */ last: function() { return this.selection.length ? this.selection.slice(-1)[0] : NULL; }, /** * Returns an exact match element located at idx position * @param {Number} idx Index * @return {Var} Any type of object located in result Array[idx] */ eq: function(idx) { return this.selection.length ? this.selection[idx] : NULL; }, /** * Applies matching pattern to an object * @param {String} pattern jPath expression * @param {Function} cfn Custom comparisson function * @param {Object|Array} obj Search subject object * @return {this} */ select: function(pattern, cfn, obj) { this.selection = hidden.merge.apply(this, arguments); return this; }, /** * Merges additional pattern-matching results with existing ones * @param {String} pattern jPath expression * @return {this} */ and: function(pattern) { this.selection = this.selection.concat(hidden.merge.apply(this, arguments)); return this; }, /** * Returns all matches * @return {Array} */ val: function() { return this.selection; } }; // export jpath object with select and filter methods jpath = {}; /** * Runs a select filter against an object and returns an instance of a JPath object * @param {Object|Array} obj Search subject * @param {String} pattern jPath expression * @param {Function} cfn Custom comparisson function (optional) * @return {JPath} Instance of a JPath object pre-filled with results */ jpath.select = function(obj, pattern, cfn) { return JPath(obj).select(pattern, cfn, NULL); }; /** * Returns results of the pattern-matching as an Array * @param {Object|Array} obj Search subject * @param {String} pattern jPath expression * @param {Function} cfn Custom comparisson function (optional) * @return {Array} Search results */ jpath.filter = function(obj, pattern, cfn) { return JPath(obj).select(pattern, cfn, NULL).val(); }; })(Array);