query.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.json.query"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.json.query"] = true;
  8. dojo.provide("dojox.json.query");
  9. (function(){
  10. dojox.json._slice = function(obj,start,end,step){
  11. // handles slice operations: [3:6:2]
  12. var len=obj.length,results = [];
  13. end = end || len;
  14. start = (start < 0) ? Math.max(0,start+len) : Math.min(len,start);
  15. end = (end < 0) ? Math.max(0,end+len) : Math.min(len,end);
  16. for(var i=start; i<end; i+=step){
  17. results.push(obj[i]);
  18. }
  19. return results;
  20. }
  21. dojox.json._find = function e(obj,name){
  22. // handles ..name, .*, [*], [val1,val2], [val]
  23. // name can be a property to search for, undefined for full recursive, or an array for picking by index
  24. var results = [];
  25. function walk(obj){
  26. if(name){
  27. if(name===true && !(obj instanceof Array)){
  28. //recursive object search
  29. results.push(obj);
  30. }else if(obj[name]){
  31. // found the name, add to our results
  32. results.push(obj[name]);
  33. }
  34. }
  35. for(var i in obj){
  36. var val = obj[i];
  37. if(!name){
  38. // if we don't have a name we are just getting all the properties values (.* or [*])
  39. results.push(val);
  40. }else if(val && typeof val == 'object'){
  41. walk(val);
  42. }
  43. }
  44. }
  45. if(name instanceof Array){
  46. // this is called when multiple items are in the brackets: [3,4,5]
  47. if(name.length==1){
  48. // this can happen as a result of the parser becoming confused about commas
  49. // in the brackets like [@.func(4,2)]. Fixing the parser would require recursive
  50. // analsys, very expensive, but this fixes the problem nicely.
  51. return obj[name[0]];
  52. }
  53. for(var i = 0; i < name.length; i++){
  54. results.push(obj[name[i]]);
  55. }
  56. }else{
  57. // otherwise we expanding
  58. walk(obj);
  59. }
  60. return results;
  61. }
  62. dojox.json._distinctFilter = function(array, callback){
  63. // does the filter with removal of duplicates in O(n)
  64. var outArr = [];
  65. var primitives = {};
  66. for(var i=0,l=array.length; i<l; ++i){
  67. var value = array[i];
  68. if(callback(value, i, array)){
  69. if((typeof value == 'object') && value){
  70. // with objects we prevent duplicates with a marker property
  71. if(!value.__included){
  72. value.__included = true;
  73. outArr.push(value);
  74. }
  75. }else if(!primitives[value + typeof value]){
  76. // with primitives we prevent duplicates by putting it in a map
  77. primitives[value + typeof value] = true;
  78. outArr.push(value);
  79. }
  80. }
  81. }
  82. for(i=0,l=outArr.length; i<l; ++i){
  83. // cleanup the marker properties
  84. if(outArr[i]){
  85. delete outArr[i].__included;
  86. }
  87. }
  88. return outArr;
  89. }
  90. dojox.json.query = function(/*String*/query,/*Object?*/obj){
  91. // summary:
  92. // Performs a JSONQuery on the provided object and returns the results.
  93. // If no object is provided (just a query), it returns a "compiled" function that evaluates objects
  94. // according to the provided query.
  95. // query:
  96. // Query string
  97. // obj:
  98. // Target of the JSONQuery
  99. //
  100. // description:
  101. // JSONQuery provides a comprehensive set of data querying tools including filtering,
  102. // recursive search, sorting, mapping, range selection, and powerful expressions with
  103. // wildcard string comparisons and various operators. JSONQuery generally supersets
  104. // JSONPath and provides syntax that matches and behaves like JavaScript where
  105. // possible.
  106. //
  107. // JSONQuery evaluations begin with the provided object, which can referenced with
  108. // $. From
  109. // the starting object, various operators can be successively applied, each operating
  110. // on the result of the last operation.
  111. //
  112. // Supported Operators:
  113. // --------------------
  114. // * .property - This will return the provided property of the object, behaving exactly
  115. // like JavaScript.
  116. // * [expression] - This returns the property name/index defined by the evaluation of
  117. // the provided expression, behaving exactly like JavaScript.
  118. // * [?expression] - This will perform a filter operation on an array, returning all the
  119. // items in an array that match the provided expression. This operator does not
  120. // need to be in brackets, you can simply use ?expression, but since it does not
  121. // have any containment, no operators can be used afterwards when used
  122. // without brackets.
  123. // * [^?expression] - This will perform a distinct filter operation on an array. This behaves
  124. // as [?expression] except that it will remove any duplicate values/objects from the
  125. // result set.
  126. // * [/expression], [\expression], [/expression, /expression] - This performs a sort
  127. // operation on an array, with sort based on the provide expression. Multiple comma delimited sort
  128. // expressions can be provided for multiple sort orders (first being highest priority). /
  129. // indicates ascending order and \ indicates descending order
  130. // * [=expression] - This performs a map operation on an array, creating a new array
  131. // with each item being the evaluation of the expression for each item in the source array.
  132. // * [start:end:step] - This performs an array slice/range operation, returning the elements
  133. // from the optional start index to the optional end index, stepping by the optional step number.
  134. // * [expr,expr] - This a union operator, returning an array of all the property/index values from
  135. // the evaluation of the comma delimited expressions.
  136. // * .* or [*] - This returns the values of all the properties of the current object.
  137. // * $ - This is the root object, If a JSONQuery expression does not being with a $,
  138. // it will be auto-inserted at the beginning.
  139. // * @ - This is the current object in filter, sort, and map expressions. This is generally
  140. // not necessary, names are auto-converted to property references of the current object
  141. // in expressions.
  142. // * ..property - Performs a recursive search for the given property name, returning
  143. // an array of all values with such a property name in the current object and any subobjects
  144. // * expr = expr - Performs a comparison (like JS's ==). When comparing to
  145. // a string, the comparison string may contain wildcards * (matches any number of
  146. // characters) and ? (matches any single character).
  147. // * expr ~ expr - Performs a string comparison with case insensitivity.
  148. // * ..[?expression] - This will perform a deep search filter operation on all the objects and
  149. // subobjects of the current data. Rather than only searching an array, this will search
  150. // property values, arrays, and their children.
  151. // * $1,$2,$3, etc. - These are references to extra parameters passed to the query
  152. // function or the evaluator function.
  153. // * +, -, /, *, &, |, %, (, ), <, >, <=, >=, != - These operators behave just as they do
  154. // in JavaScript.
  155. //
  156. //
  157. //
  158. // | dojox.json.query(queryString,object)
  159. // and
  160. // | dojox.json.query(queryString)(object)
  161. // always return identical results. The first one immediately evaluates, the second one returns a
  162. // function that then evaluates the object.
  163. //
  164. // example:
  165. // | dojox.json.query("foo",{foo:"bar"})
  166. // This will return "bar".
  167. //
  168. // example:
  169. // | evaluator = dojox.json.query("?foo='bar'&rating>3");
  170. // This creates a function that finds all the objects in an array with a property
  171. // foo that is equals to "bar" and with a rating property with a value greater
  172. // than 3.
  173. // | evaluator([{foo:"bar",rating:4},{foo:"baz",rating:2}])
  174. // This returns:
  175. // | {foo:"bar",rating:4}
  176. //
  177. // example:
  178. // | evaluator = dojox.json.query("$[?price<15.00][\rating][0:10]");
  179. // This finds objects in array with a price less than 15.00 and sorts then
  180. // by rating, highest rated first, and returns the first ten items in from this
  181. // filtered and sorted list.
  182. var depth = 0;
  183. var str = [];
  184. query = query.replace(/"(\\.|[^"\\])*"|'(\\.|[^'\\])*'|[\[\]]/g,function(t){
  185. depth += t == '[' ? 1 : t == ']' ? -1 : 0; // keep track of bracket depth
  186. return (t == ']' && depth > 0) ? '`]' : // we mark all the inner brackets as skippable
  187. (t.charAt(0) == '"' || t.charAt(0) == "'") ? "`" + (str.push(t) - 1) :// and replace all the strings
  188. t;
  189. });
  190. var prefix = '';
  191. function call(name){
  192. // creates a function call and puts the expression so far in a parameter for a call
  193. prefix = name + "(" + prefix;
  194. }
  195. function makeRegex(t,a,b,c,d,e,f,g){
  196. // creates a regular expression matcher for when wildcards and ignore case is used
  197. return str[g].match(/[\*\?]/) || f == '~' ?
  198. "/^" + str[g].substring(1,str[g].length-1).replace(/\\([btnfr\\"'])|([^\w\*\?])/g,"\\$1$2").replace(/([\*\?])/g,"[\\w\\W]$1") + (f == '~' ? '$/i' : '$/') + ".test(" + a + ")" :
  199. t;
  200. }
  201. query.replace(/(\]|\)|push|pop|shift|splice|sort|reverse)\s*\(/,function(){
  202. throw new Error("Unsafe function call");
  203. });
  204. query = query.replace(/([^=]=)([^=])/g,"$1=$2"). // change the equals to comparisons
  205. replace(/@|(\.\s*)?[a-zA-Z\$_]+(\s*:)?/g,function(t){
  206. return t.charAt(0) == '.' ? t : // leave .prop alone
  207. t == '@' ? "$obj" :// the reference to the current object
  208. (t.match(/:|^(\$|Math|true|false|null)$/) ? "" : "$obj.") + t; // plain names should be properties of root... unless they are a label in object initializer
  209. }).
  210. replace(/\.?\.?\[(`\]|[^\]])*\]|\?.*|\.\.([\w\$_]+)|\.\*/g,function(t,a,b){
  211. var oper = t.match(/^\.?\.?(\[\s*\^?\?|\^?\?|\[\s*==)(.*?)\]?$/); // [?expr] and ?expr and [=expr and =expr
  212. if(oper){
  213. var prefix = '';
  214. if(t.match(/^\./)){
  215. // recursive object search
  216. call("dojox.json._find");
  217. prefix = ",true)";
  218. }
  219. call(oper[1].match(/\=/) ? "dojo.map" : oper[1].match(/\^/) ? "dojox.json._distinctFilter" : "dojo.filter");
  220. return prefix + ",function($obj){return " + oper[2] + "})";
  221. }
  222. oper = t.match(/^\[\s*([\/\\].*)\]/); // [/sortexpr,\sortexpr]
  223. if(oper){
  224. // make a copy of the array and then sort it using the sorting expression
  225. return ".concat().sort(function(a,b){" + oper[1].replace(/\s*,?\s*([\/\\])\s*([^,\\\/]+)/g,function(t,a,b){
  226. return "var av= " + b.replace(/\$obj/,"a") + ",bv= " + b.replace(/\$obj/,"b") + // FIXME: Should check to make sure the $obj token isn't followed by characters
  227. ";if(av>bv||bv==null){return " + (a== "/" ? 1 : -1) +";}\n" +
  228. "if(bv>av||av==null){return " + (a== "/" ? -1 : 1) +";}\n";
  229. }) + "return 0;})";
  230. }
  231. oper = t.match(/^\[(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)\]/); // slice [0:3]
  232. if(oper){
  233. call("dojox.json._slice");
  234. return "," + (oper[1] || 0) + "," + (oper[2] || 0) + "," + (oper[3] || 1) + ")";
  235. }
  236. if(t.match(/^\.\.|\.\*|\[\s*\*\s*\]|,/)){ // ..prop and [*]
  237. call("dojox.json._find");
  238. return (t.charAt(1) == '.' ?
  239. ",'" + b + "'" : // ..prop
  240. t.match(/,/) ?
  241. "," + t : // [prop1,prop2]
  242. "") + ")"; // [*]
  243. }
  244. return t;
  245. }).
  246. replace(/(\$obj\s*((\.\s*[\w_$]+\s*)|(\[\s*`([0-9]+)\s*`\]))*)(==|~)\s*`([0-9]+)/g,makeRegex). // create regex matching
  247. replace(/`([0-9]+)\s*(==|~)\s*(\$obj\s*((\.\s*[\w_$]+)|(\[\s*`([0-9]+)\s*`\]))*)/g,function(t,a,b,c,d,e,f,g){ // and do it for reverse =
  248. return makeRegex(t,c,d,e,f,g,b,a);
  249. });
  250. query = prefix + (query.charAt(0) == '$' ? "" : "$") + query.replace(/`([0-9]+|\])/g,function(t,a){
  251. //restore the strings
  252. return a == ']' ? ']' : str[a];
  253. });
  254. // create a function within this scope (so it can use expand and slice)
  255. var executor = eval("1&&function($,$1,$2,$3,$4,$5,$6,$7,$8,$9){var $obj=$;return " + query + "}");
  256. for(var i = 0;i<arguments.length-1;i++){
  257. arguments[i] = arguments[i+1];
  258. }
  259. return obj ? executor.apply(this,arguments) : executor;
  260. };
  261. })();
  262. }