@irvinebroque/http-rfc-utils 0.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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/auth.d.ts +139 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +991 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/cache-status.d.ts +15 -0
  8. package/dist/cache-status.d.ts.map +1 -0
  9. package/dist/cache-status.js +152 -0
  10. package/dist/cache-status.js.map +1 -0
  11. package/dist/cache.d.ts +94 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +244 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/client-hints.d.ts +23 -0
  16. package/dist/client-hints.d.ts.map +1 -0
  17. package/dist/client-hints.js +81 -0
  18. package/dist/client-hints.js.map +1 -0
  19. package/dist/conditional.d.ts +97 -0
  20. package/dist/conditional.d.ts.map +1 -0
  21. package/dist/conditional.js +300 -0
  22. package/dist/conditional.js.map +1 -0
  23. package/dist/content-disposition.d.ts +23 -0
  24. package/dist/content-disposition.d.ts.map +1 -0
  25. package/dist/content-disposition.js +122 -0
  26. package/dist/content-disposition.js.map +1 -0
  27. package/dist/cookie.d.ts +43 -0
  28. package/dist/cookie.d.ts.map +1 -0
  29. package/dist/cookie.js +472 -0
  30. package/dist/cookie.js.map +1 -0
  31. package/dist/cors.d.ts +53 -0
  32. package/dist/cors.d.ts.map +1 -0
  33. package/dist/cors.js +170 -0
  34. package/dist/cors.js.map +1 -0
  35. package/dist/datetime.d.ts +53 -0
  36. package/dist/datetime.d.ts.map +1 -0
  37. package/dist/datetime.js +205 -0
  38. package/dist/datetime.js.map +1 -0
  39. package/dist/digest.d.ts +220 -0
  40. package/dist/digest.d.ts.map +1 -0
  41. package/dist/digest.js +355 -0
  42. package/dist/digest.js.map +1 -0
  43. package/dist/encoding.d.ts +14 -0
  44. package/dist/encoding.d.ts.map +1 -0
  45. package/dist/encoding.js +86 -0
  46. package/dist/encoding.js.map +1 -0
  47. package/dist/etag.d.ts +55 -0
  48. package/dist/etag.d.ts.map +1 -0
  49. package/dist/etag.js +182 -0
  50. package/dist/etag.js.map +1 -0
  51. package/dist/ext-value.d.ts +40 -0
  52. package/dist/ext-value.d.ts.map +1 -0
  53. package/dist/ext-value.js +119 -0
  54. package/dist/ext-value.js.map +1 -0
  55. package/dist/forwarded.d.ts +14 -0
  56. package/dist/forwarded.d.ts.map +1 -0
  57. package/dist/forwarded.js +93 -0
  58. package/dist/forwarded.js.map +1 -0
  59. package/dist/header-utils.d.ts +71 -0
  60. package/dist/header-utils.d.ts.map +1 -0
  61. package/dist/header-utils.js +143 -0
  62. package/dist/header-utils.js.map +1 -0
  63. package/dist/headers.d.ts +71 -0
  64. package/dist/headers.d.ts.map +1 -0
  65. package/dist/headers.js +134 -0
  66. package/dist/headers.js.map +1 -0
  67. package/dist/hsts.d.ts +15 -0
  68. package/dist/hsts.d.ts.map +1 -0
  69. package/dist/hsts.js +106 -0
  70. package/dist/hsts.js.map +1 -0
  71. package/dist/http-signatures.d.ts +202 -0
  72. package/dist/http-signatures.d.ts.map +1 -0
  73. package/dist/http-signatures.js +720 -0
  74. package/dist/http-signatures.js.map +1 -0
  75. package/dist/index.d.ts +41 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +125 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/json-pointer.d.ts +97 -0
  80. package/dist/json-pointer.d.ts.map +1 -0
  81. package/dist/json-pointer.js +278 -0
  82. package/dist/json-pointer.js.map +1 -0
  83. package/dist/jsonpath.d.ts +98 -0
  84. package/dist/jsonpath.d.ts.map +1 -0
  85. package/dist/jsonpath.js +1470 -0
  86. package/dist/jsonpath.js.map +1 -0
  87. package/dist/language.d.ts +14 -0
  88. package/dist/language.d.ts.map +1 -0
  89. package/dist/language.js +95 -0
  90. package/dist/language.js.map +1 -0
  91. package/dist/link.d.ts +102 -0
  92. package/dist/link.d.ts.map +1 -0
  93. package/dist/link.js +437 -0
  94. package/dist/link.js.map +1 -0
  95. package/dist/linkset.d.ts +111 -0
  96. package/dist/linkset.d.ts.map +1 -0
  97. package/dist/linkset.js +501 -0
  98. package/dist/linkset.js.map +1 -0
  99. package/dist/negotiate.d.ts +71 -0
  100. package/dist/negotiate.d.ts.map +1 -0
  101. package/dist/negotiate.js +357 -0
  102. package/dist/negotiate.js.map +1 -0
  103. package/dist/pagination.d.ts +80 -0
  104. package/dist/pagination.d.ts.map +1 -0
  105. package/dist/pagination.js +188 -0
  106. package/dist/pagination.js.map +1 -0
  107. package/dist/prefer.d.ts +18 -0
  108. package/dist/prefer.d.ts.map +1 -0
  109. package/dist/prefer.js +93 -0
  110. package/dist/prefer.js.map +1 -0
  111. package/dist/problem.d.ts +54 -0
  112. package/dist/problem.d.ts.map +1 -0
  113. package/dist/problem.js +104 -0
  114. package/dist/problem.js.map +1 -0
  115. package/dist/proxy-status.d.ts +28 -0
  116. package/dist/proxy-status.d.ts.map +1 -0
  117. package/dist/proxy-status.js +220 -0
  118. package/dist/proxy-status.js.map +1 -0
  119. package/dist/range.d.ts +28 -0
  120. package/dist/range.d.ts.map +1 -0
  121. package/dist/range.js +243 -0
  122. package/dist/range.js.map +1 -0
  123. package/dist/response.d.ts +101 -0
  124. package/dist/response.d.ts.map +1 -0
  125. package/dist/response.js +200 -0
  126. package/dist/response.js.map +1 -0
  127. package/dist/sorting.d.ts +66 -0
  128. package/dist/sorting.d.ts.map +1 -0
  129. package/dist/sorting.js +168 -0
  130. package/dist/sorting.js.map +1 -0
  131. package/dist/structured-fields.d.ts +30 -0
  132. package/dist/structured-fields.d.ts.map +1 -0
  133. package/dist/structured-fields.js +468 -0
  134. package/dist/structured-fields.js.map +1 -0
  135. package/dist/types.d.ts +772 -0
  136. package/dist/types.d.ts.map +1 -0
  137. package/dist/types.js +8 -0
  138. package/dist/types.js.map +1 -0
  139. package/dist/uri-template.d.ts +48 -0
  140. package/dist/uri-template.d.ts.map +1 -0
  141. package/dist/uri-template.js +483 -0
  142. package/dist/uri-template.js.map +1 -0
  143. package/dist/uri.d.ts +80 -0
  144. package/dist/uri.d.ts.map +1 -0
  145. package/dist/uri.js +423 -0
  146. package/dist/uri.js.map +1 -0
  147. package/package.json +66 -0
@@ -0,0 +1,1470 @@
1
+ /**
2
+ * JSONPath query expressions per RFC 9535.
3
+ * RFC 9535 §§2.1-2.7.
4
+ * @see https://www.rfc-editor.org/rfc/rfc9535.html
5
+ *
6
+ * Implements:
7
+ * - Root identifier ($) and current node identifier (@)
8
+ * - Name, wildcard, index, slice, and filter selectors
9
+ * - Child and descendant segments
10
+ * - Built-in functions: length(), count(), match(), search(), value()
11
+ * - Normalized path formatting
12
+ *
13
+ * Out of scope:
14
+ * - Custom function extensions (only built-in functions supported)
15
+ * - Full I-Regexp (RFC 9485) validation (uses JavaScript RegExp)
16
+ */
17
+ // =============================================================================
18
+ // Constants
19
+ // =============================================================================
20
+ // RFC 9535 §2.1: Integers MUST be within I-JSON range
21
+ const I_JSON_MIN = -(2 ** 53) + 1; // -9007199254740991
22
+ const I_JSON_MAX = 2 ** 53 - 1; // 9007199254740991
23
+ // Built-in function names (RFC 9535 §2.4)
24
+ const BUILTIN_FUNCTIONS = new Set([
25
+ 'length', 'count', 'match', 'search', 'value'
26
+ ]);
27
+ class Lexer {
28
+ input;
29
+ pos = 0;
30
+ tokens = [];
31
+ tokenIndex = 0;
32
+ constructor(input) {
33
+ this.input = input;
34
+ this.tokenize();
35
+ }
36
+ tokenize() {
37
+ while (this.pos < this.input.length) {
38
+ this.skipWhitespace();
39
+ if (this.pos >= this.input.length)
40
+ break;
41
+ const ch = this.input[this.pos];
42
+ const startPos = this.pos;
43
+ // Two-character tokens
44
+ if (ch === '.' && this.peek(1) === '.') {
45
+ this.pos += 2;
46
+ this.tokens.push({ type: 'DOTDOT', value: '..', pos: startPos });
47
+ continue;
48
+ }
49
+ if (ch === '&' && this.peek(1) === '&') {
50
+ this.pos += 2;
51
+ this.tokens.push({ type: 'AND', value: '&&', pos: startPos });
52
+ continue;
53
+ }
54
+ if (ch === '|' && this.peek(1) === '|') {
55
+ this.pos += 2;
56
+ this.tokens.push({ type: 'OR', value: '||', pos: startPos });
57
+ continue;
58
+ }
59
+ if (ch === '=' && this.peek(1) === '=') {
60
+ this.pos += 2;
61
+ this.tokens.push({ type: 'EQ', value: '==', pos: startPos });
62
+ continue;
63
+ }
64
+ if (ch === '!' && this.peek(1) === '=') {
65
+ this.pos += 2;
66
+ this.tokens.push({ type: 'NE', value: '!=', pos: startPos });
67
+ continue;
68
+ }
69
+ if (ch === '<' && this.peek(1) === '=') {
70
+ this.pos += 2;
71
+ this.tokens.push({ type: 'LE', value: '<=', pos: startPos });
72
+ continue;
73
+ }
74
+ if (ch === '>' && this.peek(1) === '=') {
75
+ this.pos += 2;
76
+ this.tokens.push({ type: 'GE', value: '>=', pos: startPos });
77
+ continue;
78
+ }
79
+ // Single-character tokens
80
+ switch (ch) {
81
+ case '$':
82
+ this.pos++;
83
+ this.tokens.push({ type: 'ROOT', value: '$', pos: startPos });
84
+ continue;
85
+ case '@':
86
+ this.pos++;
87
+ this.tokens.push({ type: 'CURRENT', value: '@', pos: startPos });
88
+ continue;
89
+ case '.':
90
+ this.pos++;
91
+ this.tokens.push({ type: 'DOT', value: '.', pos: startPos });
92
+ continue;
93
+ case '[':
94
+ this.pos++;
95
+ this.tokens.push({ type: 'LBRACKET', value: '[', pos: startPos });
96
+ continue;
97
+ case ']':
98
+ this.pos++;
99
+ this.tokens.push({ type: 'RBRACKET', value: ']', pos: startPos });
100
+ continue;
101
+ case '(':
102
+ this.pos++;
103
+ this.tokens.push({ type: 'LPAREN', value: '(', pos: startPos });
104
+ continue;
105
+ case ')':
106
+ this.pos++;
107
+ this.tokens.push({ type: 'RPAREN', value: ')', pos: startPos });
108
+ continue;
109
+ case ':':
110
+ this.pos++;
111
+ this.tokens.push({ type: 'COLON', value: ':', pos: startPos });
112
+ continue;
113
+ case ',':
114
+ this.pos++;
115
+ this.tokens.push({ type: 'COMMA', value: ',', pos: startPos });
116
+ continue;
117
+ case '*':
118
+ this.pos++;
119
+ this.tokens.push({ type: 'WILDCARD', value: '*', pos: startPos });
120
+ continue;
121
+ case '?':
122
+ this.pos++;
123
+ this.tokens.push({ type: 'QUESTION', value: '?', pos: startPos });
124
+ continue;
125
+ case '!':
126
+ this.pos++;
127
+ this.tokens.push({ type: 'NOT', value: '!', pos: startPos });
128
+ continue;
129
+ case '<':
130
+ this.pos++;
131
+ this.tokens.push({ type: 'LT', value: '<', pos: startPos });
132
+ continue;
133
+ case '>':
134
+ this.pos++;
135
+ this.tokens.push({ type: 'GT', value: '>', pos: startPos });
136
+ continue;
137
+ }
138
+ // String literals
139
+ if (ch === '"' || ch === "'") {
140
+ const str = this.readString(ch);
141
+ if (str === null) {
142
+ throw new Error(`Invalid string at position ${startPos}`);
143
+ }
144
+ this.tokens.push({ type: 'STRING', value: str, pos: startPos });
145
+ continue;
146
+ }
147
+ // Numbers (including negative)
148
+ if (ch === '-' || (ch >= '0' && ch <= '9')) {
149
+ const num = this.readNumber();
150
+ if (num === null) {
151
+ throw new Error(`Invalid number at position ${startPos}`);
152
+ }
153
+ this.tokens.push({ type: 'NUMBER', value: num, pos: startPos });
154
+ continue;
155
+ }
156
+ // Keywords and names
157
+ if (this.isNameFirst(ch)) {
158
+ const name = this.readName();
159
+ if (name === 'true') {
160
+ this.tokens.push({ type: 'TRUE', value: true, pos: startPos });
161
+ }
162
+ else if (name === 'false') {
163
+ this.tokens.push({ type: 'FALSE', value: false, pos: startPos });
164
+ }
165
+ else if (name === 'null') {
166
+ this.tokens.push({ type: 'NULL', value: null, pos: startPos });
167
+ }
168
+ else {
169
+ this.tokens.push({ type: 'NAME', value: name, pos: startPos });
170
+ }
171
+ continue;
172
+ }
173
+ throw new Error(`Unexpected character '${ch}' at position ${this.pos}`);
174
+ }
175
+ this.tokens.push({ type: 'EOF', value: null, pos: this.pos });
176
+ }
177
+ peek(offset = 0) {
178
+ return this.input[this.pos + offset];
179
+ }
180
+ skipWhitespace() {
181
+ // RFC 9535 §2.1.1: B = %x20 / %x09 / %x0A / %x0D
182
+ while (this.pos < this.input.length) {
183
+ const ch = this.input[this.pos];
184
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
185
+ this.pos++;
186
+ }
187
+ else {
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ // RFC 9535 §2.3.1.1: member-name-shorthand = name-first *name-char
193
+ isNameFirst(ch) {
194
+ const code = ch.codePointAt(0);
195
+ if (code === undefined)
196
+ return false;
197
+ // ALPHA / "_" / %x80-D7FF / %xE000-10FFFF
198
+ return ((code >= 0x41 && code <= 0x5A) || // A-Z
199
+ (code >= 0x61 && code <= 0x7A) || // a-z
200
+ code === 0x5F || // _
201
+ (code >= 0x80 && code <= 0xD7FF) ||
202
+ (code >= 0xE000 && code <= 0x10FFFF));
203
+ }
204
+ isNameChar(ch) {
205
+ const code = ch.codePointAt(0);
206
+ if (code === undefined)
207
+ return false;
208
+ // name-first / DIGIT
209
+ return this.isNameFirst(ch) || (code >= 0x30 && code <= 0x39);
210
+ }
211
+ readName() {
212
+ const start = this.pos;
213
+ // Handle multi-byte characters
214
+ while (this.pos < this.input.length) {
215
+ const ch = this.input[this.pos];
216
+ if (!this.isNameChar(ch))
217
+ break;
218
+ // Handle surrogate pairs
219
+ const code = ch.codePointAt(0);
220
+ if (code > 0xFFFF) {
221
+ this.pos += 2;
222
+ }
223
+ else {
224
+ this.pos++;
225
+ }
226
+ }
227
+ return this.input.slice(start, this.pos);
228
+ }
229
+ // RFC 9535 §2.3.1.1: String literal parsing with escape sequences
230
+ readString(quote) {
231
+ this.pos++; // Skip opening quote
232
+ let result = '';
233
+ while (this.pos < this.input.length) {
234
+ const ch = this.input[this.pos];
235
+ if (ch === quote) {
236
+ this.pos++; // Skip closing quote
237
+ return result;
238
+ }
239
+ if (ch === '\\') {
240
+ this.pos++;
241
+ if (this.pos >= this.input.length)
242
+ return null;
243
+ const escaped = this.input[this.pos];
244
+ switch (escaped) {
245
+ case 'b':
246
+ result += '\b';
247
+ break;
248
+ case 'f':
249
+ result += '\f';
250
+ break;
251
+ case 'n':
252
+ result += '\n';
253
+ break;
254
+ case 'r':
255
+ result += '\r';
256
+ break;
257
+ case 't':
258
+ result += '\t';
259
+ break;
260
+ case '/':
261
+ result += '/';
262
+ break;
263
+ case '\\':
264
+ result += '\\';
265
+ break;
266
+ case '"':
267
+ result += '"';
268
+ break;
269
+ case "'":
270
+ result += "'";
271
+ break;
272
+ case 'u': {
273
+ // Unicode escape: \uXXXX or surrogate pair
274
+ this.pos++;
275
+ const hex = this.input.slice(this.pos, this.pos + 4);
276
+ if (!/^[0-9A-Fa-f]{4}$/.test(hex))
277
+ return null;
278
+ const codePoint = parseInt(hex, 16);
279
+ this.pos += 4;
280
+ // Check for high surrogate
281
+ if (codePoint >= 0xD800 && codePoint <= 0xDBFF) {
282
+ // Must be followed by \uXXXX low surrogate
283
+ if (this.input.slice(this.pos, this.pos + 2) !== '\\u')
284
+ return null;
285
+ this.pos += 2;
286
+ const hex2 = this.input.slice(this.pos, this.pos + 4);
287
+ if (!/^[0-9A-Fa-f]{4}$/.test(hex2))
288
+ return null;
289
+ const lowSurrogate = parseInt(hex2, 16);
290
+ if (lowSurrogate < 0xDC00 || lowSurrogate > 0xDFFF)
291
+ return null;
292
+ this.pos += 4;
293
+ // Decode surrogate pair
294
+ const combined = 0x10000 + ((codePoint - 0xD800) << 10) + (lowSurrogate - 0xDC00);
295
+ result += String.fromCodePoint(combined);
296
+ }
297
+ else {
298
+ result += String.fromCodePoint(codePoint);
299
+ }
300
+ continue; // Already advanced pos
301
+ }
302
+ default:
303
+ return null; // Invalid escape
304
+ }
305
+ this.pos++;
306
+ }
307
+ else {
308
+ result += ch;
309
+ this.pos++;
310
+ }
311
+ }
312
+ return null; // Unterminated string
313
+ }
314
+ // RFC 9535 §2.3.3.1: int = "0" / (["-"] DIGIT1 *DIGIT)
315
+ readNumber() {
316
+ const start = this.pos;
317
+ let hasSign = false;
318
+ if (this.input[this.pos] === '-') {
319
+ hasSign = true;
320
+ this.pos++;
321
+ }
322
+ if (this.pos >= this.input.length) {
323
+ this.pos = start;
324
+ return null;
325
+ }
326
+ const firstDigit = this.input[this.pos];
327
+ // "0" by itself or leading zeros not allowed for non-zero integers
328
+ if (firstDigit === '0') {
329
+ this.pos++;
330
+ // Check if followed by more digits (invalid: 00, 01, etc.)
331
+ if (this.pos < this.input.length) {
332
+ const next = this.input[this.pos];
333
+ if (next >= '0' && next <= '9') {
334
+ // Could be a decimal
335
+ if (next !== '.') {
336
+ this.pos = start;
337
+ return null; // Leading zero in integer
338
+ }
339
+ }
340
+ }
341
+ }
342
+ else if (firstDigit >= '1' && firstDigit <= '9') {
343
+ this.pos++;
344
+ while (this.pos < this.input.length) {
345
+ const ch = this.input[this.pos];
346
+ if (ch >= '0' && ch <= '9') {
347
+ this.pos++;
348
+ }
349
+ else {
350
+ break;
351
+ }
352
+ }
353
+ }
354
+ else if (hasSign) {
355
+ // - not followed by digit
356
+ this.pos = start;
357
+ return null;
358
+ }
359
+ else {
360
+ this.pos = start;
361
+ return null;
362
+ }
363
+ const numStr = this.input.slice(start, this.pos);
364
+ const num = parseInt(numStr, 10);
365
+ // RFC 9535 §2.1: Integers MUST be within I-JSON range
366
+ if (num < I_JSON_MIN || num > I_JSON_MAX) {
367
+ return null;
368
+ }
369
+ return num;
370
+ }
371
+ // Token access methods
372
+ current() {
373
+ return this.tokens[this.tokenIndex] ?? this.tokens[this.tokens.length - 1];
374
+ }
375
+ advance() {
376
+ const token = this.current();
377
+ if (this.tokenIndex < this.tokens.length - 1) {
378
+ this.tokenIndex++;
379
+ }
380
+ return token;
381
+ }
382
+ check(type) {
383
+ return this.current().type === type;
384
+ }
385
+ match(...types) {
386
+ for (const type of types) {
387
+ if (this.check(type)) {
388
+ this.advance();
389
+ return true;
390
+ }
391
+ }
392
+ return false;
393
+ }
394
+ expect(type) {
395
+ if (!this.check(type)) {
396
+ throw new Error(`Expected ${type} but got ${this.current().type}`);
397
+ }
398
+ return this.advance();
399
+ }
400
+ isAtEnd() {
401
+ return this.check('EOF');
402
+ }
403
+ }
404
+ // =============================================================================
405
+ // Parser
406
+ // =============================================================================
407
+ /**
408
+ * Parse a JSONPath query string into an AST.
409
+ * Returns null if the query is not well-formed or valid.
410
+ *
411
+ * @param query - JSONPath query string (e.g., "$.store.book[*].author")
412
+ * @returns Parsed AST, or null on invalid syntax
413
+ *
414
+ * @example
415
+ * parseJsonPath('$.store.book[*]') // { type: 'query', root: '$', segments: [...] }
416
+ * parseJsonPath('invalid') // null
417
+ *
418
+ * @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.1
419
+ */
420
+ export function parseJsonPath(query) {
421
+ try {
422
+ const lexer = new Lexer(query);
423
+ const ast = parseQuery(lexer, '$');
424
+ // Must consume entire input
425
+ if (!lexer.isAtEnd()) {
426
+ return null;
427
+ }
428
+ return ast;
429
+ }
430
+ catch {
431
+ return null;
432
+ }
433
+ }
434
+ function parseQuery(lexer, expectedRoot) {
435
+ // RFC 9535 §2.2.1: jsonpath-query = root-identifier segments
436
+ const rootType = expectedRoot === '$' ? 'ROOT' : 'CURRENT';
437
+ if (!lexer.match(rootType)) {
438
+ return null;
439
+ }
440
+ const segments = parseSegments(lexer);
441
+ return {
442
+ type: 'query',
443
+ root: expectedRoot,
444
+ segments,
445
+ };
446
+ }
447
+ function parseSegments(lexer) {
448
+ // RFC 9535 §2.1.1: segments = *(S segment)
449
+ const segments = [];
450
+ while (!lexer.isAtEnd()) {
451
+ const segment = parseSegment(lexer);
452
+ if (segment === null)
453
+ break;
454
+ segments.push(segment);
455
+ }
456
+ return segments;
457
+ }
458
+ function parseSegment(lexer) {
459
+ // RFC 9535 §2.5: segment = child-segment / descendant-segment
460
+ // Descendant segment: ".." (bracketed-selection / wildcard-selector / member-name-shorthand)
461
+ if (lexer.match('DOTDOT')) {
462
+ return parseDescendantSegment(lexer);
463
+ }
464
+ // Child segment: "[" ... "]" or "." (wildcard-selector / member-name-shorthand)
465
+ if (lexer.check('LBRACKET')) {
466
+ return parseChildBracketSegment(lexer);
467
+ }
468
+ if (lexer.match('DOT')) {
469
+ return parseChildDotSegment(lexer);
470
+ }
471
+ return null;
472
+ }
473
+ function parseDescendantSegment(lexer) {
474
+ // RFC 9535 §2.5.2.1: descendant-segment = ".." (bracketed-selection / wildcard / member-name)
475
+ if (lexer.check('LBRACKET')) {
476
+ const selectors = parseBracketedSelection(lexer);
477
+ if (selectors === null)
478
+ return null;
479
+ return { type: 'descendant', selectors };
480
+ }
481
+ if (lexer.match('WILDCARD')) {
482
+ return { type: 'descendant', selectors: [{ type: 'wildcard' }] };
483
+ }
484
+ if (lexer.check('NAME')) {
485
+ const name = lexer.advance().value;
486
+ return { type: 'descendant', selectors: [{ type: 'name', name }] };
487
+ }
488
+ return null;
489
+ }
490
+ function parseChildBracketSegment(lexer) {
491
+ const selectors = parseBracketedSelection(lexer);
492
+ if (selectors === null)
493
+ return null;
494
+ return { type: 'child', selectors };
495
+ }
496
+ function parseChildDotSegment(lexer) {
497
+ // RFC 9535 §2.5.1.1: "." (wildcard-selector / member-name-shorthand)
498
+ if (lexer.match('WILDCARD')) {
499
+ return { type: 'child', selectors: [{ type: 'wildcard' }] };
500
+ }
501
+ if (lexer.check('NAME')) {
502
+ const name = lexer.advance().value;
503
+ return { type: 'child', selectors: [{ type: 'name', name }] };
504
+ }
505
+ return null;
506
+ }
507
+ function parseBracketedSelection(lexer) {
508
+ // RFC 9535 §2.5.1.1: bracketed-selection = "[" S selector *( S "," S selector ) S "]"
509
+ if (!lexer.match('LBRACKET'))
510
+ return null;
511
+ const selectors = [];
512
+ const first = parseSelector(lexer);
513
+ if (first === null)
514
+ return null;
515
+ selectors.push(first);
516
+ while (lexer.match('COMMA')) {
517
+ const next = parseSelector(lexer);
518
+ if (next === null)
519
+ return null;
520
+ selectors.push(next);
521
+ }
522
+ if (!lexer.match('RBRACKET'))
523
+ return null;
524
+ return selectors;
525
+ }
526
+ function parseSelector(lexer) {
527
+ // RFC 9535 §2.3: selector = name-selector / wildcard / slice / index / filter
528
+ // Filter selector: "?" logical-expr
529
+ if (lexer.match('QUESTION')) {
530
+ const expr = parseLogicalExpr(lexer);
531
+ if (expr === null)
532
+ return null;
533
+ return { type: 'filter', expression: expr };
534
+ }
535
+ // Wildcard selector
536
+ if (lexer.match('WILDCARD')) {
537
+ return { type: 'wildcard' };
538
+ }
539
+ // Name selector (string literal)
540
+ if (lexer.check('STRING')) {
541
+ const name = lexer.advance().value;
542
+ return { type: 'name', name };
543
+ }
544
+ // Index or slice selector
545
+ if (lexer.check('NUMBER') || lexer.check('COLON')) {
546
+ return parseIndexOrSlice(lexer);
547
+ }
548
+ return null;
549
+ }
550
+ function parseIndexOrSlice(lexer) {
551
+ // RFC 9535 §2.3.3/§2.3.4: Determine if this is an index or slice
552
+ let start;
553
+ let end;
554
+ let step;
555
+ let isSlice = false;
556
+ // Start value
557
+ if (lexer.check('NUMBER')) {
558
+ start = lexer.advance().value;
559
+ }
560
+ // First colon (makes it a slice)
561
+ if (lexer.match('COLON')) {
562
+ isSlice = true;
563
+ // End value
564
+ if (lexer.check('NUMBER')) {
565
+ end = lexer.advance().value;
566
+ }
567
+ // Second colon and step
568
+ if (lexer.match('COLON')) {
569
+ if (lexer.check('NUMBER')) {
570
+ step = lexer.advance().value;
571
+ }
572
+ }
573
+ }
574
+ if (isSlice) {
575
+ return { type: 'slice', start, end, step };
576
+ }
577
+ if (start !== undefined) {
578
+ return { type: 'index', index: start };
579
+ }
580
+ return null;
581
+ }
582
+ // =============================================================================
583
+ // Filter Expression Parser
584
+ // =============================================================================
585
+ function parseLogicalExpr(lexer) {
586
+ // RFC 9535 §2.3.5.1: logical-expr = logical-or-expr
587
+ return parseLogicalOrExpr(lexer);
588
+ }
589
+ function parseLogicalOrExpr(lexer) {
590
+ // RFC 9535 §2.3.5.1: logical-or-expr = logical-and-expr *( "||" S logical-and-expr )
591
+ let left = parseLogicalAndExpr(lexer);
592
+ if (left === null)
593
+ return null;
594
+ const operands = [left];
595
+ while (lexer.match('OR')) {
596
+ const right = parseLogicalAndExpr(lexer);
597
+ if (right === null)
598
+ return null;
599
+ operands.push(right);
600
+ }
601
+ if (operands.length === 1) {
602
+ return operands[0];
603
+ }
604
+ return { type: 'or', operands };
605
+ }
606
+ function parseLogicalAndExpr(lexer) {
607
+ // RFC 9535 §2.3.5.1: logical-and-expr = basic-expr *( "&&" S basic-expr )
608
+ let left = parseBasicExpr(lexer);
609
+ if (left === null)
610
+ return null;
611
+ const operands = [left];
612
+ while (lexer.match('AND')) {
613
+ const right = parseBasicExpr(lexer);
614
+ if (right === null)
615
+ return null;
616
+ operands.push(right);
617
+ }
618
+ if (operands.length === 1) {
619
+ return operands[0];
620
+ }
621
+ return { type: 'and', operands };
622
+ }
623
+ function parseBasicExpr(lexer) {
624
+ // RFC 9535 §2.3.5.1: basic-expr = paren-expr / comparison-expr / test-expr
625
+ // Parenthesized expression
626
+ if (lexer.match('LPAREN')) {
627
+ const expr = parseLogicalExpr(lexer);
628
+ if (expr === null)
629
+ return null;
630
+ if (!lexer.match('RPAREN'))
631
+ return null;
632
+ return expr;
633
+ }
634
+ // Negation
635
+ if (lexer.match('NOT')) {
636
+ const operand = parseBasicExpr(lexer);
637
+ if (operand === null)
638
+ return null;
639
+ return { type: 'not', operand };
640
+ }
641
+ // Try to parse a comparable (for comparison) or test expression
642
+ return parseComparisonOrTest(lexer);
643
+ }
644
+ function parseComparisonOrTest(lexer) {
645
+ // This handles both comparison expressions and test expressions
646
+ // We need to look ahead to determine which one
647
+ // Check for filter query (existence test): @... or $...
648
+ if (lexer.check('CURRENT') || lexer.check('ROOT')) {
649
+ const query = parseFilterQuery(lexer);
650
+ if (query === null)
651
+ return null;
652
+ // Check if followed by comparison operator
653
+ const op = tryParseComparisonOp(lexer);
654
+ if (op !== null) {
655
+ const right = parseComparable(lexer);
656
+ if (right === null)
657
+ return null;
658
+ // Convert query to singular-query comparable
659
+ const left = {
660
+ type: 'singular-query',
661
+ root: query.root,
662
+ segments: query.segments,
663
+ };
664
+ return { type: 'comparison', operator: op, left, right };
665
+ }
666
+ // It's a test expression
667
+ return { type: 'test', query };
668
+ }
669
+ // Check for function call
670
+ if (lexer.check('NAME')) {
671
+ const func = parseFunctionExpr(lexer);
672
+ if (func === null)
673
+ return null;
674
+ // Check if followed by comparison operator
675
+ const op = tryParseComparisonOp(lexer);
676
+ if (op !== null) {
677
+ const right = parseComparable(lexer);
678
+ if (right === null)
679
+ return null;
680
+ return { type: 'comparison', operator: op, left: func, right };
681
+ }
682
+ // It's a test expression (function returning LogicalType)
683
+ return func;
684
+ }
685
+ // Literal followed by comparison
686
+ const literal = parseLiteral(lexer);
687
+ if (literal !== null) {
688
+ const op = tryParseComparisonOp(lexer);
689
+ if (op !== null) {
690
+ const right = parseComparable(lexer);
691
+ if (right === null)
692
+ return null;
693
+ return { type: 'comparison', operator: op, left: literal, right };
694
+ }
695
+ // Bare literal is not valid as a test expression
696
+ return null;
697
+ }
698
+ return null;
699
+ }
700
+ function parseFilterQuery(lexer) {
701
+ // RFC 9535 §2.3.5.1: filter-query = rel-query / jsonpath-query
702
+ if (lexer.check('ROOT')) {
703
+ return parseQuery(lexer, '$');
704
+ }
705
+ if (lexer.check('CURRENT')) {
706
+ return parseQuery(lexer, '@');
707
+ }
708
+ return null;
709
+ }
710
+ function tryParseComparisonOp(lexer) {
711
+ if (lexer.match('EQ'))
712
+ return '==';
713
+ if (lexer.match('NE'))
714
+ return '!=';
715
+ if (lexer.match('LE'))
716
+ return '<=';
717
+ if (lexer.match('GE'))
718
+ return '>=';
719
+ if (lexer.match('LT'))
720
+ return '<';
721
+ if (lexer.match('GT'))
722
+ return '>';
723
+ return null;
724
+ }
725
+ function parseComparable(lexer) {
726
+ // RFC 9535 §2.3.5.1: comparable = literal / singular-query / function-expr
727
+ // Function
728
+ if (lexer.check('NAME')) {
729
+ return parseFunctionExpr(lexer);
730
+ }
731
+ // Singular query
732
+ if (lexer.check('ROOT') || lexer.check('CURRENT')) {
733
+ const query = parseFilterQuery(lexer);
734
+ if (query === null)
735
+ return null;
736
+ return {
737
+ type: 'singular-query',
738
+ root: query.root,
739
+ segments: query.segments,
740
+ };
741
+ }
742
+ // Literal
743
+ return parseLiteral(lexer);
744
+ }
745
+ function parseLiteral(lexer) {
746
+ if (lexer.check('STRING')) {
747
+ return { type: 'literal', value: lexer.advance().value };
748
+ }
749
+ if (lexer.check('NUMBER')) {
750
+ return { type: 'literal', value: lexer.advance().value };
751
+ }
752
+ if (lexer.match('TRUE')) {
753
+ return { type: 'literal', value: true };
754
+ }
755
+ if (lexer.match('FALSE')) {
756
+ return { type: 'literal', value: false };
757
+ }
758
+ if (lexer.match('NULL')) {
759
+ return { type: 'literal', value: null };
760
+ }
761
+ return null;
762
+ }
763
+ function parseFunctionExpr(lexer) {
764
+ // RFC 9535 §2.4: function-expr = function-name "(" [function-argument *( "," function-argument )] ")"
765
+ if (!lexer.check('NAME'))
766
+ return null;
767
+ const name = lexer.advance().value;
768
+ if (!BUILTIN_FUNCTIONS.has(name)) {
769
+ return null;
770
+ }
771
+ if (!lexer.match('LPAREN'))
772
+ return null;
773
+ const args = [];
774
+ if (!lexer.check('RPAREN')) {
775
+ const first = parseFunctionArg(lexer);
776
+ if (first === null)
777
+ return null;
778
+ args.push(first);
779
+ while (lexer.match('COMMA')) {
780
+ const next = parseFunctionArg(lexer);
781
+ if (next === null)
782
+ return null;
783
+ args.push(next);
784
+ }
785
+ }
786
+ if (!lexer.match('RPAREN'))
787
+ return null;
788
+ return {
789
+ type: 'function',
790
+ name: name,
791
+ args,
792
+ };
793
+ }
794
+ function parseFunctionArg(lexer) {
795
+ // RFC 9535 §2.4: function-argument = literal / filter-query / function-expr
796
+ // Function
797
+ if (lexer.check('NAME')) {
798
+ return parseFunctionExpr(lexer);
799
+ }
800
+ // Filter query
801
+ if (lexer.check('ROOT') || lexer.check('CURRENT')) {
802
+ return parseFilterQuery(lexer);
803
+ }
804
+ // Literal
805
+ return parseLiteral(lexer);
806
+ }
807
+ /**
808
+ * Execute a JSONPath query against a JSON document.
809
+ * Returns an array of matching values (nodelist).
810
+ *
811
+ * @param query - JSONPath query string
812
+ * @param document - JSON document to query
813
+ * @param options - Query options
814
+ * @returns Array of matching values, or null on invalid query
815
+ *
816
+ * @example
817
+ * queryJsonPath('$.store.book[*].author', doc) // ['Author1', 'Author2']
818
+ * queryJsonPath('$..price', doc) // [8.95, 12.99, 399]
819
+ * queryJsonPath('$.store.book[?@.price<10]', doc) // [{ title: 'Book1', ... }]
820
+ *
821
+ * @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.1.2
822
+ */
823
+ export function queryJsonPath(query, document, options) {
824
+ const ast = parseJsonPath(query);
825
+ if (ast === null) {
826
+ if (options?.throwOnError) {
827
+ throw new Error(`Invalid JSONPath query: ${query}`);
828
+ }
829
+ return null;
830
+ }
831
+ const nodes = evaluateQuery(ast, document);
832
+ return nodes.map(n => n.value);
833
+ }
834
+ /**
835
+ * Execute a JSONPath query and return nodes with paths.
836
+ *
837
+ * @param query - JSONPath query string
838
+ * @param document - JSON document to query
839
+ * @returns Array of nodes with values and paths, or null on invalid query
840
+ *
841
+ * @example
842
+ * queryJsonPathNodes('$.store.book[0].title', doc)
843
+ * // [{ value: 'Sayings of the Century', path: "$['store']['book'][0]['title']" }]
844
+ *
845
+ * @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7
846
+ */
847
+ export function queryJsonPathNodes(query, document) {
848
+ const ast = parseJsonPath(query);
849
+ if (ast === null)
850
+ return null;
851
+ return evaluateQuery(ast, document);
852
+ }
853
+ /**
854
+ * Validate a JSONPath query string without parsing.
855
+ *
856
+ * @param query - String to validate
857
+ * @returns true if valid JSONPath syntax
858
+ *
859
+ * @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.1
860
+ */
861
+ export function isValidJsonPath(query) {
862
+ return parseJsonPath(query) !== null;
863
+ }
864
+ /**
865
+ * Format a normalized path from path segments.
866
+ *
867
+ * @param segments - Array of string keys or numeric indices
868
+ * @returns Normalized path string
869
+ *
870
+ * @example
871
+ * formatNormalizedPath(['store', 'book', 0]) // "$['store']['book'][0]"
872
+ *
873
+ * @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7
874
+ */
875
+ export function formatNormalizedPath(segments) {
876
+ // RFC 9535 §2.7: normalized-path = root-identifier *(normal-index-segment / normal-name-segment)
877
+ let path = '$';
878
+ for (const seg of segments) {
879
+ if (typeof seg === 'number') {
880
+ path += `[${seg}]`;
881
+ }
882
+ else {
883
+ // Escape single quotes and backslashes
884
+ const escaped = seg
885
+ .replace(/\\/g, '\\\\')
886
+ .replace(/'/g, "\\'");
887
+ path += `['${escaped}']`;
888
+ }
889
+ }
890
+ return path;
891
+ }
892
+ /**
893
+ * Compile a JSONPath query for repeated execution.
894
+ * More efficient when the same query is used multiple times.
895
+ *
896
+ * @param query - JSONPath query string
897
+ * @returns Compiled query function, or null on invalid query
898
+ *
899
+ * @example
900
+ * const fn = compileJsonPath('$.store.book[*].author');
901
+ * fn(doc1) // ['Author1', ...]
902
+ * fn(doc2) // ['Author2', ...]
903
+ */
904
+ export function compileJsonPath(query) {
905
+ const ast = parseJsonPath(query);
906
+ if (ast === null)
907
+ return null;
908
+ return (document) => {
909
+ const nodes = evaluateQuery(ast, document);
910
+ return nodes.map(n => n.value);
911
+ };
912
+ }
913
+ function evaluateQuery(ast, document) {
914
+ // RFC 9535 §2.1.2: Start with root node
915
+ const ctx = {
916
+ root: document,
917
+ current: document,
918
+ currentPath: [],
919
+ };
920
+ let nodes = [{
921
+ value: document,
922
+ path: '$',
923
+ }];
924
+ // Apply each segment in sequence
925
+ for (const segment of ast.segments) {
926
+ nodes = evaluateSegment(segment, nodes, ctx);
927
+ }
928
+ return nodes;
929
+ }
930
+ function evaluateSegment(segment, nodes, ctx) {
931
+ if (segment.type === 'child') {
932
+ return evaluateChildSegment(segment, nodes, ctx);
933
+ }
934
+ else {
935
+ return evaluateDescendantSegment(segment, nodes, ctx);
936
+ }
937
+ }
938
+ function evaluateChildSegment(segment, nodes, ctx) {
939
+ // RFC 9535 §2.5.1.2: Apply selectors to each input node
940
+ const result = [];
941
+ for (const node of nodes) {
942
+ for (const selector of segment.selectors) {
943
+ const selected = evaluateSelector(selector, node, ctx);
944
+ result.push(...selected);
945
+ }
946
+ }
947
+ return result;
948
+ }
949
+ function evaluateDescendantSegment(segment, nodes, ctx) {
950
+ // RFC 9535 §2.5.2.2: Select from node and all its descendants
951
+ const result = [];
952
+ for (const node of nodes) {
953
+ // Collect all descendants including the node itself
954
+ const allNodes = collectDescendants(node);
955
+ for (const descNode of allNodes) {
956
+ for (const selector of segment.selectors) {
957
+ const selected = evaluateSelector(selector, descNode, ctx);
958
+ result.push(...selected);
959
+ }
960
+ }
961
+ }
962
+ return result;
963
+ }
964
+ function collectDescendants(node) {
965
+ const result = [node];
966
+ const value = node.value;
967
+ if (Array.isArray(value)) {
968
+ for (let i = 0; i < value.length; i++) {
969
+ const childPath = parseNormalizedPath(node.path);
970
+ childPath.push(i);
971
+ const childNode = {
972
+ value: value[i],
973
+ path: formatNormalizedPath(childPath),
974
+ };
975
+ result.push(...collectDescendants(childNode));
976
+ }
977
+ }
978
+ else if (value !== null && typeof value === 'object') {
979
+ for (const key of Object.keys(value)) {
980
+ const childPath = parseNormalizedPath(node.path);
981
+ childPath.push(key);
982
+ const childNode = {
983
+ value: value[key],
984
+ path: formatNormalizedPath(childPath),
985
+ };
986
+ result.push(...collectDescendants(childNode));
987
+ }
988
+ }
989
+ return result;
990
+ }
991
+ function parseNormalizedPath(path) {
992
+ // Parse a normalized path back into segments
993
+ const segments = [];
994
+ if (!path.startsWith('$'))
995
+ return segments;
996
+ let i = 1; // Skip $
997
+ while (i < path.length) {
998
+ if (path[i] === '[') {
999
+ i++; // Skip [
1000
+ if (path[i] === "'") {
1001
+ // String segment
1002
+ i++; // Skip opening quote
1003
+ let name = '';
1004
+ while (i < path.length && path[i] !== "'") {
1005
+ if (path[i] === '\\' && i + 1 < path.length) {
1006
+ i++;
1007
+ name += path[i];
1008
+ }
1009
+ else {
1010
+ name += path[i];
1011
+ }
1012
+ i++;
1013
+ }
1014
+ i++; // Skip closing quote
1015
+ segments.push(name);
1016
+ }
1017
+ else {
1018
+ // Numeric segment
1019
+ let numStr = '';
1020
+ while (i < path.length && path[i] !== ']') {
1021
+ numStr += path[i];
1022
+ i++;
1023
+ }
1024
+ segments.push(parseInt(numStr, 10));
1025
+ }
1026
+ i++; // Skip ]
1027
+ }
1028
+ else {
1029
+ break;
1030
+ }
1031
+ }
1032
+ return segments;
1033
+ }
1034
+ function evaluateSelector(selector, node, ctx) {
1035
+ switch (selector.type) {
1036
+ case 'name':
1037
+ return evaluateNameSelector(selector, node);
1038
+ case 'wildcard':
1039
+ return evaluateWildcardSelector(node);
1040
+ case 'index':
1041
+ return evaluateIndexSelector(selector, node);
1042
+ case 'slice':
1043
+ return evaluateSliceSelector(selector, node);
1044
+ case 'filter':
1045
+ return evaluateFilterSelector(selector, node, ctx);
1046
+ }
1047
+ }
1048
+ function evaluateNameSelector(selector, node) {
1049
+ // RFC 9535 §2.3.1.2: Select member value by name
1050
+ const value = node.value;
1051
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
1052
+ return [];
1053
+ }
1054
+ const obj = value;
1055
+ if (!(selector.name in obj)) {
1056
+ return [];
1057
+ }
1058
+ const childPath = parseNormalizedPath(node.path);
1059
+ childPath.push(selector.name);
1060
+ return [{
1061
+ value: obj[selector.name],
1062
+ path: formatNormalizedPath(childPath),
1063
+ }];
1064
+ }
1065
+ function evaluateWildcardSelector(node) {
1066
+ // RFC 9535 §2.3.2.2: Select all children
1067
+ const value = node.value;
1068
+ const result = [];
1069
+ if (Array.isArray(value)) {
1070
+ for (let i = 0; i < value.length; i++) {
1071
+ const childPath = parseNormalizedPath(node.path);
1072
+ childPath.push(i);
1073
+ result.push({
1074
+ value: value[i],
1075
+ path: formatNormalizedPath(childPath),
1076
+ });
1077
+ }
1078
+ }
1079
+ else if (value !== null && typeof value === 'object') {
1080
+ for (const key of Object.keys(value)) {
1081
+ const childPath = parseNormalizedPath(node.path);
1082
+ childPath.push(key);
1083
+ result.push({
1084
+ value: value[key],
1085
+ path: formatNormalizedPath(childPath),
1086
+ });
1087
+ }
1088
+ }
1089
+ return result;
1090
+ }
1091
+ function evaluateIndexSelector(selector, node) {
1092
+ // RFC 9535 §2.3.3.2: Select array element by index
1093
+ const value = node.value;
1094
+ if (!Array.isArray(value)) {
1095
+ return [];
1096
+ }
1097
+ let index = selector.index;
1098
+ // RFC 9535 §2.3.3.2: Negative index counts from end
1099
+ if (index < 0) {
1100
+ index = value.length + index;
1101
+ }
1102
+ if (index < 0 || index >= value.length) {
1103
+ return [];
1104
+ }
1105
+ const childPath = parseNormalizedPath(node.path);
1106
+ childPath.push(index);
1107
+ return [{
1108
+ value: value[index],
1109
+ path: formatNormalizedPath(childPath),
1110
+ }];
1111
+ }
1112
+ function evaluateSliceSelector(selector, node) {
1113
+ // RFC 9535 §2.3.4.2: Array slice
1114
+ const value = node.value;
1115
+ if (!Array.isArray(value)) {
1116
+ return [];
1117
+ }
1118
+ const len = value.length;
1119
+ const step = selector.step ?? 1;
1120
+ // RFC 9535 §2.3.4.2.1: step of 0 selects no elements
1121
+ if (step === 0) {
1122
+ return [];
1123
+ }
1124
+ // RFC 9535 Table 8: Default values depend on step sign
1125
+ const defaultStart = step >= 0 ? 0 : len - 1;
1126
+ const defaultEnd = step >= 0 ? len : -len - 1;
1127
+ const start = selector.start ?? defaultStart;
1128
+ const end = selector.end ?? defaultEnd;
1129
+ // Normalize
1130
+ const nStart = normalize(start, len);
1131
+ const nEnd = normalize(end, len);
1132
+ // Compute bounds
1133
+ let lower, upper;
1134
+ if (step >= 0) {
1135
+ lower = Math.min(Math.max(nStart, 0), len);
1136
+ upper = Math.min(Math.max(nEnd, 0), len);
1137
+ }
1138
+ else {
1139
+ lower = Math.min(Math.max(nEnd, -1), len - 1);
1140
+ upper = Math.min(Math.max(nStart, -1), len - 1);
1141
+ }
1142
+ const result = [];
1143
+ const basePath = parseNormalizedPath(node.path);
1144
+ if (step > 0) {
1145
+ for (let i = lower; i < upper; i += step) {
1146
+ const childPath = [...basePath, i];
1147
+ result.push({
1148
+ value: value[i],
1149
+ path: formatNormalizedPath(childPath),
1150
+ });
1151
+ }
1152
+ }
1153
+ else {
1154
+ for (let i = upper; i > lower; i += step) {
1155
+ const childPath = [...basePath, i];
1156
+ result.push({
1157
+ value: value[i],
1158
+ path: formatNormalizedPath(childPath),
1159
+ });
1160
+ }
1161
+ }
1162
+ return result;
1163
+ }
1164
+ function normalize(i, len) {
1165
+ return i >= 0 ? i : len + i;
1166
+ }
1167
+ function evaluateFilterSelector(selector, node, ctx) {
1168
+ // RFC 9535 §2.3.5.2: Filter children by logical expression
1169
+ const value = node.value;
1170
+ const result = [];
1171
+ if (Array.isArray(value)) {
1172
+ for (let i = 0; i < value.length; i++) {
1173
+ const childPath = parseNormalizedPath(node.path);
1174
+ childPath.push(i);
1175
+ const childNode = {
1176
+ value: value[i],
1177
+ path: formatNormalizedPath(childPath),
1178
+ };
1179
+ const childCtx = {
1180
+ ...ctx,
1181
+ current: value[i],
1182
+ currentPath: childPath,
1183
+ };
1184
+ if (evaluateLogicalExpr(selector.expression, childCtx)) {
1185
+ result.push(childNode);
1186
+ }
1187
+ }
1188
+ }
1189
+ else if (value !== null && typeof value === 'object') {
1190
+ for (const key of Object.keys(value)) {
1191
+ const childPath = parseNormalizedPath(node.path);
1192
+ childPath.push(key);
1193
+ const childValue = value[key];
1194
+ const childNode = {
1195
+ value: childValue,
1196
+ path: formatNormalizedPath(childPath),
1197
+ };
1198
+ const childCtx = {
1199
+ ...ctx,
1200
+ current: childValue,
1201
+ currentPath: childPath,
1202
+ };
1203
+ if (evaluateLogicalExpr(selector.expression, childCtx)) {
1204
+ result.push(childNode);
1205
+ }
1206
+ }
1207
+ }
1208
+ return result;
1209
+ }
1210
+ // =============================================================================
1211
+ // Logical Expression Evaluator
1212
+ // =============================================================================
1213
+ function evaluateLogicalExpr(expr, ctx) {
1214
+ switch (expr.type) {
1215
+ case 'or':
1216
+ return evaluateOrExpr(expr, ctx);
1217
+ case 'and':
1218
+ return evaluateAndExpr(expr, ctx);
1219
+ case 'not':
1220
+ return evaluateNotExpr(expr, ctx);
1221
+ case 'comparison':
1222
+ return evaluateComparisonExpr(expr, ctx);
1223
+ case 'test':
1224
+ return evaluateTestExpr(expr, ctx);
1225
+ case 'function':
1226
+ return evaluateFunctionAsBool(expr, ctx);
1227
+ }
1228
+ }
1229
+ function evaluateOrExpr(expr, ctx) {
1230
+ for (const operand of expr.operands) {
1231
+ if (evaluateLogicalExpr(operand, ctx)) {
1232
+ return true;
1233
+ }
1234
+ }
1235
+ return false;
1236
+ }
1237
+ function evaluateAndExpr(expr, ctx) {
1238
+ for (const operand of expr.operands) {
1239
+ if (!evaluateLogicalExpr(operand, ctx)) {
1240
+ return false;
1241
+ }
1242
+ }
1243
+ return true;
1244
+ }
1245
+ function evaluateNotExpr(expr, ctx) {
1246
+ return !evaluateLogicalExpr(expr.operand, ctx);
1247
+ }
1248
+ function evaluateComparisonExpr(expr, ctx) {
1249
+ const left = evaluateComparable(expr.left, ctx);
1250
+ const right = evaluateComparable(expr.right, ctx);
1251
+ return compare(left, expr.operator, right);
1252
+ }
1253
+ function evaluateTestExpr(expr, ctx) {
1254
+ // RFC 9535 §2.3.5.2: Existence test - true if nodelist is non-empty
1255
+ const nodes = evaluateFilterQuery(expr.query, ctx);
1256
+ return nodes.length > 0;
1257
+ }
1258
+ function evaluateFunctionAsBool(expr, ctx) {
1259
+ // RFC 9535 §2.4: Functions returning LogicalType
1260
+ const result = evaluateFunction(expr, ctx);
1261
+ return result === true;
1262
+ }
1263
+ function evaluateComparable(comp, ctx) {
1264
+ switch (comp.type) {
1265
+ case 'literal':
1266
+ return comp.value;
1267
+ case 'singular-query':
1268
+ return evaluateSingularQuery(comp, ctx);
1269
+ case 'function':
1270
+ return evaluateFunction(comp, ctx);
1271
+ }
1272
+ }
1273
+ function evaluateSingularQuery(query, ctx) {
1274
+ const startValue = query.root === '$' ? ctx.root : ctx.current;
1275
+ const startPath = query.root === '$' ? [] : [...ctx.currentPath];
1276
+ let nodes = [{
1277
+ value: startValue,
1278
+ path: formatNormalizedPath(startPath),
1279
+ }];
1280
+ for (const segment of query.segments) {
1281
+ nodes = evaluateSegment(segment, nodes, ctx);
1282
+ }
1283
+ // RFC 9535 §2.3.5.1: Singular query produces at most one node
1284
+ return nodes.length === 1 ? nodes[0].value : undefined;
1285
+ }
1286
+ function evaluateFilterQuery(query, ctx) {
1287
+ const startValue = query.root === '$' ? ctx.root : ctx.current;
1288
+ const startPath = query.root === '$' ? [] : [...ctx.currentPath];
1289
+ let nodes = [{
1290
+ value: startValue,
1291
+ path: formatNormalizedPath(startPath),
1292
+ }];
1293
+ for (const segment of query.segments) {
1294
+ nodes = evaluateSegment(segment, nodes, ctx);
1295
+ }
1296
+ return nodes;
1297
+ }
1298
+ // =============================================================================
1299
+ // Comparison
1300
+ // =============================================================================
1301
+ function compare(left, op, right) {
1302
+ // RFC 9535 §2.3.5.2.2: Comparison semantics
1303
+ switch (op) {
1304
+ case '==':
1305
+ return deepEqual(left, right);
1306
+ case '!=':
1307
+ return !deepEqual(left, right);
1308
+ case '<':
1309
+ return compareLessThan(left, right);
1310
+ case '<=':
1311
+ return compareLessThanOrEqual(left, right);
1312
+ case '>':
1313
+ return compareLessThan(right, left);
1314
+ case '>=':
1315
+ return compareLessThanOrEqual(right, left);
1316
+ }
1317
+ }
1318
+ function deepEqual(a, b) {
1319
+ if (a === b)
1320
+ return true;
1321
+ if (typeof a !== typeof b)
1322
+ return false;
1323
+ if (a === null || b === null)
1324
+ return a === b;
1325
+ if (Array.isArray(a) && Array.isArray(b)) {
1326
+ if (a.length !== b.length)
1327
+ return false;
1328
+ for (let i = 0; i < a.length; i++) {
1329
+ if (!deepEqual(a[i], b[i]))
1330
+ return false;
1331
+ }
1332
+ return true;
1333
+ }
1334
+ if (typeof a === 'object' && typeof b === 'object') {
1335
+ const keysA = Object.keys(a);
1336
+ const keysB = Object.keys(b);
1337
+ if (keysA.length !== keysB.length)
1338
+ return false;
1339
+ for (const key of keysA) {
1340
+ if (!keysB.includes(key))
1341
+ return false;
1342
+ if (!deepEqual(a[key], b[key]))
1343
+ return false;
1344
+ }
1345
+ return true;
1346
+ }
1347
+ return false;
1348
+ }
1349
+ function compareLessThan(left, right) {
1350
+ // RFC 9535 §2.3.5.2.2: < only defined for numbers and strings of same type
1351
+ if (typeof left === 'number' && typeof right === 'number') {
1352
+ return left < right;
1353
+ }
1354
+ if (typeof left === 'string' && typeof right === 'string') {
1355
+ return left < right;
1356
+ }
1357
+ return false;
1358
+ }
1359
+ function compareLessThanOrEqual(left, right) {
1360
+ if (typeof left === 'number' && typeof right === 'number') {
1361
+ return left <= right;
1362
+ }
1363
+ if (typeof left === 'string' && typeof right === 'string') {
1364
+ return left <= right;
1365
+ }
1366
+ return false;
1367
+ }
1368
+ // =============================================================================
1369
+ // Built-in Functions
1370
+ // =============================================================================
1371
+ function evaluateFunction(expr, ctx) {
1372
+ switch (expr.name) {
1373
+ case 'length':
1374
+ return fnLength(expr.args, ctx);
1375
+ case 'count':
1376
+ return fnCount(expr.args, ctx);
1377
+ case 'match':
1378
+ return fnMatch(expr.args, ctx);
1379
+ case 'search':
1380
+ return fnSearch(expr.args, ctx);
1381
+ case 'value':
1382
+ return fnValue(expr.args, ctx);
1383
+ }
1384
+ }
1385
+ // RFC 9535 §2.4.4: length() function
1386
+ function fnLength(args, ctx) {
1387
+ if (args.length !== 1)
1388
+ return null;
1389
+ const value = evaluateFunctionArg(args[0], ctx);
1390
+ if (typeof value === 'string') {
1391
+ return value.length;
1392
+ }
1393
+ if (Array.isArray(value)) {
1394
+ return value.length;
1395
+ }
1396
+ if (value !== null && typeof value === 'object') {
1397
+ return Object.keys(value).length;
1398
+ }
1399
+ return null;
1400
+ }
1401
+ // RFC 9535 §2.4.5: count() function
1402
+ function fnCount(args, ctx) {
1403
+ if (args.length !== 1)
1404
+ return 0;
1405
+ const arg = args[0];
1406
+ if (arg.type === 'query') {
1407
+ const nodes = evaluateFilterQuery(arg, ctx);
1408
+ return nodes.length;
1409
+ }
1410
+ return 0;
1411
+ }
1412
+ // RFC 9535 §2.4.6: match() function - full regex match
1413
+ function fnMatch(args, ctx) {
1414
+ if (args.length !== 2)
1415
+ return false;
1416
+ const value = evaluateFunctionArg(args[0], ctx);
1417
+ const pattern = evaluateFunctionArg(args[1], ctx);
1418
+ if (typeof value !== 'string' || typeof pattern !== 'string') {
1419
+ return false;
1420
+ }
1421
+ try {
1422
+ // RFC 9535 §2.4.6: Full match (anchored)
1423
+ const re = new RegExp(`^(?:${pattern})$`, 'u');
1424
+ return re.test(value);
1425
+ }
1426
+ catch {
1427
+ return false;
1428
+ }
1429
+ }
1430
+ // RFC 9535 §2.4.7: search() function - partial regex match
1431
+ function fnSearch(args, ctx) {
1432
+ if (args.length !== 2)
1433
+ return false;
1434
+ const value = evaluateFunctionArg(args[0], ctx);
1435
+ const pattern = evaluateFunctionArg(args[1], ctx);
1436
+ if (typeof value !== 'string' || typeof pattern !== 'string') {
1437
+ return false;
1438
+ }
1439
+ try {
1440
+ const re = new RegExp(pattern, 'u');
1441
+ return re.test(value);
1442
+ }
1443
+ catch {
1444
+ return false;
1445
+ }
1446
+ }
1447
+ // RFC 9535 §2.4.8: value() function
1448
+ function fnValue(args, ctx) {
1449
+ if (args.length !== 1)
1450
+ return null;
1451
+ const arg = args[0];
1452
+ if (arg.type === 'query') {
1453
+ const nodes = evaluateFilterQuery(arg, ctx);
1454
+ return nodes.length === 1 ? nodes[0].value : null;
1455
+ }
1456
+ return null;
1457
+ }
1458
+ function evaluateFunctionArg(arg, ctx) {
1459
+ switch (arg.type) {
1460
+ case 'literal':
1461
+ return arg.value;
1462
+ case 'query':
1463
+ // For value-type arguments, get the singular value
1464
+ const nodes = evaluateFilterQuery(arg, ctx);
1465
+ return nodes.length === 1 ? nodes[0].value : undefined;
1466
+ case 'function':
1467
+ return evaluateFunction(arg, ctx);
1468
+ }
1469
+ }
1470
+ //# sourceMappingURL=jsonpath.js.map