diff --git a/.eslintrc.json b/.eslintrc similarity index 100% rename from .eslintrc.json rename to .eslintrc diff --git a/.npmignore b/.npmignore index f05b1f2..45d76d4 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,3 @@ node_modules test +test-helpers diff --git a/.remarkrc b/.remarkrc index 8abacbe..2bc8fab 100644 --- a/.remarkrc +++ b/.remarkrc @@ -2,12 +2,15 @@ "plugins": { "lint": { "no-multiple-toplevel-headings": false, + "no-consecutive-blank-lines": false, + "code-block-style": "fenced", + "no-html": false, "ordered-list-marker-value": "one", "list-item-spacing": false, "list-item-indent": false, "table-pipes": false, "table-pipe-alignment": false, - "no-consecutive-blank-lines": false + "table-cell-padding": false } } } diff --git a/CHANGES.md b/CHANGES.md index cb6f7ae..f212d27 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,66 +1,122 @@ -# JSONPath changes +# jsonpath-plus changes -## Jan 10, 2016 -- Add `@scalar()` type operator (in JavaScript mode, will also include) -- Version 0.14.0 +## 0.16.0 (January 14, 2017) -## Jan 5, 2016 -- Avoid double-encoding path in results -- Version 0.13.1 +- Breaking change: Give preference to treating special chars in a property + as special (override with backtick operator) +- Breaking feature: Add custom \` operator to allow unambiguous literal + sequences (if an initial backtick is needed, an additional one must + now be added) +- Fix: `toPathArray` caching bug +- Improvements: Performance optimizations +- Dev testing: Rename test file -## Dec 13, 2015 -- Breaking change (from version 0.11): Silently strip `~` and `^` operators and type operators such as `@string()` in `JSONPath.toPathString()` calls. -- Breaking change: Remove `Array.isArray` polyfill as no longer supporting IE <= 8 +## 0.15.0 (Mar 15, 2016) + +- Fix: Fixing support for sandbox in the case of functions +- Feature: Use `this` if present for global export +- Docs: Clarify function signature +- Docs: Update testing section +- Dev testing: Add in missing test for browser testing +- Dev testing: Add remark linting to testing process (#70) +- Dev testing: Lint JS test support files +- Dev testing: Split out tests into `eslint`, `remark`, `lint`, `nodeunit` +- Dev testing: Remove need for nodeunit build step +- Dev testing: Simplify nodeunit usage and make available + as `npm run browser-test` + +## 0.14.0 (Jan 10, 2016) + +- Feature: Add `@scalar()` type operator (in JavaScript mode, will also + include) + +## 0.13.1 (Jan 5, 2016) + +- Fix: Avoid double-encoding path in results + +## 0.13.0 (Dec 13, 2015) + +- Breaking change (from version 0.11): Silently strip `~` and `^` operators + and type operators such as `@string()` in `JSONPath.toPathString()` calls. +- Breaking change: Remove `Array.isArray` polyfill as no longer + supporting IE <= 8 - Feature: Allow omission of options first argument to `JSONPath` - Feature: Add `JSONPath.toPointer()` and "pointer" `resultType` option. -- Fix: Correctly support `callback` and `otherTypeCallback` as numbered arguments to `JSONPath`. +- Fix: Correctly support `callback` and `otherTypeCallback` as numbered + arguments to `JSONPath`. - Fix: Enhance Node checking to avoid issue reported with angular-mock -- Fix: Allow for `@` or other special characters in at-sign-prefixed property names (by use of `[?(@['...'])]` or `[(@['...'])]`). -- Version 0.13.0 +- Fix: Allow for `@` or other special characters in at-sign-prefixed + property names (by use of `[?(@['...'])]` or `[(@['...'])]`). + +## 0.12.0 (Dec 12, 2015 10:39pm) + +- Breaking change: Problems with upper-case letters in npm is causing + us to rename the package, so have renamed package to "jsonpath-plus" + (there are already package with lower-case "jsonpath" or "json-path"). + The new name also reflects that there have been changes to the + original spec. + +## 0.11.2 (Dec 12, 2015 10:36pm) + +- Docs: Actually add the warning in the README that problems in npm + with upper-case letters is causing us to rename to "jsonpath-plus" + (next version will actually apply the change). -## Dec 12, 2015 10:39pm -- Breaking change: Problems with upper-case letters in npm is causing us to rename the package, so have renamed package to "jsonpath-plus" (there are already package with lower-case "jsonpath" or "json-path"). The new name also reflects that -there have been changes to the original spec. -- Version 0.12.0 +## 0.11.1 (Dec 12, 2015 10:11pm) -## Dec 12, 2015 10:36pm -- Actually add the warning in the README that problems in npm with upper-case letters is causing us to rename to "jsonpath-plus" (next version will actually apply the change). -- Version 0.11.2 +- Docs: Give warning in README that problems in npm with upper-case letters + is causing us to rename to "jsonpath-plus" (next version will actually + apply the change). -## Dec 12, 2015 10:11pm -- Give warning in README that problems in npm with upper-case letters is causing us to rename to "jsonpath-plus" (next version will actually apply the change). -- Version 0.11.1 +## 0.11.0 (Dec 12, 2015) -## Dec 12, 2015 -- Breaking change: For unwrapped results, return `undefined` instead of `false` upon failure to find path (to allow distinguishing of `undefined`--a non-allowed JSON value--from the valid JSON values, `null` or `false`) and return the exact value upon falsy single results (in order to allow return of `null`) +- Breaking change: For unwrapped results, return `undefined` instead + of `false` upon failure to find path (to allow distinguishing of + `undefined`--a non-allowed JSON value--from the valid JSON values, + `null` or `false`) and return the exact value upon falsy single + results (in order to allow return of `null`) - Deprecated: Use of `jsonPath.eval()`; use new class-based API instead - Feature: AMD export -- Feature: By using `self` instead of `window` export, allow JSONPath to be trivially imported into web workers, without breaking compatibility in normal scenarios. See [MDN on self](https://developer.mozilla.org/en-US/docs/Web/API/Window/self) -- Feature: Offer new class-based API and object-based arguments (with option to run new queries via `evaluate()` method without resupplying config) +- Feature: By using `self` instead of `window` export, allow JSONPath + to be trivially imported into web workers, without breaking + compatibility in normal scenarios. See [MDN on self](https://developer.mozilla.org/en-US/docs/Web/API/Window/self) +- Feature: Offer new class-based API and object-based arguments (with + option to run new queries via `evaluate()` method without resupplying config) - Feature: Allow new `preventEval=true` and `autostart=false` option -- Feature: Allow new callback option to allow a callback function to execute as each final result node is obtained -- Feature: Allow type operators: JavaScript types (`@boolean()`, `@number()`, `@string()`), other fundamental JavaScript types (`@null()`, `@object()`, `@array()`), the JSONSchema-added type, `@integer()`, and the following non-JSON types that can nevertheless be used with JSONPath when querying non-JSON JavaScript objects (`@undefined()`, `@function()`, `@nonFinite()`). Finally, `@other()` is made available in conjunction with a new callback option, `otherTypeCallback`, can be used to allow user-defined type detection (at least until JSON Schema awareness may be provided). -- Feature: Support "parent" and "parentProperty" for resultType along with "all" (which also includes "path" and "value" together) -- Feature: Support custom `@parent`, `@parentProperty`, `@property` (in addition to custom property `@path`) inside evaluations +- Feature: Allow new callback option to allow a callback function to execute as + each final result node is obtained +- Feature: Allow type operators: JavaScript types (`@boolean()`, `@number()`, + `@string()`), other fundamental JavaScript types (`@null()`, `@object()`, + `@array()`), the JSONSchema-added type, `@integer()`, and the following + non-JSON types that can nevertheless be used with JSONPath when querying + non-JSON JavaScript objects (`@undefined()`, `@function()`, `@nonFinite()`). + Finally, `@other()` is made available in conjunction with a new callback + option, `otherTypeCallback`, can be used to allow user-defined type + detection (at least until JSON Schema awareness may be provided). +- Feature: Support "parent" and "parentProperty" for resultType along with + "all" (which also includes "path" and "value" together) +- Feature: Support custom `@parent`, `@parentProperty`, `@property` (in + addition to custom property `@path`) inside evaluations - Feature: Support a custom operator (`~`) to allow grabbing of property names -- Feature: Support `$` for retrieval of root, and document this as well as `$..` behavior -- Feature: Expose cache on `JSONPath.cache` for those who wish to preserve and reuse it -- Feature: Expose class methods `toPathString` for converting a path as array into a (normalized) path as string and `toPathArray` for the reverse (though accepting unnormalized strings as well as normalized) +- Feature: Support `$` for retrieval of root, and document this as well as + `$..` behavior +- Feature: Expose cache on `JSONPath.cache` for those who wish to preserve and + reuse it +- Feature: Expose class methods `toPathString` for converting a path as array + into a (normalized) path as string and `toPathArray` for the reverse (though + accepting unnormalized strings as well as normalized) - Fix: Allow `^` as property name - Fix: Support `.` within properties - Fix: `@path` in index/property evaluations -- Version 0.11 -## Oct 23, 2013 +## 0.10.0 (Oct 23, 2013) -- Support for parent selection via `^` -- Access current path via `@path` in test statements -- Allowing for multi-statement evals -- Performance improvements -- Version 0.10 +- Feature: Support for parent selection via `^` +- Feature: Access current path via `@path` in test statements +- Feature: Allowing for multi-statement evals +- Improvements: Performance -## Mar 28, 2012 +## 0.9.0 (Mar 28, 2012) -- Support a sandbox arg to eval -- Use vm.runInNewContext in place of eval -- Version 0.9.0 +- Feature: Support a sandbox arg to eval +- Improvements: Use `vm.runInNewContext` in place of eval diff --git a/README.md b/README.md index bf08adf..f8bfe81 100644 --- a/README.md +++ b/README.md @@ -3,41 +3,60 @@ Analyse, transform, and selectively extract data from JSON documents (and JavaScript objects). +**Note that `jsonpath-plus` is currently suffering from [performance problems](https://github.com/s3u/JSONPath/issues/14) +and the maintainers are not currently able to work on resolving. +You may wish to use [jsonpath](https://www.npmjs.com/package/jsonpath) +to avoid this problem (though noting that it does not include the +proprietary features added to `jsonpath-plus`).** + # Install - npm install jsonpath-plus +```shell + npm install jsonpath-plus +``` # Usage ## Syntax -In node.js: +In Node.js: ```js var JSONPath = require('jsonpath-plus'); - JSONPath({json: obj, path: path, callback: callback}); + var result = JSONPath({json: obj, path: path}); ``` -For browser usage you can directly include `lib/jsonpath.js`, no browserify -magic necessary: +For browser usage you can directly include `lib/jsonpath.js`; no Browserify +magic is necessary: ```html ``` -An alternative syntax is available as: +The full signature available is: ```js - JSONPath([options,] path, obj, callback, otherTypeCallback); + var result = JSONPath([options,] path, json, callback, otherTypeCallback); ``` +The arguments `path`, `json`, `callback`, and `otherTypeCallback` +can alternatively be expressed (along with any other of the +available properties) on `options`. + +Note that `result` will contain all items found (optionally +wrapped into an array) whereas `callback` can be used if you +wish to perform some operation as each item is discovered, with +the callback function being executed 0 to N times depending +on the number of independent items to be found in the result. +See the docs below for more on `JSONPath`'s available arguments. + The following format is now deprecated: ```js - jsonPath.eval(options, obj, path); + jsonPath.eval(options, json, path); ``` ## Properties @@ -66,11 +85,13 @@ evaluate method (as the first argument) include: expressions; see the Syntax section for details.) - ***wrap*** (**default: true**) - Whether or not to wrap the results in an array. If `wrap` is set to false, and no results are found, - `undefined` will be returned (as opposed to an empty array with - `wrap` set to true). If `wrap` is set to false and a single result + `undefined` will be returned (as opposed to an empty array when + `wrap` is set to true). If `wrap` is set to false and a single result is found, that result will be the only item returned (not within an array). An array will still be returned if multiple results are - found, however. + found, however. To avoid ambiguities (in the case where it is necessary + to distinguish between a result which is a failure and one which is an + empty array), it is recommended to switch the default to `false`. - ***preventEval*** (**default: false**) - Although JavaScript evaluation expressions are allowed by default, for security reasons (if one is operating on untrusted user input, for example), one may wish to @@ -212,31 +233,32 @@ Please note that the XPath examples below do not distinguish between retrieving elements and their text content (except where useful for comparisons or to prevent ambiguity). -XPath | JSONPath | Result | Notes -------------------- | ---------------------- | ------------------------------------- | ----- -/store/book/author | $.store.book[*].author | The authors of all books in the store | Can also be represented without the `$.` as `store.book[*].author` (though this is not present in the original spec) +| XPath | JSONPath | Result | Notes | +| ----------------- | ---------------------- | ------------------------------------- | ----- | +/store/book/author | $.store.book\[*].author | The authors of all books in the store | Can also be represented without the `$.` as `store.book[*].author` (though this is not present in the original spec) //author | $..author | All authors | /store/* | $.store.* | All things in store, which are its books (a book array) and a red bicycle (a bicycle object).| /store//price | $.store..price | The price of everything in the store. | -//book[3] | $..book[2] | The third book (book object) | -//book[last()] | $..book[(@.length-1)]
$..book[-1:] | The last book in order.| To access a property with a special character, utilize `[(@['...'])]` for the filter (this particular feature is not present in the original spec) -//book[position()<3]| $..book[0,1]
$..book[:2]| The first two books | -//book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0 | $..book[0][category,author]| The categories and authors of all books | -//book[isbn] | $..book[?(@.isbn)] | Filter all books with an ISBN number | To access a property with a special character, utilize `[?@['...']]` for the filter (this particular feature is not present in the original spec) -//book[price<10] | $..book[?(@.price<10)] | Filter all books cheaper than 10 | -| //\*[name() = 'price' and . != 8.95] | $..\*[?(@property === 'price' && @ !== 8.95)] | Obtain all property values of objects whose property is price and which does not equal 8.95 | +//book\[3] | $..book\[2] | The third book (book object) | +//book\[last()] | $..book\[(@.length-1)]
$..book\[-1:] | The last book in order.| To access a property with a special character, utilize `[(@['...'])]` for the filter (this particular feature is not present in the original spec) +//book\[position()<3]| $..book\[0,1]
$..book\[:2]| The first two books | +//book/*\[self::category\|self::author] or //book/(category,author) in XPath 2.0 | $..book\[0]\[category,author]| The categories and authors of all books | +//book\[isbn] | $..book\[?(@.isbn)] | Filter all books with an ISBN number | To access a property with a special character, utilize `[?@['...']]` for the filter (this particular feature is not present in the original spec) +//book\[price<10] | $..book\[?(@.price<10)] | Filter all books cheaper than 10 | +| //\*\[name() = 'price' and . != 8.95] | $..\*\[?(@property === 'price' && @ !== 8.95)] | Obtain all property values of objects whose property is price and which does not equal 8.95 | / | $ | The root of the JSON object (i.e., the whole object itself) | //\*/\*\|//\*/\*/text() | $..* | All Elements (and text) beneath root in an XML document. All members of a JSON structure beneath the root. | //* | $.. | All Elements in an XML document. All parent components of a JSON structure including root. | This behavior was not directly specified in the original spec -//*[price>19]/.. | $..[?(@.price>19)]^ | Parent of those specific items with a price greater than 19 (i.e., the store value as the parent of the bicycle and the book array as parent of an individual book) | Parent (caret) not documented in the original spec +//*\[price>19]/.. | $..\[?(@.price>19)]^ | Parent of those specific items with a price greater than 19 (i.e., the store value as the parent of the bicycle and the book array as parent of an individual book) | Parent (caret) not documented in the original spec /store/*/name() (in XPath 2.0) | $.store.*~ | The property names of the store sub-object ("book" and "bicycle"). Useful with wildcard properties. | Property name (tilde) is not present in the original spec -/store/book\[not(. is /store/book\[1\])\] (in XPath 2.0) | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec -//book[parent::\*/bicycle/color = "red"]/category | $..book[?(@parent.bicycle && @parent.bicycle.color === "red")].category | Grabs all categories of books where the parent object of the book has a bicycle child whose color is red (i.e., all the books) | @parent is not present in the original spec -//book/*[name() != 'category'] | $..book.*[?(@property !== "category")] | Grabs all children of "book" except for "category" ones | @property is not present in the original spec -//book/*[position() != 0] | $..book[?(@property !== 0)] | Grabs all books whose property (which, being that we are reaching inside an array, is the numeric index) is not 0 | @property is not present in the original spec -/store/\*/\*[name(parent::*) != 'book'] | $.store.*[?(@parentProperty !== "book")] | Grabs the grandchildren of store whose parent property is not book (i.e., bicycle's children, "color" and "price") | @parentProperty is not present in the original spec -//book[count(preceding-sibling::\*) != 0]/\*/text() | $..book.*[?(@parentProperty !== 0)] | Get the property values of all book instances whereby the parent property of these values (i.e., the array index holding the book item parent object) is not 0 | @parentProperty is not present in the original spec +/store/book\[not(. is /store/book\[1\])\] (in XPath 2.0) | $.store.book\[?(@path !== "$\[\'store\']\[\'book\']\[0]")] | All books besides that at the path pointing to the first | @path not present in the original spec +//book\[parent::\*/bicycle/color = "red"]/category | $..book\[?(@parent.bicycle && @parent.bicycle.color === "red")].category | Grabs all categories of books where the parent object of the book has a bicycle child whose color is red (i.e., all the books) | @parent is not present in the original spec +//book/*\[name() != 'category'] | $..book.*\[?(@property !== "category")] | Grabs all children of "book" except for "category" ones | @property is not present in the original spec +//book/*\[position() != 0] | $..book\[?(@property !== 0)] | Grabs all books whose property (which, being that we are reaching inside an array, is the numeric index) is not 0 | @property is not present in the original spec +/store/\*/\*\[name(parent::*) != 'book'] | $.store.*\[?(@parentProperty !== "book")] | Grabs the grandchildren of store whose parent property is not book (i.e., bicycle's children, "color" and "price") | @parentProperty is not present in the original spec +//book\[count(preceding-sibling::\*) != 0]/\*/text() | $..book.*\[?(@parentProperty !== 0)] | Get the property values of all book instances whereby the parent property of these values (i.e., the array index holding the book item parent object) is not 0 | @parentProperty is not present in the original spec //book/../\*\[. instance of element(\*, xs:decimal)\] (in XPath 2.0) | $..book..\*@number() | Get the numeric values within the book array | @number(), the other basic types (@boolean(), @string()), other low-level derived types (@null(), @object(), @array()), the JSONSchema-added type, @integer(), the compound type @scalar() (which also accepts `undefined` and non-finite numbers for JavaScript objects as well as all of the basic non-object/non-function types), the type, @other(), to be used in conjunction with a user-defined callback (see `otherTypeCallback`) and the following non-JSON types that can nevertheless be used with JSONPath when querying non-JSON JavaScript objects (@undefined(), @function(), @nonFinite()) are not present in the original spec +| | `` ` `` (e.g., `` `$`` to match a property literally named `$`) | Escapes the entire sequence following (to be treated as a literal) | `\`` is not present in the original spec Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) @@ -262,17 +284,21 @@ whereas in XPath, they use a single equal sign. # Development -Running the tests on node: `npm test`. For in-browser tests: +Running the tests on Node: + +```shell +npm test +``` + +For in-browser tests: -- Ensure that nodeunit is browser-compiled: `cd node_modules/nodeunit; make browser;` - Serve the js/html files: -```sh - node -e "require('http').createServer(function(req,res) { \ - var s = require('fs').createReadStream('.' + req.url); \ - s.pipe(res); s.on('error', function() {}); }).listen(8082);" +```shell +npm run browser-test ``` -- To run the tests visit [http://localhost:8082/test/test.html](). + +- Visit [http://localhost:8082/test/](http://localhost:8082/test/). # License diff --git a/lib/jsonpath-min.js b/lib/jsonpath-min.js index f197088..bbe3cda 100644 --- a/lib/jsonpath-min.js +++ b/lib/jsonpath-min.js @@ -1 +1 @@ -var module;!function(require){"use strict";function push(t,e){return t=t.slice(),t.push(e),t}function unshift(t,e){return e=e.slice(),e.unshift(t),e}function NewError(t){this.avoidNew=!0,this.value=t,this.message='JSONPath should not be called with "new" (it prevents return of (unwrapped) scalar values)'}function JSONPath(t,e,r,a,n){if(!(this instanceof JSONPath))try{return new JSONPath(t,e,r,a,n)}catch(i){if(!i.avoidNew)throw i;return i.value}"string"==typeof t&&(n=a,a=r,r=e,e=t,t={}),t=t||{};var o=t.hasOwnProperty("json")&&t.hasOwnProperty("path");if(this.json=t.json||r,this.path=t.path||e,this.resultType=t.resultType&&t.resultType.toLowerCase()||"value",this.flatten=t.flatten||!1,this.wrap=t.hasOwnProperty("wrap")?t.wrap:!0,this.sandbox=t.sandbox||{},this.preventEval=t.preventEval||!1,this.parent=t.parent||null,this.parentProperty=t.parentProperty||null,this.callback=t.callback||a||null,this.otherTypeCallback=t.otherTypeCallback||n||function(){throw new Error("You must supply an otherTypeCallback callback option with the @other() operator.")},t.autostart!==!1){var s=this.evaluate({path:o?t.path:e,json:o?t.json:r});if(!s||"object"!=typeof s)throw new NewError(s);return s}}var isNode=module&&!!module.exports,allowedResultTypes=["value","path","pointer","parent","parentProperty","all"],vm=isNode?require("vm"):{runInNewContext:function(expr,context){return eval(Object.keys(context).reduce(function(t,e){return"var "+e+"="+JSON.stringify(context[e]).replace(/\u2028|\u2029/g,function(t){return"\\u202"+("\u2028"===t?"8":"9")})+";"+t},expr))}};JSONPath.prototype.evaluate=function(t,e,r,a){var n=this,i=this.flatten,o=this.wrap,s=this.parent,p=this.parentProperty;if(this.currResultType=this.resultType,this.currPreventEval=this.preventEval,this.currSandbox=this.sandbox,r=r||this.callback,this.currOtherTypeCallback=a||this.otherTypeCallback,e=e||this.json,t=t||this.path,t&&"object"==typeof t){if(!t.path)throw new Error('You must supply a "path" property when providing an object argument to JSONPath.evaluate().');e=t.hasOwnProperty("json")?t.json:e,i=t.hasOwnProperty("flatten")?t.flatten:i,this.currResultType=t.hasOwnProperty("resultType")?t.resultType:this.currResultType,this.currSandbox=t.hasOwnProperty("sandbox")?t.sandbox:this.currSandbox,o=t.hasOwnProperty("wrap")?t.wrap:o,this.currPreventEval=t.hasOwnProperty("preventEval")?t.preventEval:this.currPreventEval,r=t.hasOwnProperty("callback")?t.callback:r,this.currOtherTypeCallback=t.hasOwnProperty("otherTypeCallback")?t.otherTypeCallback:this.currOtherTypeCallback,s=t.hasOwnProperty("parent")?t.parent:s,p=t.hasOwnProperty("parentProperty")?t.parentProperty:p,t=t.path}if(s=s||null,p=p||null,Array.isArray(t)&&(t=JSONPath.toPathString(t)),t&&e&&-1!==allowedResultTypes.indexOf(this.currResultType)){this._obj=e;var h=JSONPath.toPathArray(t);"$"===h[0]&&h.length>1&&h.shift();var l=this._trace(h,e,["$"],s,p,r);return l=l.filter(function(t){return t&&!t.isParentSelector}),l.length?1!==l.length||o||Array.isArray(l[0].value)?l.reduce(function(t,e){var r=n._getPreferredOutput(e);return i&&Array.isArray(r)?t=t.concat(r):t.push(r),t},[]):this._getPreferredOutput(l[0]):o?[]:void 0}},JSONPath.prototype._getPreferredOutput=function(t){var e=this.currResultType;switch(e){case"all":return t.path="string"==typeof t.path?t.path:JSONPath.toPathString(t.path),t;case"value":case"parent":case"parentProperty":return t[e];case"path":return JSONPath.toPathString(t[e]);case"pointer":return JSONPath.toPointer(t.path)}},JSONPath.prototype._handleCallback=function(t,e,r){if(e){var a=this._getPreferredOutput(t);t.path="string"==typeof t.path?t.path:JSONPath.toPathString(t.path),e(a,r,t)}},JSONPath.prototype._trace=function(t,e,r,a,n,i){function o(t){c=c.concat(t)}var s,p=this;if(!t.length)return s={path:r,value:e,parent:a,parentProperty:n},this._handleCallback(s,i,"value"),s;var h=t[0],l=t.slice(1),c=[];if(e&&Object.prototype.hasOwnProperty.call(e,h))o(this._trace(l,e[h],push(r,h),e,h,i));else if("*"===h)this._walk(h,l,e,r,a,n,i,function(t,e,r,a,n,i,s,h){o(p._trace(unshift(t,r),a,n,i,s,h))});else if(".."===h)o(this._trace(l,e,r,a,n,i)),this._walk(h,l,e,r,a,n,i,function(t,e,r,a,n,i,s,h){"object"==typeof a[t]&&o(p._trace(unshift(e,r),a[t],push(n,t),a,t,h))});else if("("===h[0]){if(this.currPreventEval)throw new Error("Eval [(expr)] prevented in JSONPath expression.");o(this._trace(unshift(this._eval(h,e,r[r.length-1],r.slice(0,-1),a,n),l),e,r,a,n,i))}else{if("^"===h)return r.length?{path:r.slice(0,-1),expr:l,isParentSelector:!0}:[];if("~"===h)return s={path:push(r,h),value:n,parent:a,parentProperty:null},this._handleCallback(s,i,"property"),s;if("$"===h)o(this._trace(l,e,r,null,null,i));else if(0===h.indexOf("?(")){if(this.currPreventEval)throw new Error("Eval [?(expr)] prevented in JSONPath expression.");this._walk(h,l,e,r,a,n,i,function(t,e,r,a,n,i,s,h){p._eval(e.replace(/^\?\((.*?)\)$/,"$1"),a[t],t,n,i,s)&&o(p._trace(unshift(t,r),a,n,i,s,h))})}else if(h.indexOf(",")>-1){var u,f;for(u=h.split(","),f=0;fp;p++)s(p,t,e,r,a,n,i,o);else if("object"==typeof r)for(l in r)Object.prototype.hasOwnProperty.call(r,l)&&s(l,t,e,r,a,n,i,o)},JSONPath.prototype._slice=function(t,e,r,a,n,i,o){if(Array.isArray(r)){var s,p=r.length,h=t.split(":"),l=h[0]&&parseInt(h[0],10)||0,c=h[1]&&parseInt(h[1],10)||p,u=h[2]&&parseInt(h[2],10)||1;l=0>l?Math.max(0,l+p):Math.min(p,l),c=0>c?Math.max(0,c+p):Math.min(p,c);var f=[];for(s=l;c>s;s+=u)f=f.concat(this._trace(unshift(s,e),r,a,n,i,o));return f}},JSONPath.prototype._eval=function(t,e,r,a,n,i){if(!this._obj||!e)return!1;t.indexOf("@parentProperty")>-1&&(this.currSandbox._$_parentProperty=i,t=t.replace(/@parentProperty/g,"_$_parentProperty")),t.indexOf("@parent")>-1&&(this.currSandbox._$_parent=n,t=t.replace(/@parent/g,"_$_parent")),t.indexOf("@property")>-1&&(this.currSandbox._$_property=r,t=t.replace(/@property/g,"_$_property")),t.indexOf("@path")>-1&&(this.currSandbox._$_path=JSONPath.toPathString(a.concat([r])),t=t.replace(/@path/g,"_$_path")),t.match(/@([\.\s\)\[])/)&&(this.currSandbox._$_v=e,t=t.replace(/@([\.\s\)\[])/g,"_$_v$1"));try{return vm.runInNewContext(t,this.currSandbox)}catch(o){throw console.log(o),new Error("jsonPath: "+o.message+": "+t)}},JSONPath.cache={},JSONPath.toPathString=function(t){var e,r,a=t,n="$";for(e=1,r=a.length;r>e;e++)/^(~|\^|@.*?\(\))$/.test(a[e])||(n+=/^[0-9*]+$/.test(a[e])?"["+a[e]+"]":"['"+a[e]+"']");return n},JSONPath.toPointer=function(t){var e,r,a=t,n="";for(e=1,r=a.length;r>e;e++)/^(~|\^|@.*?\(\))$/.test(a[e])||(n+="/"+a[e].toString().replace(/\~/g,"~0").replace(/\//g,"~1"));return n},JSONPath.toPathArray=function(t){var e=JSONPath.cache;if(e[t])return e[t];var r=[],a=t.replace(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/g,";$&;").replace(/[\['](\??\(.*?\))[\]']/g,function(t,e){return"[#"+(r.push(e)-1)+"]"}).replace(/\['([^'\]]*)'\]/g,function(t,e){return"['"+e.replace(/\./g,"%@%").replace(/~/g,"%%@@%%")+"']"}).replace(/~/g,";~;").replace(/'?\.'?(?![^\[]*\])|\['?/g,";").replace(/%@%/g,".").replace(/%%@@%%/g,"~").replace(/(?:;)?(\^+)(?:;)?/g,function(t,e){return";"+e.split("").join(";")+";"}).replace(/;;;|;;/g,";..;").replace(/;$|'?\]|'$/g,""),n=a.split(";").map(function(t){var e=t.match(/#([0-9]+)/);return e&&e[1]?r[e[1]]:t});return e[t]=n,e[t]},JSONPath.eval=function(t,e,r){return JSONPath(r,e,t)},"function"==typeof define&&define.amd?define(function(){return JSONPath}):isNode?module.exports=JSONPath:(self.jsonPath={eval:JSONPath.eval},self.JSONPath=JSONPath)}("undefined"==typeof require?null:require); \ No newline at end of file +var module;!function(glbl,require){"use strict";function push(t,e){return t=t.slice(),t.push(e),t}function unshift(t,e){return e=e.slice(),e.unshift(t),e}function NewError(t){this.avoidNew=!0,this.value=t,this.message='JSONPath should not be called with "new" (it prevents return of (unwrapped) scalar values)'}function JSONPath(t,e,r,a,n){if(!(this instanceof JSONPath))try{return new JSONPath(t,e,r,a,n)}catch(s){if(!s.avoidNew)throw s;return s.value}"string"==typeof t&&(n=a,a=r,r=e,e=t,t={}),t=t||{};var i=t.hasOwnProperty("json")&&t.hasOwnProperty("path");if(this.json=t.json||r,this.path=t.path||e,this.resultType=t.resultType&&t.resultType.toLowerCase()||"value",this.flatten=t.flatten||!1,this.wrap=t.hasOwnProperty("wrap")?t.wrap:!0,this.sandbox=t.sandbox||{},this.preventEval=t.preventEval||!1,this.parent=t.parent||null,this.parentProperty=t.parentProperty||null,this.callback=t.callback||a||null,this.otherTypeCallback=t.otherTypeCallback||n||function(){throw new Error("You must supply an otherTypeCallback callback option with the @other() operator.")},t.autostart!==!1){var o=this.evaluate({path:i?t.path:e,json:i?t.json:r});if(!o||"object"!=typeof o)throw new NewError(o);return o}}var isNode=module&&!!module.exports,allowedResultTypes=["value","path","pointer","parent","parentProperty","all"];Array.prototype.includes||(Array.prototype.includes=function(t){return this.indexOf(t)>-1}),String.prototype.includes||(String.prototype.includes=function(t){return this.indexOf(t)>-1});var moveToAnotherArray=function(t,e,r){for(var a=0,n=t.length;n>a;a++){var s=t[a];r(s)&&e.push(t.splice(a--,1)[0])}},vm=isNode?require("vm"):{runInNewContext:function(expr,context){var keys=Object.keys(context),funcs=[];moveToAnotherArray(keys,funcs,function(t){return"function"==typeof context[t]});var code=funcs.reduce(function(t,e){return"var "+e+"="+context[e].toString()+";"+t},"");return code+=keys.reduce(function(t,e){return"var "+e+"="+JSON.stringify(context[e]).replace(/\u2028|\u2029/g,function(t){return"\\u202"+("\u2028"===t?"8":"9")})+";"+t},expr),eval(code)}};JSONPath.prototype.evaluate=function(t,e,r,a){var n=this,s=this.flatten,i=this.wrap,o=this.parent,h=this.parentProperty;if(this.currResultType=this.resultType,this.currPreventEval=this.preventEval,this.currSandbox=this.sandbox,r=r||this.callback,this.currOtherTypeCallback=a||this.otherTypeCallback,e=e||this.json,t=t||this.path,t&&"object"==typeof t){if(!t.path)throw new Error('You must supply a "path" property when providing an object argument to JSONPath.evaluate().');e=t.hasOwnProperty("json")?t.json:e,s=t.hasOwnProperty("flatten")?t.flatten:s,this.currResultType=t.hasOwnProperty("resultType")?t.resultType:this.currResultType,this.currSandbox=t.hasOwnProperty("sandbox")?t.sandbox:this.currSandbox,i=t.hasOwnProperty("wrap")?t.wrap:i,this.currPreventEval=t.hasOwnProperty("preventEval")?t.preventEval:this.currPreventEval,r=t.hasOwnProperty("callback")?t.callback:r,this.currOtherTypeCallback=t.hasOwnProperty("otherTypeCallback")?t.otherTypeCallback:this.currOtherTypeCallback,o=t.hasOwnProperty("parent")?t.parent:o,h=t.hasOwnProperty("parentProperty")?t.parentProperty:h,t=t.path}if(o=o||null,h=h||null,Array.isArray(t)&&(t=JSONPath.toPathString(t)),t&&e&&allowedResultTypes.includes(this.currResultType)){this._obj=e;var p=JSONPath.toPathArray(t);"$"===p[0]&&p.length>1&&p.shift(),this._hasParentSelector=null;var l=this._trace(p,e,["$"],o,h,r);return l=l.filter(function(t){return t&&!t.isParentSelector}),l.length?1!==l.length||i||Array.isArray(l[0].value)?l.reduce(function(t,e){var r=n._getPreferredOutput(e);return s&&Array.isArray(r)?t=t.concat(r):t.push(r),t},[]):this._getPreferredOutput(l[0]):i?[]:void 0}},JSONPath.prototype._getPreferredOutput=function(t){var e=this.currResultType;switch(e){case"all":return t.path="string"==typeof t.path?t.path:JSONPath.toPathString(t.path),t;case"value":case"parent":case"parentProperty":return t[e];case"path":return JSONPath.toPathString(t[e]);case"pointer":return JSONPath.toPointer(t.path)}},JSONPath.prototype._handleCallback=function(t,e,r){if(e){var a=this._getPreferredOutput(t);t.path="string"==typeof t.path?t.path:JSONPath.toPathString(t.path),e(a,r,t)}},JSONPath.prototype._trace=function(t,e,r,a,n,s,i){function o(t){f.push(t)}function h(t){Array.isArray(t)?t.forEach(o):f.push(t)}var p,l=this;if(!t.length)return p={path:r,value:e,parent:a,parentProperty:n},this._handleCallback(p,s,"value"),p;var c=t[0],u=t.slice(1),f=[];if(("string"!=typeof c||i)&&e&&Object.prototype.hasOwnProperty.call(e,c))h(this._trace(u,e[c],push(r,c),e,c,s));else if("*"===c)this._walk(c,u,e,r,a,n,s,function(t,e,r,a,n,s,i,o){h(l._trace(unshift(t,r),a,n,s,i,o,!0))});else if(".."===c)h(this._trace(u,e,r,a,n,s)),this._walk(c,u,e,r,a,n,s,function(t,e,r,a,n,s,i,o){"object"==typeof a[t]&&h(l._trace(unshift(e,r),a[t],push(n,t),a,t,o))});else{if("^"===c)return this._hasParentSelector=!0,r.length?{path:r.slice(0,-1),expr:u,isParentSelector:!0}:[];if("~"===c)return p={path:push(r,c),value:n,parent:a,parentProperty:null},this._handleCallback(p,s,"property"),p;if("$"===c)h(this._trace(u,e,r,null,null,s));else if(/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(c))h(this._slice(c,u,e,r,a,n,s));else if(0===c.indexOf("?(")){if(this.currPreventEval)throw new Error("Eval [?(expr)] prevented in JSONPath expression.");this._walk(c,u,e,r,a,n,s,function(t,e,r,a,n,s,i,o){l._eval(e.replace(/^\?\((.*?)\)$/,"$1"),a[t],t,n,s,i)&&h(l._trace(unshift(t,r),a,n,s,i,o))})}else if("("===c[0]){if(this.currPreventEval)throw new Error("Eval [(expr)] prevented in JSONPath expression.");h(this._trace(unshift(this._eval(c,e,r[r.length-1],r.slice(0,-1),a,n),u),e,r,a,n,s))}else if("@"===c[0]){var y=!1,v=c.slice(1,-2);switch(v){case"scalar":e&&["object","function"].includes(typeof e)||(y=!0);break;case"boolean":case"string":case"undefined":case"function":typeof e===v&&(y=!0);break;case"number":typeof e===v&&isFinite(e)&&(y=!0);break;case"nonFinite":"number"!=typeof e||isFinite(e)||(y=!0);break;case"object":e&&typeof e===v&&(y=!0);break;case"array":Array.isArray(e)&&(y=!0);break;case"other":y=this.currOtherTypeCallback(e,r,a,n);break;case"integer":e!==+e||!isFinite(e)||e%1||(y=!0);break;case"null":null===e&&(y=!0)}if(y)return p={path:r,value:e,parent:a,parentProperty:n},this._handleCallback(p,s,"value"),p}else if("`"===c[0]&&e&&Object.prototype.hasOwnProperty.call(e,c.slice(1))){var P=c.slice(1);h(this._trace(u,e[P],push(r,P),e,P,s,!0))}else if(c.includes(",")){var d,O;for(d=c.split(","),O=0;Ow;w++)b++,f.splice(b,0,S[w])}else f[b]=S}}return f},JSONPath.prototype._walk=function(t,e,r,a,n,s,i,o){var h,p,l;if(Array.isArray(r))for(h=0,p=r.length;p>h;h++)o(h,t,e,r,a,n,s,i);else if("object"==typeof r)for(l in r)Object.prototype.hasOwnProperty.call(r,l)&&o(l,t,e,r,a,n,s,i)},JSONPath.prototype._slice=function(t,e,r,a,n,s,i){if(Array.isArray(r)){var o,h=r.length,p=t.split(":"),l=p[0]&&parseInt(p[0],10)||0,c=p[1]&&parseInt(p[1],10)||h,u=p[2]&&parseInt(p[2],10)||1;l=0>l?Math.max(0,l+h):Math.min(h,l),c=0>c?Math.max(0,c+h):Math.min(h,c);var f=[];for(o=l;c>o;o+=u){var y=this._trace(unshift(o,e),r,a,n,s,i);Array.isArray(y)?y.forEach(function(t){f.push(t)}):f.push(y)}return f}},JSONPath.prototype._eval=function(t,e,r,a,n,s){if(!this._obj||!e)return!1;t.includes("@parentProperty")&&(this.currSandbox._$_parentProperty=s,t=t.replace(/@parentProperty/g,"_$_parentProperty")),t.includes("@parent")&&(this.currSandbox._$_parent=n,t=t.replace(/@parent/g,"_$_parent")),t.includes("@property")&&(this.currSandbox._$_property=r,t=t.replace(/@property/g,"_$_property")),t.includes("@path")&&(this.currSandbox._$_path=JSONPath.toPathString(a.concat([r])),t=t.replace(/@path/g,"_$_path")),t.match(/@([\.\s\)\[])/)&&(this.currSandbox._$_v=e,t=t.replace(/@([\.\s\)\[])/g,"_$_v$1"));try{return vm.runInNewContext(t,this.currSandbox)}catch(i){throw console.log(i),new Error("jsonPath: "+i.message+": "+t)}},JSONPath.cache={},JSONPath.toPathString=function(t){var e,r,a=t,n="$";for(e=1,r=a.length;r>e;e++)/^(~|\^|@.*?\(\))$/.test(a[e])||(n+=/^[0-9*]+$/.test(a[e])?"["+a[e]+"]":"['"+a[e]+"']");return n},JSONPath.toPointer=function(t){var e,r,a=t,n="";for(e=1,r=a.length;r>e;e++)/^(~|\^|@.*?\(\))$/.test(a[e])||(n+="/"+a[e].toString().replace(/\~/g,"~0").replace(/\//g,"~1"));return n},JSONPath.toPathArray=function(t){var e=JSONPath.cache;if(e[t])return e[t].concat();var r=[],a=t.replace(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/g,";$&;").replace(/[\['](\??\(.*?\))[\]']/g,function(t,e){return"[#"+(r.push(e)-1)+"]"}).replace(/\['([^'\]]*)'\]/g,function(t,e){return"['"+e.replace(/\./g,"%@%").replace(/~/g,"%%@@%%")+"']"}).replace(/~/g,";~;").replace(/'?\.'?(?![^\[]*\])|\['?/g,";").replace(/%@%/g,".").replace(/%%@@%%/g,"~").replace(/(?:;)?(\^+)(?:;)?/g,function(t,e){return";"+e.split("").join(";")+";"}).replace(/;;;|;;/g,";..;").replace(/;$|'?\]|'$/g,""),n=a.split(";").map(function(t){var e=t.match(/#([0-9]+)/);return e&&e[1]?r[e[1]]:t});return e[t]=n,e[t]},JSONPath.eval=function(t,e,r){return JSONPath(r,e,t)},"function"==typeof define&&define.amd?define(function(){return JSONPath}):isNode?module.exports=JSONPath:(glbl.jsonPath={eval:JSONPath.eval},glbl.JSONPath=JSONPath)}(this||self,"undefined"==typeof require?null:require); \ No newline at end of file diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 516ed2d..8b4b6e0 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -7,7 +7,7 @@ */ var module; -(function (require) {'use strict'; +(function (glbl, require) {'use strict'; // Make sure to know if we are in real node or not (the `require` variable // could actually be require.js, for example. @@ -15,15 +15,44 @@ var isNode = module && !!module.exports; var allowedResultTypes = ['value', 'path', 'pointer', 'parent', 'parentProperty', 'all']; +if (!Array.prototype.includes) { + Array.prototype.includes = function (item) { // eslint-disable-line no-extend-native + return this.indexOf(item) > -1; + }; +} +if (!String.prototype.includes) { + String.prototype.includes = function (item) { // eslint-disable-line no-extend-native + return this.indexOf(item) > -1; + }; +} + +var moveToAnotherArray = function (source, target, conditionCb) { + for (var i = 0, kl = source.length; i < kl; i++) { + var key = source[i]; + if (conditionCb(key)) { + target.push(source.splice(i--, 1)[0]); + } + } +}; + var vm = isNode ? require('vm') : { runInNewContext: function (expr, context) { - return eval(Object.keys(context).reduce(function (s, vr) { + var keys = Object.keys(context); + var funcs = []; + moveToAnotherArray(keys, funcs, function (key) { + return typeof context[key] === 'function'; + }); + var code = funcs.reduce(function (s, func) { + return 'var ' + func + '=' + context[func].toString() + ';' + s; + }, ''); + code += keys.reduce(function (s, vr) { return 'var ' + vr + '=' + JSON.stringify(context[vr]).replace(/\u2028|\u2029/g, function (m) { // http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/ return '\\u202' + (m === '\u2028' ? '8' : '9'); }) + ';' + s; - }, expr)); + }, expr); + return eval(code); } }; @@ -122,13 +151,14 @@ JSONPath.prototype.evaluate = function (expr, json, callback, otherTypeCallback) if (Array.isArray(expr)) { expr = JSONPath.toPathString(expr); } - if (!expr || !json || allowedResultTypes.indexOf(this.currResultType) === -1) { + if (!expr || !json || !allowedResultTypes.includes(this.currResultType)) { return; } this._obj = json; var exprList = JSONPath.toPathArray(expr); if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} + this._hasParentSelector = null; var result = this._trace(exprList, json, ['$'], currParent, currParentProperty, callback); result = result.filter(function (ea) {return ea && !ea.isParentSelector;}); @@ -173,7 +203,7 @@ JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) { } }; -JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback) { +JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback, literalPriority) { // No expr to follow? return path and value as the result of this trace branch var retObj, self = this; if (!expr.length) { @@ -187,14 +217,23 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c // We need to gather the return value of recursive trace calls in order to // do the parent sel computation. var ret = []; - function addRet (elems) {ret = ret.concat(elems);} + function retPush (elem) { + ret.push(elem); + } + function addRet (elems) { + if (Array.isArray(elems)) { + elems.forEach(retPush); + } else { + ret.push(elems); + } + } - if (val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property + if ((typeof loc !== 'string' || literalPriority) && val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback)); } else if (loc === '*') { // all child properties this._walk(loc, x, val, path, parent, parentPropName, callback, function (m, l, x, v, p, par, pr, cb) { - addRet(self._trace(unshift(m, x), v, p, par, pr, cb)); + addRet(self._trace(unshift(m, x), v, p, par, pr, cb, true)); }); } else if (loc === '..') { // all descendent parent properties @@ -206,17 +245,11 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c } }); } - else if (loc[0] === '(') { // [(expr)] (dynamic property/index) - if (this.currPreventEval) { - throw new Error('Eval [(expr)] prevented in JSONPath expression.'); - } - // As this will resolve to a property name (but we don't know it yet), property and parent information is relative to the parent of the property to which this expression will resolve - addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback)); - } // The parent sel computation is handled in the frame above using the // ancestor object of val else if (loc === '^') { - // This is not a final endpoint, so we do not invoke the callback here + // This is not a final endpoint, so we do not invoke the callback here + this._hasParentSelector = true; return path.length ? { path: path.slice(0, -1), expr: x, @@ -231,6 +264,9 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c else if (loc === '$') { // root only addRet(this._trace(x, val, path, null, null, callback)); } + else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] Python slice syntax + addRet(this._slice(loc, x, val, path, parent, parentPropName, callback)); + } else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering) if (this.currPreventEval) { throw new Error('Eval [?(expr)] prevented in JSONPath expression.'); @@ -241,18 +277,19 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c } }); } - else if (loc.indexOf(',') > -1) { // [name1,name2,...] - var parts, i; - for (parts = loc.split(','), i = 0; i < parts.length; i++) { - addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName, callback)); + else if (loc[0] === '(') { // [(expr)] (dynamic property/index) + if (this.currPreventEval) { + throw new Error('Eval [(expr)] prevented in JSONPath expression.'); } + // As this will resolve to a property name (but we don't know it yet), property and parent information is relative to the parent of the property to which this expression will resolve + addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback)); } else if (loc[0] === '@') { // value type: @boolean(), etc. var addType = false; var valueType = loc.slice(1, -2); switch (valueType) { case 'scalar': - if (!val || (['object', 'function'].indexOf(typeof val) === -1)) { + if (!val || !(['object', 'function'].includes(typeof val))) { addType = true; } break; @@ -301,17 +338,42 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c return retObj; } } - else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] Python slice syntax - addRet(this._slice(loc, x, val, path, parent, parentPropName, callback)); + else if (loc[0] === '`' && val && Object.prototype.hasOwnProperty.call(val, loc.slice(1))) { // `-escaped property + var locProp = loc.slice(1); + addRet(this._trace(x, val[locProp], push(path, locProp), val, locProp, callback, true)); + } + else if (loc.includes(',')) { // [name1,name2,...] + var parts, i; + for (parts = loc.split(','), i = 0; i < parts.length; i++) { + addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName, callback)); + } + } + else if (!literalPriority && val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property + addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback, true)); } // We check the resulting values for parent selections. For parent // selections we discard the value object and continue the trace with the - // current val object - return ret.reduce(function (all, ea) { - return all.concat(ea.isParentSelector ? self._trace(ea.expr, val, ea.path, parent, parentPropName, callback) : ea); - }, []); -}; + // current val object + if (this._hasParentSelector) { + for (var t = 0; t < ret.length; t++) { + var rett = ret[t]; + if (rett.isParentSelector) { + var tmp = self._trace(rett.expr, val, rett.path, parent, parentPropName, callback); + if (Array.isArray(tmp)) { + ret[t] = tmp[0]; + for (var tt = 1, tl = tmp.length; tt < tl; tt++) { + t++; + ret.splice(t, 0, tmp[tt]); + } + } else { + ret[t] = tmp; + } + } + } + } + return ret; +}; JSONPath.prototype._walk = function (loc, expr, val, path, parent, parentPropName, callback, f) { var i, n, m; @@ -340,26 +402,34 @@ JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropNa end = (end < 0) ? Math.max(0, end + len) : Math.min(len, end); var ret = []; for (i = start; i < end; i += step) { - ret = ret.concat(this._trace(unshift(i, expr), val, path, parent, parentPropName, callback)); + var tmp = this._trace(unshift(i, expr), val, path, parent, parentPropName, callback); + if (Array.isArray(tmp)) { + tmp.forEach(function (t) { + ret.push(t); + }); + } + else { + ret.push(tmp); + } } return ret; }; JSONPath.prototype._eval = function (code, _v, _vname, path, parent, parentPropName) { if (!this._obj || !_v) {return false;} - if (code.indexOf('@parentProperty') > -1) { + if (code.includes('@parentProperty')) { this.currSandbox._$_parentProperty = parentPropName; code = code.replace(/@parentProperty/g, '_$_parentProperty'); } - if (code.indexOf('@parent') > -1) { + if (code.includes('@parent')) { this.currSandbox._$_parent = parent; code = code.replace(/@parent/g, '_$_parent'); } - if (code.indexOf('@property') > -1) { + if (code.includes('@property')) { this.currSandbox._$_property = _vname; code = code.replace(/@property/g, '_$_property'); } - if (code.indexOf('@path') > -1) { + if (code.includes('@path')) { this.currSandbox._$_path = JSONPath.toPathString(path.concat([_vname])); code = code.replace(/@path/g, '_$_path'); } @@ -405,7 +475,7 @@ JSONPath.toPointer = function (pointer) { JSONPath.toPathArray = function (expr) { var cache = JSONPath.cache; - if (cache[expr]) {return cache[expr];} + if (cache[expr]) {return cache[expr].concat();} var subx = []; var normalized = expr // Properties @@ -414,7 +484,10 @@ JSONPath.toPathArray = function (expr) { .replace(/[\['](\??\(.*?\))[\]']/g, function ($0, $1) {return '[#' + (subx.push($1) - 1) + ']';}) // Escape periods and tildes within properties .replace(/\['([^'\]]*)'\]/g, function ($0, prop) { - return "['" + prop.replace(/\./g, '%@%').replace(/~/g, '%%@@%%') + "']"; + return "['" + prop + .replace(/\./g, '%@%') + .replace(/~/g, '%%@@%%') + + "']"; }) // Properties operator .replace(/~/g, ';~;') @@ -451,9 +524,9 @@ else if (isNode) { module.exports = JSONPath; } else { - self.jsonPath = { // Deprecated + glbl.jsonPath = { // Deprecated eval: JSONPath.eval }; - self.JSONPath = JSONPath; + glbl.JSONPath = JSONPath; } -}(typeof require === 'undefined' ? null : require)); +}(this || self, typeof require === 'undefined' ? null : require)); diff --git a/package.json b/package.json index a32213b..aa2a706 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ } ], "license": "MIT", - "version": "0.14.0", + "version": "0.16.0", "repository": { "type": "git", "url": "https://github.com/mplabs/JSONPath.git" @@ -45,15 +45,23 @@ "eslint": "^1.10.3", "eslint-config-standard": "^4.4.0", "eslint-plugin-standard": "^1.3.1", + "nodeunit": "0.9.0", + "remark-lint": "^3.0.0", + "remark": "^4.1.2", "gulp": "^3.9.1", - "gulp-minify": "0.0.5", - "nodeunit": "0.9.0" + "gulp-minify": "0.0.5" }, "keywords": [ "json", "jsonpath" ], "scripts": { - "test": "./node_modules/.bin/eslint test lib && \"./node_modules/.bin/nodeunit\" test" + "eslint": "./node_modules/.bin/eslint test lib test-helpers", + "remark": "./node_modules/.bin/remark -q -f .", + "lint": "npm run eslint && npm run remark", + "nodeunit": "./node_modules/.bin/nodeunit test", + "test": "npm run lint && npm run nodeunit", + "browser-test": "npm run lint && node ./test-helpers/nodeunit-server", + "start": "npm run browser-test" } } diff --git a/test-helpers/loadTests.js b/test-helpers/loadTests.js new file mode 100644 index 0000000..21594ca --- /dev/null +++ b/test-helpers/loadTests.js @@ -0,0 +1,21 @@ +/*global loadJS, nodeunit, suites*/ +[ + 'test.all.js', + 'test.arr.js', + 'test.at_and_dollar.js', + 'test.callback.js', + 'test.custom-properties.js', + 'test.escaping.js', + 'test.eval.js', + 'test.examples.js', + 'test.intermixed.arr.js', + 'test.parent-selector.js', + 'test.path_expressions.js', + 'test.pointer.js', + 'test.properties.js', + 'test.return.js', + 'test.toPath.js', + 'test.toPointer.js', + 'test.type-operators.js' +].forEach(loadJS); +nodeunit.run(suites); diff --git a/test-helpers/nodeunit-server.js b/test-helpers/nodeunit-server.js new file mode 100644 index 0000000..6d53384 --- /dev/null +++ b/test-helpers/nodeunit-server.js @@ -0,0 +1,7 @@ +require('http').createServer(function (req, res) { + var extra = req.url === '/test/' ? 'index.html' : ''; + var s = require('fs').createReadStream('.' + req.url + extra); + s.pipe(res); + s.on('error', function () {}); +}).listen(8084); +console.log('Started server; open http://localhost:8084/test/ in the browser'); diff --git a/test-helpers/testLoading.js b/test-helpers/testLoading.js new file mode 100644 index 0000000..e8e8607 --- /dev/null +++ b/test-helpers/testLoading.js @@ -0,0 +1,43 @@ +/* exported require */ +/*global nodeunit, JSONPath, ActiveXObject */ +// helper to get all the test cases +'use strict'; +var suites = [], _testCase = nodeunit.testCase; +nodeunit.testCase = function (tc) { + suites.push(tc); + return _testCase(tc); +}; +// stubs to load nodejs tests +function require (path) { // eslint-disable-line no-unused-vars + if (path === 'nodeunit') {return nodeunit;} + if (path.match(/^\.\.\/?$/)) {return JSONPath;} +} +var module = {exports: {}}; // eslint-disable-line no-unused-vars + +// synchronous load function for JS code, uses XMLHttpRequest abstraction from +// http://www.quirksmode.org/js/xmlhttp.html +// Since the tests are written in node.js style we need to wrap their code into +// a function, otherwise they would pollute the global NS and interfere with each +// other +function get (url, callback) { + function createXMLHTTPObject () { + var i, XMLHttpFactories = [ + function () {return new XMLHttpRequest();}, + function () {return new ActiveXObject('Msxml2.XMLHTTP');}, + function () {return new ActiveXObject('Msxml3.XMLHTTP');}, + function () {return new ActiveXObject('Microsoft.XMLHTTP');}]; + for (i = 0; i < XMLHttpFactories.length; i++) { + try {return XMLHttpFactories[i]();} + catch (ignore) {} + } + return false; + } + function sendRequest (url, callback) { + var req = createXMLHTTPObject(); + req.open('GET', url, false /* sync */); + req.onreadystatechange = function () {if (req.readyState === 4) {callback(req);}}; + if (req.readyState !== 4) {req.send();} + } + sendRequest(url, callback); +} +function loadJS (url) {get(url, function (req) {new Function(req.responseText)();});} // eslint-disable-line no-unused-vars, no-new-func diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..43f9974 --- /dev/null +++ b/test/index.html @@ -0,0 +1,15 @@ + + + + + JSONPath Tests + + + + + + +

JSONPath Tests

+ + + diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index deb2a73..b8981c0 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -38,10 +38,10 @@ module.exports = testCase({ 'test $ and @': function (test) { // ============================================================================ test.expect(5); - test.strictEqual(t1.$, jsonpath({json: t1, path: '$'})[0]); + test.strictEqual(t1.$, jsonpath({json: t1, path: '`$'})[0]); test.strictEqual(t1.a$a, jsonpath({json: t1, path: 'a$a'})[0]); - test.strictEqual(t1['@'], jsonpath({json: t1, path: '@'})[0]); - test.strictEqual(t1.$['@'], jsonpath({json: t1, path: '$.$.@'})[0]); + test.strictEqual(t1['@'], jsonpath({json: t1, path: '`@'})[0]); + test.strictEqual(t1.$['@'], jsonpath({json: t1, path: '$.`$.`@'})[0]); test.strictEqual(undefined, jsonpath({json: t1, path: '\\@'})[1]); test.done(); diff --git a/test/test.escaping.js b/test/test.escaping.js new file mode 100644 index 0000000..c2e21f6 --- /dev/null +++ b/test/test.escaping.js @@ -0,0 +1,52 @@ +/*global require, module*/ +/*eslint-disable quotes*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + +var json = { + '*': 'star', + 'rest': 'rest', + 'foo': 'bar' +}; + +var jsonMissingSpecial = { + 'rest': 'rest', + 'foo': 'bar' +}; + +module.exports = testCase({ + 'escape *': function (test) { + var expected = ['star']; + var result = jsonpath({json: json, path: "$['`*']"}); + test.deepEqual(expected, result); + + expected = []; + result = jsonpath({json: jsonMissingSpecial, path: "$['`*']"}); + test.deepEqual(expected, result); + + expected = ['star', 'rest']; + result = jsonpath({json: json, path: "$[`*,rest]"}); + test.deepEqual(expected, result); + + expected = ['star']; + result = jsonpath({json: json, path: "$.`*"}); + test.deepEqual(expected, result); + + expected = []; + result = jsonpath({json: jsonMissingSpecial, path: "$.`*"}); + test.deepEqual(expected, result); + + expected = ['star', 'rest', 'bar']; + result = jsonpath({json: json, path: "$['*']"}); + test.deepEqual(expected, result); + + expected = ['rest', 'bar']; + result = jsonpath({json: jsonMissingSpecial, path: "$['*']"}); + test.deepEqual(expected, result); + + test.done(); + } +}); +}()); diff --git a/test/test.html b/test/test.html deleted file mode 100644 index 71f53c7..0000000 --- a/test/test.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - JSONPath Tests - - - - - - -

JSONPath Tests

- - - diff --git a/test/test.path_expressions.js b/test/test.path_expressions.js index a67e9de..d72f7e9 100644 --- a/test/test.path_expressions.js +++ b/test/test.path_expressions.js @@ -102,7 +102,7 @@ module.exports = testCase({ }, // ============================================================================ - 'mixed notation continaing dots': function (test) { + 'mixed notation containing dots': function (test) { // ============================================================================ test.expect(1); var books = json.store.book; diff --git a/test/test.performance.js b/test/test.performance.js new file mode 100644 index 0000000..2ba1bb0 --- /dev/null +++ b/test/test.performance.js @@ -0,0 +1,48 @@ +/*global require, module*/ +/*eslint-disable quotes*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + +var arraySize = 12333, + resultCount = 1150, + itemCount = 150, + groupCount = 245; + +var json = { + results: [] +}; + +var i, j; + +var bigArray = []; +for (i = 0; i < arraySize; i++) { + bigArray[i] = 1; +} + +var items = []; +for (i = 0; i < itemCount; i++) { + items[i] = JSON.parse(JSON.stringify({a: {b: 0, c: 0}, s: {b: {c: bigArray}}})); +} + +for (i = 0; i < resultCount; i++) { + json.results[i] = {groups: [], v: {v: [1, 2, 3, 4, 5, 6, 7, 8]}}; + json.results[i].groups = []; + for (j = 0; j < groupCount; j++) { + json.results[i].groups[j] = {items: items, a: "121212"}; + } +} + +module.exports = testCase({ + // ============================================================================ + 'performance': function (test) { + // ============================================================================ + test.expect(1); + var start = Date.now(); + jsonpath({json: json, path: '$.results[*].groups[*].items[42]'}); + test.strictEqual((Date.now() - start) < 2500, true); + test.done(); + } +}); +}()); diff --git a/test/test.toPath.js b/test/test.toPath.js index ab771f0..21bbfff 100644 --- a/test/test.toPath.js +++ b/test/test.toPath.js @@ -49,6 +49,30 @@ module.exports = testCase({ var result = jsonpath.toPathArray("$.store['bicycle'].color"); test.deepEqual(expected, result); + test.done(); + }, + + 'toPathArray (avoid cache reference issue #78)': function (test) { + test.expect(3); + + var originalPath = "$['foo']['bar']"; + var json = { foo: { bar: 'baz' } }; + var pathArr = jsonpath.toPathArray(originalPath); + + test.equal(pathArr.length, 3); + + // Shouldn't manipulate pathArr values + jsonpath({ + json: json, + path: originalPath, + wrap: false, + resultType: 'value' + }); + + test.equal(pathArr.length, 3); + var path = jsonpath.toPathString(pathArr); + + test.equal(path, originalPath); test.done(); } }); diff --git a/test/toPointer.js b/test/test.toPointer.js similarity index 100% rename from test/toPointer.js rename to test/test.toPointer.js