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
+
+
+
+
+
+
+