-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathquery.js
409 lines (386 loc) · 16.5 KB
/
query.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
var jsonQuery = {};
jsonQuery._addPaths = function(obj, path) {
if((typeof obj !== 'object') || (obj === null)) {
return;
}
if (typeof obj[this.pathPropName] !== 'undefined') {
return;
}
Object.defineProperty(obj, this.pathPropName, {
value: path,
enumerable: false
});
for(var key in obj) {
if(obj.hasOwnProperty(key)) {
this._addPaths(obj[key], path + "['" + key + "']");
}
}
}
jsonQuery._slice = function(obj,start,end,step){
// handles slice operations: [3:6:2]
var len=obj.length,results = [];
end = end || len;
start = (start < 0) ? Math.max(0,start+len) : Math.min(len,start);
end = (end < 0) ? Math.max(0,end+len) : Math.min(len,end);
for(var i=start; i<end; i+=step){
results.push(obj[i]);
}
return results;
},
jsonQuery._find = function e(obj,name){
// handles ..name, .*, [*], [val1,val2], [val]
// name can be a property to search for, undefined for full recursive, or an array for picking by index
// Save some work and avoid saving to cache later
if (typeof obj === "undefined")
return undefined;
var results = [];
function walk(obj){
if(name){
if(name===true && !(obj instanceof Array)){
//recursive object search
results.push(obj);
}else if(obj[name]){
// found the name, add to our results
results.push(obj[name]);
}
}
for(var i in obj){
var val = obj[i];
if(!name){
// if we don't have a name we are just getting all the properties values (.* or [*])
results.push(val);
}else if(val && typeof val == 'object'){
walk(val);
}
}
}
// If using cache, take results from it if exists
if (this.cacheData && this.cache[name]) {
cached_results = this.cache[name].get(obj);
if (cached_results) {
return cached_results;
}
}
if(name instanceof Array){
// this is called when multiple items are in the brackets: [3,4,5]
if(name.length==1){
// this can happen as a result of the parser becoming confused about commas
// in the brackets like [@.func(4,2)]. Fixing the parser would require recursive
// analsys, very expensive, but this fixes the problem nicely.
return obj[name[0]];
}
for(var i = 0; i < name.length; i++){
results.push(obj[name[i]]);
}
}else{
// otherwise we expanding
walk(obj);
}
// If using cache, update it
if (this.cacheData) {
if (!this.cache[name]) {
this.cache[name] = new WeakMap();
}
this.cache[name].set(obj, results);
}
return results;
},
jsonQuery.map = function(arr, callback, thisObject, Ctr){
// summary:
// applies callback to each element of arr and returns
// an Array with the results
// arr: Array|String
// the array to iterate on. If a string, operates on
// individual characters.
// callback: Function|String
// a function is invoked with three arguments, (item, index,
// array), and returns a value
// thisObject: Object?
// may be used to scope the call to callback
// returns: Array
// description:
// This function corresponds to the JavaScript 1.6 Array.map() method, with one difference: when
// run over sparse arrays, this implementation passes the "holes" in the sparse array to
// the callback function with a value of undefined. JavaScript 1.6's map skips the holes in the sparse array.
// For more details, see:
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map
// example:
// | // returns [2, 3, 4, 5]
// | array.map([1, 2, 3, 4], function(item){ return item+1 });
// TODO: why do we have a non-standard signature here? do we need "Ctr"?
var i = 0, l = arr && arr.length || 0, out = new (Ctr || Array)(l);
if(l && typeof arr == "string") arr = arr.split("");
if(typeof callback == "string") callback = cache[callback] || buildFn(callback);
if(thisObject){
for(; i < l; ++i){
out[i] = callback.call(thisObject, arr[i], i, arr);
}
}else{
for(; i < l; ++i){
out[i] = callback(arr[i], i, arr);
}
}
return out; // Array
},
jsonQuery.filter = function(arr, callback, thisObject){
// summary:
// Returns a new Array with those items from arr that match the
// condition implemented by callback.
// arr: Array
// the array to iterate over.
// callback: Function|String
// a function that is invoked with three arguments (item,
// index, array). The return of this function is expected to
// be a boolean which determines whether the passed-in item
// will be included in the returned array.
// thisObject: Object?
// may be used to scope the call to callback
// returns: Array
// description:
// This function corresponds to the JavaScript 1.6 Array.filter() method, with one difference: when
// run over sparse arrays, this implementation passes the "holes" in the sparse array to
// the callback function with a value of undefined. JavaScript 1.6's filter skips the holes in the sparse array.
// For more details, see:
// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter
// example:
// | // returns [2, 3, 4]
// | array.filter([1, 2, 3, 4], function(item){ return item>1; });
// TODO: do we need "Ctr" here like in map()?
var i = 0, l = arr && arr.length || 0, out = [], value;
if(l && typeof arr == "string") arr = arr.split("");
if(typeof callback == "string") callback = cache[callback] || buildFn(callback);
if(thisObject){
for(; i < l; ++i){
value = arr[i];
if(callback.call(thisObject, value, i, arr)){
out.push(value);
}
}
}else{
for(; i < l; ++i){
value = arr[i];
if(callback(value, i, arr)){
out.push(value);
}
}
}
return out; // Array
},
jsonQuery._distinctFilter = function(array, callback){
// does the filter with removal of duplicates in O(n)
var outArr = [];
var primitives = {};
for(var i=0,l=array.length; i<l; ++i){
var value = array[i];
if(callback(value, i, array)){
if((typeof value == 'object') && value){
// with objects we prevent duplicates with a marker property
if(!value.__included){
value.__included = true;
outArr.push(value);
}
}else if(!primitives[value + typeof value]){
// with primitives we prevent duplicates by putting it in a map
primitives[value + typeof value] = true;
outArr.push(value);
}
}
}
for(i=0,l=outArr.length; i<l; ++i){
// cleanup the marker properties
if(outArr[i]){
delete outArr[i].__included;
}
}
return outArr;
},
jsonQuery.query = function(/*String*/query,/*Object?*/obj){
// summary:
// Performs a JSONQuery on the provided object and returns the results.
// If no object is provided (just a query), it returns a "compiled" function that evaluates objects
// according to the provided query.
// query:
// Query string
// obj:
// Target of the JSONQuery
// description:
// JSONQuery provides a comprehensive set of data querying tools including filtering,
// recursive search, sorting, mapping, range selection, and powerful expressions with
// wildcard string comparisons and various operators. JSONQuery generally supersets
// JSONPath and provides syntax that matches and behaves like JavaScript where
// possible.
//
// JSONQuery evaluations begin with the provided object, which can referenced with
// $. From
// the starting object, various operators can be successively applied, each operating
// on the result of the last operation.
//
// Supported Operators
// --------------------
//
// - .property - This will return the provided property of the object, behaving exactly
// like JavaScript.
// - [expression] - This returns the property name/index defined by the evaluation of
// the provided expression, behaving exactly like JavaScript.
// - [?expression] - This will perform a filter operation on an array, returning all the
// items in an array that match the provided expression. This operator does not
// need to be in brackets, you can simply use ?expression, but since it does not
// have any containment, no operators can be used afterwards when used
// without brackets.
// - [^?expression] - This will perform a distinct filter operation on an array. This behaves
// as [?expression] except that it will remove any duplicate values/objects from the
// result set.
// - [/expression], [\expression], [/expression, /expression] - This performs a sort
// operation on an array, with sort based on the provide expression. Multiple comma delimited sort
// expressions can be provided for multiple sort orders (first being highest priority). /
// indicates ascending order and \ indicates descending order
// - [=expression] - This performs a map operation on an array, creating a new array
// with each item being the evaluation of the expression for each item in the source array.
// - [start:end:step] - This performs an array slice/range operation, returning the elements
// from the optional start index to the optional end index, stepping by the optional step number.
// - [expr,expr] - This a union operator, returning an array of all the property/index values from
// the evaluation of the comma delimited expressions.
// - .* or [*] - This returns the values of all the properties of the current object.
// - $ - This is the root object, If a JSONQuery expression does not being with a $,
// it will be auto-inserted at the beginning.
// - @ - This is the current object in filter, sort, and map expressions. This is generally
// not necessary, names are auto-converted to property references of the current object
// in expressions.
// - ..property - Performs a recursive search for the given property name, returning
// an array of all values with such a property name in the current object and any subobjects
// - expr = expr - Performs a comparison (like JS's ==). When comparing to
// a string, the comparison string may contain wildcards * (matches any number of
// characters) and ? (matches any single character).
// - expr ~ expr - Performs a string comparison with case insensitivity.
// - ..[?expression] - This will perform a deep search filter operation on all the objects and
// subobjects of the current data. Rather than only searching an array, this will search
// property values, arrays, and their children.
// - $1,$2,$3, etc. - These are references to extra parameters passed to the query
// function or the evaluator function.
// - +, -, /, *, &, |, %, (, ), <, >, <=, >=, != - These operators behave just as they do
// in JavaScript.
//
// | jsonQuery.query(queryString,object)
// and
// | jsonQuery.query(queryString)(object)
// always return identical results. The first one immediately evaluates, the second one returns a
// function that then evaluates the object.
//
// example:
// | jsonQuery.query("foo",{foo:"bar"})
// This will return "bar".
//
// example:
// | evaluator = jsonQuery.query("?foo='bar'&rating>3");
// This creates a function that finds all the objects in an array with a property
// foo that is equals to "bar" and with a rating property with a value greater
// than 3.
// | evaluator([{foo:"bar",rating:4},{foo:"baz",rating:2}])
// This returns:
// | {foo:"bar",rating:4}
//
// example:
// | evaluator = jsonQuery.query("$[?price<15.00][\rating][0:10]");
// This finds objects in array with a price less than 15.00 and sorts then
// by rating, highest rated first, and returns the first ten items in from this
// filtered and sorted list.
// Add path properties across the object (should be very quick for consecutive applications)
if (jsonQuery.pathPropName) {
this._addPaths(obj, "");
}
var depth = 0;
var str = [];
query = query.replace(/"(\\.|[^"\\])*"|'(\\.|[^'\\])*'|[\[\]]/g,function(t){
depth += t == '[' ? 1 : t == ']' ? -1 : 0; // keep track of bracket depth
return (t == ']' && depth > 0) ? '`]' : // we mark all the inner brackets as skippable
(t.charAt(0) == '"' || t.charAt(0) == "'") ? "`" + (str.push(t) - 1) :// and replace all the strings
t;
});
var prefix = '';
function call(name){
// creates a function call and puts the expression so far in a parameter for a call
prefix = name + "(" + prefix;
}
function makeRegex(t,a,b,c,d,e,f,g){
// creates a regular expression matcher for when wildcards and ignore case is used
return str[g].match(/[\*\?]/) || f == '~' ?
"/^" + str[g].substring(1,str[g].length-1).replace(/\\([btnfr\\"'])|([^\w\*\?])/g,"\\$1$2").replace(/([\*\?])/g,"[\\w\\W]$1") + (f == '~' ? '$/i' : '$/') + ".test(" + a + ")" :
t;
}
query.replace(/(\]|\)|push|pop|shift|splice|sort|reverse)\s*\(/,function(){
throw new Error("Unsafe function call");
});
query = query.replace(/([^<>=]=)([^=])/g,"$1=$2"). // change the equals to comparisons except operators ==, <=, >=
replace(/@|(\.\s*)?[a-zA-Z\$_]+(\s*:)?/g,function(t){
return t.charAt(0) == '.' ? t : // leave .prop alone
t == '@' ? "$obj" :// the reference to the current object
(t.match(/:|^(\$|Math|true|false|null)$/) ? "" : "$obj.") + t; // plain names should be properties of root... unless they are a label in object initializer
}).
replace(/\.?\.?\[(`\]|[^\]])*\]|\?.*|\.\.([\w\$_]+)|\.\*/g,function(t,a,b){
var oper = t.match(/^\.?\.?(\[\s*\^?\?|\^?\?|\[\s*==)(.*?)\]?$/); // [?expr] and ?expr and [=expr and =expr
if(oper){
var prefix = '';
if(t.match(/^\./)){
// recursive object search
call("jsonQuery._find");
prefix = ",true)";
}
call(oper[1].match(/\=/) ? "jsonQuery.map" : oper[1].match(/\^/) ? "jsonQuery._distinctFilter" : "jsonQuery.filter");
return prefix + ",function($obj){return " + oper[2] + "})";
}
oper = t.match(/^\[\s*([\/\\].*)\]/); // [/sortexpr,\sortexpr]
if(oper){
// make a copy of the array and then sort it using the sorting expression
return ".concat().sort(function(a,b){" + oper[1].replace(/\s*,?\s*([\/\\])\s*([^,\\\/]+)/g,function(t,a,b){
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
";if(av>bv||bv==null){return " + (a== "/" ? 1 : -1) +";}\n" +
"if(bv>av||av==null){return " + (a== "/" ? -1 : 1) +";}\n";
}) + "return 0;})";
}
oper = t.match(/^\[(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)\]/); // slice [0:3]
if(oper){
call("jsonQuery._slice");
return "," + (oper[1] || 0) + "," + (oper[2] || 0) + "," + (oper[3] || 1) + ")";
}
if(t.match(/^\.\.|\.\*|\[\s*\*\s*\]|,/)){ // ..prop and [*]
call("jsonQuery._find");
return (t.charAt(1) == '.' ?
",'" + b + "'" : // ..prop
t.match(/,/) ?
"," + t : // [prop1,prop2]
"") + ")"; // [*]
}
return t;
}).
replace(/(\$obj\s*((\.\s*[\w_$]+\s*)|(\[\s*`([0-9]+)\s*`\]))*)(==|~)\s*`([0-9]+)/g,makeRegex). // create regex matching
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 =
return makeRegex(t,c,d,e,f,g,b,a);
});
query = prefix + (query.charAt(0) == '$' ? "" : "$") + query.replace(/`([0-9]+|\])/g,function(t,a){
//restore the strings
return a == ']' ? ']' : str[a];
});
// create a function within this scope (so it can use expand and slice)
var executor = eval("1&&function($,$1,$2,$3,$4,$5,$6,$7,$8,$9){var $obj=$;return " + query + "}");
for(var i = 0;i<arguments.length-1;i++){
arguments[i] = arguments[i+1];
}
return obj ? executor.apply(this,arguments) : executor;
}
// If this module is being used with node.js
if(typeof module != 'undefined' && module.exports) {
module.exports = function(params) {
// Merge params to this (compatible with ES5)
if (params) {
for (var attrname in params) { jsonQuery[attrname] = params[attrname]; }
}
if (jsonQuery['cacheData']) {
jsonQuery['cache'] = {};
}
return jsonQuery
}
}
// If this module is being used in browser
if (typeof window != 'undefined') {
window.jsonQuery = jsonQuery
}