@swaggerexpert/jsonpath 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,8 @@ The development of this library contributed to the identification and formal sub
43
43
  - [Statistics](#statistics)
44
44
  - [Tracing](#tracing)
45
45
  - [Validation](#validation)
46
+ - [Compilation](#compilation)
47
+ - [Escaping](#escaping)
46
48
  - [Errors](#errors)
47
49
  - [Grammar](#grammar)
48
50
  - [More about JSONPath](#more-about-jsonpath)
@@ -200,10 +202,11 @@ const { result, trace } = parse('$fdfadfd', { trace: true });
200
202
 
201
203
  result.success; // returns false
202
204
  trace.displayTrace(); // returns trace information
205
+ trace.inferExpectations(); // returns parser expectations
203
206
  ```
204
207
 
205
- By combining information from `result` and `trace`, it is possible to analyze the parsing process in detail
206
- and generate a messages like this: `'Syntax error at position 1, expected "[", ".", ".."'`. Please see this
208
+ By combining information from `result` and `trace`, you can analyze the parsing process in detail
209
+ and generate messages like: `'Syntax error at position 1, expected "[", ".", ".."'`. Please see this
207
210
  [test file](https://github.com/swaggerexpert/jsonpath/blob/main/test/parse/trace.js) for more information how to achieve that.
208
211
 
209
212
  #### Validation
@@ -226,13 +229,39 @@ test("$['a']", { normalized: true }); // => true
226
229
  test('$.store.book[0].title', { normalized: true }); // => false
227
230
  ```
228
231
 
232
+ #### Compilation
233
+
234
+ Compilation is the process of transforming a list of selectors into a [Normalized Path](https://www.rfc-editor.org/rfc/rfc9535#name-normalized-paths).
235
+ During compilation, name selectors are automatically escaped before being compiled.
236
+
237
+ ```js
238
+ import { compile } from '@swaggerexpert/jsonpath';
239
+
240
+ compile(['store', 'book', 0, 'title']); // => "$['store']['book'][0]['title']"
241
+ ```
242
+
243
+ #### Escaping
244
+
245
+ Certain characters within name selectors in Normalized Paths require escaping.
246
+ The apostrophe (`'`) and backslash (`\`) characters must be escaped.
247
+ Control characters (U+0000 through U+001F) are escaped using specific escape sequences
248
+ (`\b`, `\t`, `\n`, `\f`, `\r`) or Unicode escape sequences (`\uXXXX`).
249
+
250
+ ```js
251
+ import { escape } from '@swaggerexpert/jsonpath';
252
+
253
+ escape("it's"); // => "it\\'s"
254
+ escape('back\\slash'); // => "back\\\\slash"
255
+ escape('line\nfeed'); // => "line\\nfeed"
256
+ ```
257
+
229
258
  #### Errors
230
259
 
231
260
  `@swaggerexpert/jsonpath` provides a structured error class hierarchy,
232
- enabling precise error handling across JSONPath operations, including parsing.
261
+ enabling precise error handling across JSONPath operations, including parsing and compilation.
233
262
 
234
263
  ```js
235
- import { JSONPathError, JSONPathParseError } from '@swaggerexpert/jsonpath';
264
+ import { JSONPathError, JSONPathParseError, JSONPathCompileError } from '@swaggerexpert/jsonpath';
236
265
  ```
237
266
 
238
267
  **JSONPathError** is the base class for all JSONPath errors.
package/SECURITY.md CHANGED
@@ -6,7 +6,9 @@ If you believe you've found an exploitable security issue in @swaggerexpert/json
6
6
 
7
7
  | Version | Supported |
8
8
  |---------|--------------------|
9
- | ^1.0.0 | :white_check_mark: |
9
+ | ^1.0.0 | :x: |
10
+ | ^2.0.0 | :x: |
11
+ | ^3.0.0 | :white_check_mark: |
10
12
 
11
13
  ## Reporting a Vulnerability
12
14
 
package/cjs/apg-lite.cjs CHANGED
@@ -2,31 +2,31 @@
2
2
 
3
3
  exports.__esModule = true;
4
4
  exports.utilities = exports.identifiers = exports.Trace = exports.Stats = exports.Parser = exports.Ast = void 0;
5
- /* *************************************************************************************
6
- * copyright: Copyright (c) 2023 Lowell D. Thomas, all rights reserved
7
- * license: BSD-2-Clause (https://opensource.org/licenses/BSD-2-Clause)
8
- *
9
- * Redistribution and use in source and binary forms, with or without
10
- * modification, are permitted provided that the following conditions are met:
11
- *
12
- * 1. Redistributions of source code must retain the above copyright notice, this
13
- * list of conditions and the following disclaimer.
14
- *
15
- * 2. Redistributions in binary form must reproduce the above copyright notice,
16
- * this list of conditions and the following disclaimer in the documentation
17
- * and/or other materials provided with the distribution.
18
- *
19
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
- *
5
+ /* *************************************************************************************
6
+ * copyright: Copyright (c) 2023 Lowell D. Thomas, all rights reserved
7
+ * license: BSD-2-Clause (https://opensource.org/licenses/BSD-2-Clause)
8
+ *
9
+ * Redistribution and use in source and binary forms, with or without
10
+ * modification, are permitted provided that the following conditions are met:
11
+ *
12
+ * 1. Redistributions of source code must retain the above copyright notice, this
13
+ * list of conditions and the following disclaimer.
14
+ *
15
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
16
+ * this list of conditions and the following disclaimer in the documentation
17
+ * and/or other materials provided with the distribution.
18
+ *
19
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+ *
30
30
  * ********************************************************************************* */
31
31
 
32
32
  const Parser = exports.Parser = function fnparser() {
@@ -665,7 +665,7 @@ const Ast = exports.Ast = function fnast() {
665
665
  a.ruleDefined = index => !!nodeCallbacks[index];
666
666
  /* AST node UDT callbacks - called by the parser's `UDT` operator */
667
667
  a.udtDefined = index => !!nodeCallbacks[rules.length + index];
668
- /* called by the parser's `RNM` & `UDT` operators
668
+ /* called by the parser's `RNM` & `UDT` operators
669
669
  builds a record for the downward traversal of the node */
670
670
  a.down = (callbackIndex, name) => {
671
671
  const thisIndex = records.length;
@@ -963,8 +963,8 @@ const Stats = exports.Stats = function fnstats() {
963
963
  out += displayRow('TOTAL', totals.match, totals.empty, totals.nomatch, totals.total);
964
964
  return out;
965
965
  };
966
- /*
967
- Display rule/udt
966
+ /*
967
+ Display rule/udt
968
968
  */
969
969
  this.displayHits = type => {
970
970
  let out = '';
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ var _escape = _interopRequireDefault(require("./escape.cjs"));
6
+ var _JSONPathCompileError = _interopRequireDefault(require("./errors/JSONPathCompileError.cjs"));
7
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
8
+ /**
9
+ * Compiles an array of selectors into a normalized JSONPath.
10
+ * Follows RFC 9535 Section 2.7 normalized path format.
11
+ *
12
+ * @param {Array<string|number>} selectors - Array of name selectors (strings) or index selectors (numbers)
13
+ * @returns {string} A normalized JSONPath string
14
+ * @throws {JSONPathCompileError} If selectors is not an array or contains invalid selector types
15
+ *
16
+ * @example
17
+ * compile(['a', 'b', 1]) // returns "$['a']['b'][1]"
18
+ * compile([]) // returns "$"
19
+ * compile(['foo', 0, 'bar']) // returns "$['foo'][0]['bar']"
20
+ */
21
+ const compile = selectors => {
22
+ if (!Array.isArray(selectors)) {
23
+ throw new _JSONPathCompileError.default(`Selectors must be an array, got: ${typeof selectors}`, {
24
+ selectors
25
+ });
26
+ }
27
+ try {
28
+ const segments = selectors.map(selector => {
29
+ if (typeof selector === 'string') {
30
+ // Name selector: escape and wrap in single quotes
31
+ return `['${(0, _escape.default)(selector)}']`;
32
+ }
33
+ if (typeof selector === 'number') {
34
+ // Index selector: must be a non-negative safe integer (RFC 9535 Section 2.1)
35
+ if (!Number.isSafeInteger(selector) || selector < 0) {
36
+ throw new TypeError(`Index selector must be a non-negative safe integer, got: ${selector}`);
37
+ }
38
+ return `[${selector}]`;
39
+ }
40
+ throw new TypeError(`Selector must be a string or non-negative integer, got: ${typeof selector}`);
41
+ });
42
+ return `$${segments.join('')}`;
43
+ } catch (error) {
44
+ throw new _JSONPathCompileError.default('Failed to compile normalized JSONPath', {
45
+ cause: error,
46
+ selectors
47
+ });
48
+ }
49
+ };
50
+ var _default = exports.default = compile;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ var _JSONPathError = _interopRequireDefault(require("./JSONPathError.cjs"));
6
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
+ class JSONPathCompileError extends _JSONPathError.default {}
8
+ var _default = exports.default = JSONPathCompileError;
package/cjs/escape.cjs ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ /**
6
+ * Escapes a string for use in a normalized JSONPath name selector.
7
+ * Follows RFC 9535 Section 2.7 escaping rules for single-quoted strings.
8
+ *
9
+ * @param {string} selector - The string to escape
10
+ * @returns {string} The escaped string (without surrounding quotes)
11
+ */
12
+ const escape = selector => {
13
+ if (typeof selector !== 'string') {
14
+ throw new TypeError('Selector must be a string');
15
+ }
16
+ let escaped = '';
17
+ for (const char of selector) {
18
+ const codePoint = char.codePointAt(0);
19
+ switch (codePoint) {
20
+ case 0x08:
21
+ // backspace
22
+ escaped += '\\b';
23
+ break;
24
+ case 0x09:
25
+ // horizontal tab
26
+ escaped += '\\t';
27
+ break;
28
+ case 0x0a:
29
+ // line feed
30
+ escaped += '\\n';
31
+ break;
32
+ case 0x0c:
33
+ // form feed
34
+ escaped += '\\f';
35
+ break;
36
+ case 0x0d:
37
+ // carriage return
38
+ escaped += '\\r';
39
+ break;
40
+ case 0x27:
41
+ // apostrophe '
42
+ escaped += "\\'";
43
+ break;
44
+ case 0x5c:
45
+ // backslash \
46
+ escaped += '\\\\';
47
+ break;
48
+ default:
49
+ // Other control characters (U+0000-U+001F except those handled above)
50
+ if (codePoint <= 0x1f) {
51
+ escaped += `\\u${codePoint.toString(16).padStart(4, '0')}`;
52
+ } else {
53
+ escaped += char;
54
+ }
55
+ }
56
+ }
57
+ return escaped;
58
+ };
59
+ var _default = exports.default = escape;
package/cjs/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.test = exports.parse = exports.XMLTranslator = exports.Grammar = exports.CSTTranslator = exports.CSTOptimizedTranslator = exports.ASTTranslator = void 0;
4
+ exports.test = exports.parse = exports.escape = exports.compile = exports.XMLTranslator = exports.Trace = exports.JSONPathParseError = exports.JSONPathError = exports.JSONPathCompileError = exports.Grammar = exports.CSTTranslator = exports.CSTOptimizedTranslator = exports.ASTTranslator = void 0;
5
5
  var _grammar = _interopRequireDefault(require("./grammar.cjs"));
6
6
  exports.Grammar = _grammar.default;
7
7
  var _index = _interopRequireDefault(require("./parse/index.cjs"));
@@ -14,6 +14,18 @@ var _index2 = _interopRequireDefault(require("./parse/translators/ASTTranslator/
14
14
  exports.ASTTranslator = _index2.default;
15
15
  var _XMLTranslator = _interopRequireDefault(require("./parse/translators/XMLTranslator.cjs"));
16
16
  exports.XMLTranslator = _XMLTranslator.default;
17
+ var _Trace = _interopRequireDefault(require("./parse/trace/Trace.cjs"));
18
+ exports.Trace = _Trace.default;
17
19
  var _index3 = _interopRequireDefault(require("./test/index.cjs"));
18
20
  exports.test = _index3.default;
21
+ var _compile = _interopRequireDefault(require("./compile.cjs"));
22
+ exports.compile = _compile.default;
23
+ var _escape = _interopRequireDefault(require("./escape.cjs"));
24
+ exports.escape = _escape.default;
25
+ var _JSONPathError = _interopRequireDefault(require("./errors/JSONPathError.cjs"));
26
+ exports.JSONPathError = _JSONPathError.default;
27
+ var _JSONPathParseError = _interopRequireDefault(require("./errors/JSONPathParseError.cjs"));
28
+ exports.JSONPathParseError = _JSONPathParseError.default;
29
+ var _JSONPathCompileError = _interopRequireDefault(require("./errors/JSONPathCompileError.cjs"));
30
+ exports.JSONPathCompileError = _JSONPathCompileError.default;
19
31
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -3,6 +3,7 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _apgLite = require("../apg-lite.cjs");
6
+ var _Trace = _interopRequireDefault(require("./trace/Trace.cjs"));
6
7
  var _grammar = _interopRequireDefault(require("../grammar.cjs"));
7
8
  var _index = _interopRequireDefault(require("./translators/ASTTranslator/index.cjs"));
8
9
  var _JSONPathParseError = _interopRequireDefault(require("../errors/JSONPathParseError.cjs"));
@@ -21,7 +22,7 @@ const parse = (jsonPath, {
21
22
  const parser = new _apgLite.Parser();
22
23
  if (translator) parser.ast = translator;
23
24
  if (stats) parser.stats = new _apgLite.Stats();
24
- if (trace) parser.trace = new _apgLite.Trace();
25
+ if (trace) parser.trace = new _Trace.default();
25
26
  const startRule = normalized ? 'normalized-path' : 'jsonpath-query';
26
27
  const result = parser.parse(grammar, startRule, jsonPath);
27
28
  return {
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ class Expectations extends Array {
6
+ toString() {
7
+ return this.map(c => `"${String(c)}"`).join(', ');
8
+ }
9
+ }
10
+ var _default = exports.default = Expectations;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ var _apgLite = require("../../apg-lite.cjs");
6
+ var _Expectations = _interopRequireDefault(require("./Expectations.cjs"));
7
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
8
+ class Trace extends _apgLite.Trace {
9
+ inferExpectations() {
10
+ const lines = this.displayTrace().split('\n');
11
+ const expectations = new Set();
12
+ let lastMatchedIndex = -1;
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const line = lines[i];
15
+
16
+ // capture the max match line (first one that ends in a single character match)
17
+ if (line.includes('M|')) {
18
+ const textMatch = line.match(/]'(.*)'$/);
19
+ if (textMatch && textMatch[1]) {
20
+ lastMatchedIndex = i;
21
+ }
22
+ }
23
+
24
+ // collect terminal failures after the deepest successful match
25
+ if (i > lastMatchedIndex) {
26
+ const terminalFailMatch = line.match(/N\|\[TLS\(([^)]+)\)]/);
27
+ if (terminalFailMatch) {
28
+ expectations.add(terminalFailMatch[1]);
29
+ }
30
+ }
31
+ }
32
+ return new _Expectations.default(...expectations);
33
+ }
34
+ }
35
+ var _default = exports.default = Trace;
@@ -1,13 +1,80 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.decodeString = exports.decodeJSONValue = exports.decodeInteger = void 0;
5
- const decodeString = str => {
6
- return JSON.parse(`"${str.replace(/"/g, '\\"')}"`);
4
+ exports.decodeSingleQuotedString = exports.decodeJSONValue = exports.decodeInteger = exports.decodeDoubleQuotedString = void 0;
5
+ const decodeDoubleQuotedString = str => {
6
+ return decodeJSONValue(`"${str}"`);
7
7
  };
8
- exports.decodeString = decodeString;
8
+ exports.decodeDoubleQuotedString = decodeDoubleQuotedString;
9
+ const decodeSingleQuotedString = str => {
10
+ // Decode single-quoted string escape sequences into raw text, then let JSON.stringify
11
+ // produce a correctly escaped double-quoted JSON string.
12
+ let decoded = '';
13
+ for (let i = 0; i < str.length; i++) {
14
+ const ch = str[i];
15
+ if (ch === '\\') {
16
+ i++;
17
+ if (i >= str.length) {
18
+ // Trailing backslash, treat it as a literal backslash
19
+ decoded += '\\';
20
+ break;
21
+ }
22
+ const esc = str[i];
23
+ switch (esc) {
24
+ case 'n':
25
+ decoded += '\n';
26
+ break;
27
+ case 'r':
28
+ decoded += '\r';
29
+ break;
30
+ case 't':
31
+ decoded += '\t';
32
+ break;
33
+ case 'b':
34
+ decoded += '\b';
35
+ break;
36
+ case 'f':
37
+ decoded += '\f';
38
+ break;
39
+ case '/':
40
+ decoded += '/';
41
+ break;
42
+ case '\\':
43
+ decoded += '\\';
44
+ break;
45
+ case "'":
46
+ decoded += "'";
47
+ break;
48
+ case '"':
49
+ decoded += '"';
50
+ break;
51
+ case 'u':
52
+ {
53
+ // Unicode escape \uXXXX - grammar guarantees exactly 4 hex digits
54
+ const hex = str.slice(i + 1, i + 5);
55
+ decoded += String.fromCharCode(parseInt(hex, 16));
56
+ i += 4;
57
+ break;
58
+ }
59
+ default:
60
+ // Unrecognized escape, keep the escaped character literally
61
+ decoded += esc;
62
+ break;
63
+ }
64
+ } else {
65
+ decoded += ch;
66
+ }
67
+ }
68
+ // Use JSON.stringify to produce a valid JSON string literal
69
+ return decodeJSONValue(JSON.stringify(decoded));
70
+ };
71
+ exports.decodeSingleQuotedString = decodeSingleQuotedString;
9
72
  const decodeInteger = str => {
10
- return parseInt(str, 10);
73
+ const value = parseInt(str, 10);
74
+ if (!Number.isSafeInteger(value)) {
75
+ throw new RangeError(`Integer value out of safe range [-(2^53)+1, (2^53)-1], got: ${str}`);
76
+ }
77
+ return value;
11
78
  };
12
79
  exports.decodeInteger = decodeInteger;
13
80
  const decodeJSONValue = str => {
@@ -4,8 +4,7 @@ exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _CSTOptimizedTranslator = _interopRequireDefault(require("../CSTOptimizedTranslator.cjs"));
6
6
  var _transformers = _interopRequireWildcard(require("./transformers.cjs"));
7
- function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
8
- function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
7
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
9
8
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
9
  class ASTTranslator extends _CSTOptimizedTranslator.default {
11
10
  getTree() {
@@ -73,9 +73,10 @@ const transformers = {
73
73
  const quoted = node.children.find(({
74
74
  type
75
75
  }) => ['double-quoted', 'single-quoted'].includes(type));
76
+ const decodeString = isSingleQuoted ? _decoders.decodeSingleQuotedString : _decoders.decodeDoubleQuotedString;
76
77
  return {
77
78
  type: 'StringLiteral',
78
- value: quoted ? (0, _decoders.decodeString)(quoted.text) : '',
79
+ value: quoted ? decodeString(quoted.text) : '',
79
80
  format: isSingleQuoted ? 'single-quoted' : 'double-quoted'
80
81
  };
81
82
  },
@@ -396,7 +397,7 @@ const transformers = {
396
397
  }) => type === 'normal-single-quoted');
397
398
  return {
398
399
  type: 'NameSelector',
399
- value: child ? (0, _decoders.decodeString)(child.text) : '',
400
+ value: child ? (0, _decoders.decodeSingleQuotedString)(child.text) : '',
400
401
  format: 'single-quoted'
401
402
  };
402
403
  },
package/es/compile.mjs ADDED
@@ -0,0 +1,45 @@
1
+ import escape from "./escape.mjs";
2
+ import JSONPathCompileError from "./errors/JSONPathCompileError.mjs";
3
+ /**
4
+ * Compiles an array of selectors into a normalized JSONPath.
5
+ * Follows RFC 9535 Section 2.7 normalized path format.
6
+ *
7
+ * @param {Array<string|number>} selectors - Array of name selectors (strings) or index selectors (numbers)
8
+ * @returns {string} A normalized JSONPath string
9
+ * @throws {JSONPathCompileError} If selectors is not an array or contains invalid selector types
10
+ *
11
+ * @example
12
+ * compile(['a', 'b', 1]) // returns "$['a']['b'][1]"
13
+ * compile([]) // returns "$"
14
+ * compile(['foo', 0, 'bar']) // returns "$['foo'][0]['bar']"
15
+ */
16
+ const compile = selectors => {
17
+ if (!Array.isArray(selectors)) {
18
+ throw new JSONPathCompileError(`Selectors must be an array, got: ${typeof selectors}`, {
19
+ selectors
20
+ });
21
+ }
22
+ try {
23
+ const segments = selectors.map(selector => {
24
+ if (typeof selector === 'string') {
25
+ // Name selector: escape and wrap in single quotes
26
+ return `['${escape(selector)}']`;
27
+ }
28
+ if (typeof selector === 'number') {
29
+ // Index selector: must be a non-negative safe integer (RFC 9535 Section 2.1)
30
+ if (!Number.isSafeInteger(selector) || selector < 0) {
31
+ throw new TypeError(`Index selector must be a non-negative safe integer, got: ${selector}`);
32
+ }
33
+ return `[${selector}]`;
34
+ }
35
+ throw new TypeError(`Selector must be a string or non-negative integer, got: ${typeof selector}`);
36
+ });
37
+ return `$${segments.join('')}`;
38
+ } catch (error) {
39
+ throw new JSONPathCompileError('Failed to compile normalized JSONPath', {
40
+ cause: error,
41
+ selectors
42
+ });
43
+ }
44
+ };
45
+ export default compile;
@@ -0,0 +1,3 @@
1
+ import JSONPathError from "./JSONPathError.mjs";
2
+ class JSONPathCompileError extends JSONPathError {}
3
+ export default JSONPathCompileError;
package/es/escape.mjs ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Escapes a string for use in a normalized JSONPath name selector.
3
+ * Follows RFC 9535 Section 2.7 escaping rules for single-quoted strings.
4
+ *
5
+ * @param {string} selector - The string to escape
6
+ * @returns {string} The escaped string (without surrounding quotes)
7
+ */
8
+ const escape = selector => {
9
+ if (typeof selector !== 'string') {
10
+ throw new TypeError('Selector must be a string');
11
+ }
12
+ let escaped = '';
13
+ for (const char of selector) {
14
+ const codePoint = char.codePointAt(0);
15
+ switch (codePoint) {
16
+ case 0x08:
17
+ // backspace
18
+ escaped += '\\b';
19
+ break;
20
+ case 0x09:
21
+ // horizontal tab
22
+ escaped += '\\t';
23
+ break;
24
+ case 0x0a:
25
+ // line feed
26
+ escaped += '\\n';
27
+ break;
28
+ case 0x0c:
29
+ // form feed
30
+ escaped += '\\f';
31
+ break;
32
+ case 0x0d:
33
+ // carriage return
34
+ escaped += '\\r';
35
+ break;
36
+ case 0x27:
37
+ // apostrophe '
38
+ escaped += "\\'";
39
+ break;
40
+ case 0x5c:
41
+ // backslash \
42
+ escaped += '\\\\';
43
+ break;
44
+ default:
45
+ // Other control characters (U+0000-U+001F except those handled above)
46
+ if (codePoint <= 0x1f) {
47
+ escaped += `\\u${codePoint.toString(16).padStart(4, '0')}`;
48
+ } else {
49
+ escaped += char;
50
+ }
51
+ }
52
+ }
53
+ return escaped;
54
+ };
55
+ export default escape;
package/es/index.mjs CHANGED
@@ -4,4 +4,10 @@ export { default as CSTTranslator } from "./parse/translators/CSTTranslator.mjs"
4
4
  export { default as CSTOptimizedTranslator } from "./parse/translators/CSTOptimizedTranslator.mjs";
5
5
  export { default as ASTTranslator } from "./parse/translators/ASTTranslator/index.mjs";
6
6
  export { default as XMLTranslator } from "./parse/translators/XMLTranslator.mjs";
7
- export { default as test } from "./test/index.mjs";
7
+ export { default as Trace } from "./parse/trace/Trace.mjs";
8
+ export { default as test } from "./test/index.mjs";
9
+ export { default as compile } from "./compile.mjs";
10
+ export { default as escape } from "./escape.mjs";
11
+ export { default as JSONPathError } from "./errors/JSONPathError.mjs";
12
+ export { default as JSONPathParseError } from "./errors/JSONPathParseError.mjs";
13
+ export { default as JSONPathCompileError } from "./errors/JSONPathCompileError.mjs";
@@ -1,4 +1,5 @@
1
- import { Parser, Stats, Trace } from 'apg-lite';
1
+ import { Parser, Stats } from 'apg-lite';
2
+ import Trace from "./trace/Trace.mjs";
2
3
  import Grammar from "../grammar.mjs";
3
4
  import ASTTranslator from "./translators/ASTTranslator/index.mjs";
4
5
  import JSONPathParseError from "../errors/JSONPathParseError.mjs";
@@ -0,0 +1,6 @@
1
+ class Expectations extends Array {
2
+ toString() {
3
+ return this.map(c => `"${String(c)}"`).join(', ');
4
+ }
5
+ }
6
+ export default Expectations;
@@ -0,0 +1,30 @@
1
+ import { Trace as BaseTrace } from 'apg-lite';
2
+ import Expectations from "./Expectations.mjs";
3
+ class Trace extends BaseTrace {
4
+ inferExpectations() {
5
+ const lines = this.displayTrace().split('\n');
6
+ const expectations = new Set();
7
+ let lastMatchedIndex = -1;
8
+ for (let i = 0; i < lines.length; i++) {
9
+ const line = lines[i];
10
+
11
+ // capture the max match line (first one that ends in a single character match)
12
+ if (line.includes('M|')) {
13
+ const textMatch = line.match(/]'(.*)'$/);
14
+ if (textMatch && textMatch[1]) {
15
+ lastMatchedIndex = i;
16
+ }
17
+ }
18
+
19
+ // collect terminal failures after the deepest successful match
20
+ if (i > lastMatchedIndex) {
21
+ const terminalFailMatch = line.match(/N\|\[TLS\(([^)]+)\)]/);
22
+ if (terminalFailMatch) {
23
+ expectations.add(terminalFailMatch[1]);
24
+ }
25
+ }
26
+ }
27
+ return new Expectations(...expectations);
28
+ }
29
+ }
30
+ export default Trace;
@@ -1,8 +1,74 @@
1
- export const decodeString = str => {
2
- return JSON.parse(`"${str.replace(/"/g, '\\"')}"`);
1
+ export const decodeDoubleQuotedString = str => {
2
+ return decodeJSONValue(`"${str}"`);
3
+ };
4
+ export const decodeSingleQuotedString = str => {
5
+ // Decode single-quoted string escape sequences into raw text, then let JSON.stringify
6
+ // produce a correctly escaped double-quoted JSON string.
7
+ let decoded = '';
8
+ for (let i = 0; i < str.length; i++) {
9
+ const ch = str[i];
10
+ if (ch === '\\') {
11
+ i++;
12
+ if (i >= str.length) {
13
+ // Trailing backslash, treat it as a literal backslash
14
+ decoded += '\\';
15
+ break;
16
+ }
17
+ const esc = str[i];
18
+ switch (esc) {
19
+ case 'n':
20
+ decoded += '\n';
21
+ break;
22
+ case 'r':
23
+ decoded += '\r';
24
+ break;
25
+ case 't':
26
+ decoded += '\t';
27
+ break;
28
+ case 'b':
29
+ decoded += '\b';
30
+ break;
31
+ case 'f':
32
+ decoded += '\f';
33
+ break;
34
+ case '/':
35
+ decoded += '/';
36
+ break;
37
+ case '\\':
38
+ decoded += '\\';
39
+ break;
40
+ case "'":
41
+ decoded += "'";
42
+ break;
43
+ case '"':
44
+ decoded += '"';
45
+ break;
46
+ case 'u':
47
+ {
48
+ // Unicode escape \uXXXX - grammar guarantees exactly 4 hex digits
49
+ const hex = str.slice(i + 1, i + 5);
50
+ decoded += String.fromCharCode(parseInt(hex, 16));
51
+ i += 4;
52
+ break;
53
+ }
54
+ default:
55
+ // Unrecognized escape, keep the escaped character literally
56
+ decoded += esc;
57
+ break;
58
+ }
59
+ } else {
60
+ decoded += ch;
61
+ }
62
+ }
63
+ // Use JSON.stringify to produce a valid JSON string literal
64
+ return decodeJSONValue(JSON.stringify(decoded));
3
65
  };
4
66
  export const decodeInteger = str => {
5
- return parseInt(str, 10);
67
+ const value = parseInt(str, 10);
68
+ if (!Number.isSafeInteger(value)) {
69
+ throw new RangeError(`Integer value out of safe range [-(2^53)+1, (2^53)-1], got: ${str}`);
70
+ }
71
+ return value;
6
72
  };
7
73
  export const decodeJSONValue = str => {
8
74
  return JSON.parse(str);
@@ -1,5 +1,5 @@
1
1
  import JSONPathParseError from "../../../errors/JSONPathParseError.mjs";
2
- import { decodeString, decodeInteger, decodeJSONValue } from "./decoders.mjs";
2
+ import { decodeSingleQuotedString, decodeDoubleQuotedString, decodeInteger, decodeJSONValue } from "./decoders.mjs";
3
3
  export const transformCSTtoAST = (node, transformerMap, ctx = {
4
4
  parent: null,
5
5
  path: []
@@ -67,6 +67,7 @@ const transformers = {
67
67
  const quoted = node.children.find(({
68
68
  type
69
69
  }) => ['double-quoted', 'single-quoted'].includes(type));
70
+ const decodeString = isSingleQuoted ? decodeSingleQuotedString : decodeDoubleQuotedString;
70
71
  return {
71
72
  type: 'StringLiteral',
72
73
  value: quoted ? decodeString(quoted.text) : '',
@@ -390,7 +391,7 @@ const transformers = {
390
391
  }) => type === 'normal-single-quoted');
391
392
  return {
392
393
  type: 'NameSelector',
393
- value: child ? decodeString(child.text) : '',
394
+ value: child ? decodeSingleQuotedString(child.text) : '',
394
395
  format: 'single-quoted'
395
396
  };
396
397
  },
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@swaggerexpert/jsonpath",
3
3
  "publishConfig": {
4
- "access": "public"
4
+ "access": "public",
5
+ "registry": "https://registry.npmjs.org",
6
+ "provenance": true
5
7
  },
6
- "version": "3.0.0",
7
- "description": "RCF 9535 implementation of JSONPath",
8
+ "version": "3.1.0",
9
+ "description": "RFC 9535 implementation of JSONPath",
8
10
  "main": "./cjs/index.cjs",
9
11
  "types": "./types/index.d.ts",
10
12
  "exports": {
@@ -60,22 +62,28 @@
60
62
  "SECURITY.md"
61
63
  ],
62
64
  "dependencies": {
63
- "apg-lite": "^1.0.4"
65
+ "apg-lite": "^1.0.5"
64
66
  },
65
67
  "devDependencies": {
66
- "@babel/cli": "=7.27.0",
67
- "@babel/core": "=7.26.10",
68
- "@babel/preset-env": "=7.26.9",
69
- "@commitlint/cli": "=19.8.0",
70
- "@commitlint/config-conventional": "=19.8.0",
68
+ "@babel/cli": "=7.28.6",
69
+ "@babel/core": "=7.28.6",
70
+ "@babel/preset-env": "=7.28.6",
71
+ "@commitlint/cli": "=20.3.1",
72
+ "@commitlint/config-conventional": "=20.3.1",
73
+ "@semantic-release/commit-analyzer": "^13.0.1",
74
+ "@semantic-release/git": "^10.0.1",
75
+ "@semantic-release/github": "^12.0.2",
76
+ "@semantic-release/npm": "^13.1.3",
77
+ "@semantic-release/release-notes-generator": "^14.1.0",
71
78
  "apg-js": "^4.4.0",
72
79
  "babel-plugin-module-resolver": "^5.0.2",
73
- "chai": "=5.2.0",
74
- "cross-env": "^7.0.3",
80
+ "chai": "=6.2.2",
81
+ "cross-env": "^10.0.0",
75
82
  "husky": "=9.1.7",
76
- "mocha": "=11.1.0",
77
- "mocha-expect-snapshot": "^7.2.0",
83
+ "mocha": "=11.7.5",
84
+ "mocha-expect-snapshot": "^8.0.0",
78
85
  "npm-watch": "^0.13.0",
79
- "prettier": "^3.5.2"
86
+ "prettier": "^3.5.2",
87
+ "semantic-release": "^25.0.2"
80
88
  }
81
89
  }
package/types/index.d.ts CHANGED
@@ -20,6 +20,9 @@ export declare class CSTOptimizedTranslator implements CSTTranslator {
20
20
  constructor(options?: { collapsibleTypes?: string[] });
21
21
  getTree(): CSTTree;
22
22
  }
23
+ export declare class ASTTranslator implements Translator<ASTTree> {
24
+ getTree(): ASTTree;
25
+ }
23
26
  export declare class XMLTranslator implements Translator<XMLTree> {
24
27
  getTree(): XMLTree;
25
28
  }
@@ -35,7 +38,7 @@ export interface ParseResult<TTree = ASTTree> {
35
38
  readonly maxTreeDepth: number
36
39
  readonly nodeHits: number;
37
40
  };
38
- readonly tree: TTree;
41
+ readonly tree?: TTree;
39
42
  readonly stats?: Stats;
40
43
  readonly trace?: Trace;
41
44
  }
@@ -92,6 +95,8 @@ export interface NameSelectorASTNode extends ASTNode {
92
95
  value: string;
93
96
  format: 'single-quoted' | 'double-quoted' | 'shorthand';
94
97
  }
98
+ type NameSelectorShorthandASTNode = NameSelectorASTNode & { format: 'shorthand' };
99
+ type NameSelectorQuotedASTNode = NameSelectorASTNode & { format: 'single-quoted' | 'double-quoted' };
95
100
  // https://www.rfc-editor.org/rfc/rfc9535#section-2.3.2.1
96
101
  export interface WildcardSelectorASTNode extends ASTNode {
97
102
  type: 'WildcardSelector';
@@ -166,7 +171,7 @@ export interface FunctionExprASTNode extends ASTNode {
166
171
  // https://www.rfc-editor.org/rfc/rfc9535#section-2.5.1.1
167
172
  export interface ChildSegmentASTNode extends ASTNode {
168
173
  type: 'ChildSegment';
169
- selector: BracketedSelectionASTNode | WildcardSelectorASTNode | NameSelectorASTNode;
174
+ selector: BracketedSelectionASTNode | WildcardSelectorASTNode | NameSelectorShorthandASTNode;
170
175
  }
171
176
  export interface BracketedSelectionASTNode extends ASTNode {
172
177
  type: 'BracketedSelection';
@@ -174,11 +179,11 @@ export interface BracketedSelectionASTNode extends ASTNode {
174
179
  }
175
180
  export interface DescendantSegmentASTNode extends ASTNode {
176
181
  type: 'DescendantSegment';
177
- selector: BracketedSelectionASTNode | WildcardSelectorASTNode | NameSelectorASTNode;
182
+ selector: BracketedSelectionASTNode | WildcardSelectorASTNode | NameSelectorShorthandASTNode;
178
183
  }
179
184
  // union types
180
185
  export type SelectorASTNode =
181
- | NameSelectorASTNode
186
+ | NameSelectorQuotedASTNode
182
187
  | WildcardSelectorASTNode
183
188
  | SliceSelectorASTNode
184
189
  | IndexSelectorASTNode