@jarenjs/json 0.9.2

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 (57) hide show
  1. package/ARCHITECTURE.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +471 -0
  4. package/dist/types/basic.d.ts +32 -0
  5. package/dist/types/index.d.ts +4 -0
  6. package/dist/types/jslt/dispatch.d.ts +11 -0
  7. package/dist/types/jslt/errors.d.ts +18 -0
  8. package/dist/types/jslt/index.d.ts +53 -0
  9. package/dist/types/jslt/stylesheet.d.ts +8 -0
  10. package/dist/types/jtlt/desugar.d.ts +19 -0
  11. package/dist/types/jtlt/errors.d.ts +18 -0
  12. package/dist/types/jtlt/index.d.ts +57 -0
  13. package/dist/types/jtlt/template.d.ts +8 -0
  14. package/dist/types/jtlt/writer.d.ts +6 -0
  15. package/dist/types/path.d.ts +235 -0
  16. package/dist/types/pointer.d.ts +114 -0
  17. package/dist/types/query/compile.d.ts +21 -0
  18. package/dist/types/query/errors.d.ts +18 -0
  19. package/dist/types/query/index.d.ts +70 -0
  20. package/dist/types/query/normalize.d.ts +68 -0
  21. package/dist/types/query/operators.d.ts +424 -0
  22. package/dist/types/query/runtime.d.ts +93 -0
  23. package/dist/types/segments.d.ts +62 -0
  24. package/dist/types/xquery/index.d.ts +19 -0
  25. package/dist/types/xquery/parse.d.ts +20 -0
  26. package/docs/JSLT-FORMAT.md +861 -0
  27. package/docs/JSLT-PRELUDE.md +159 -0
  28. package/docs/JTLT-FORMAT.md +659 -0
  29. package/docs/QUERY-FORMAT.md +1221 -0
  30. package/docs/XQUERY-FRONTEND.md +321 -0
  31. package/package.json +81 -0
  32. package/schemas/jaren-jslt.draft-07.schema.json +776 -0
  33. package/schemas/jaren-jslt.schema.json +776 -0
  34. package/schemas/jaren-query.draft-07.schema.json +613 -0
  35. package/schemas/jaren-query.schema.json +375 -0
  36. package/src/basic.js +300 -0
  37. package/src/index.js +4 -0
  38. package/src/jslt/dispatch.js +934 -0
  39. package/src/jslt/errors.js +34 -0
  40. package/src/jslt/index.js +121 -0
  41. package/src/jslt/stylesheet.js +234 -0
  42. package/src/jtlt/desugar.js +231 -0
  43. package/src/jtlt/errors.js +34 -0
  44. package/src/jtlt/index.js +155 -0
  45. package/src/jtlt/template.js +130 -0
  46. package/src/jtlt/writer.js +110 -0
  47. package/src/path.js +977 -0
  48. package/src/pointer.js +453 -0
  49. package/src/query/compile.js +817 -0
  50. package/src/query/errors.js +33 -0
  51. package/src/query/index.js +150 -0
  52. package/src/query/normalize.js +1047 -0
  53. package/src/query/operators.js +1253 -0
  54. package/src/query/runtime.js +233 -0
  55. package/src/segments.js +627 -0
  56. package/src/xquery/index.js +35 -0
  57. package/src/xquery/parse.js +1647 -0
package/src/path.js ADDED
@@ -0,0 +1,977 @@
1
+ //#region JSONPath (RFC 9535)
2
+ // JSONPath: Query Expressions for JSON
3
+ // https://www.rfc-editor.org/rfc/rfc9535.html
4
+ //
5
+ // This module implements RFC 9535 as a two-stage compiler:
6
+ //
7
+ // 1. `parseJSONPath` - a strict, single-pass recursive-descent parser that
8
+ // turns a query string into an AST, enforcing the complete RFC grammar
9
+ // including the well-typedness rules of function expressions (2.4.3).
10
+ // 2. `compileJSONPath` - compiles the AST into a chain of specialized
11
+ // closures. All decisions (selector kind, sign of an index, literal
12
+ // regexps, singular-query detection) are taken at compile time so the
13
+ // returned query function does no interpretation at runtime.
14
+ //
15
+ // Runtime fast paths:
16
+ // - Singular queries (`$.a.b[3]`) compile to a direct property walk with
17
+ // no intermediate arrays.
18
+ // - Filter comparables compile to sentinel-returning getters; existence
19
+ // tests on singular queries never materialize nodelists.
20
+ // - `match()`/`search()` with a literal pattern precompile their RegExp;
21
+ // dynamic patterns use a per-callsite monomorphic cache.
22
+ // - Normalized-path production (RFC 9535 section 2.7) is compiled lazily,
23
+ // so value-only queries never pay for path-string building.
24
+
25
+ //#region constants & shared helpers
26
+
27
+ import {
28
+ NOTHING,
29
+ isSingularSegments,
30
+ compileSingularGetter,
31
+ compileSegmentV,
32
+ runSegmentsV,
33
+ compileSegmentP,
34
+ runSegmentsP,
35
+ } from './segments.js';
36
+ import {
37
+ CC_TAB,
38
+ CC_LF,
39
+ CC_CR,
40
+ CC_SPACE,
41
+ CC_BANG,
42
+ CC_DQUOTE,
43
+ CC_DOLLAR,
44
+ CC_AMP,
45
+ CC_SQUOTE,
46
+ CC_LPAREN,
47
+ CC_RPAREN,
48
+ CC_STAR,
49
+ CC_COMMA,
50
+ CC_MINUS,
51
+ CC_DOT,
52
+ CC_SLASH,
53
+ CC_0,
54
+ CC_9,
55
+ CC_COLON,
56
+ CC_LT,
57
+ CC_EQ,
58
+ CC_GT,
59
+ CC_QUESTION,
60
+ CC_AT,
61
+ CC_LBRACKET,
62
+ CC_BACKSLASH,
63
+ CC_RBRACKET,
64
+ CC_UNDERSCORE,
65
+ CC_PIPE,
66
+ isDigitCode,
67
+ } from '@jarenjs/core/scan';
68
+
69
+ /**
70
+ * Sentinel for the absence of a value ("Nothing" in RFC 9535 terms), as
71
+ * distinct from the JSON value `null`.
72
+ */
73
+ export const JSONPATH_NOTHING = NOTHING;
74
+
75
+ function deepFreeze(value) {
76
+ if (typeof value !== 'object' || value === null)
77
+ return value;
78
+ const keys = Object.keys(value);
79
+ for (let i = 0; i < keys.length; i++)
80
+ deepFreeze(value[keys[i]]);
81
+ return Object.freeze(value);
82
+ }
83
+
84
+ /**
85
+ * Error thrown when a JSONPath query is not valid RFC 9535 syntax
86
+ * (including queries that are not well-typed per section 2.4.3).
87
+ */
88
+ export class JSONPathSyntaxError extends SyntaxError {
89
+ constructor(message, source, position) {
90
+ super(`Invalid JSONPath: ${message} at position ${position} in '${source}'`);
91
+ this.name = 'JSONPathSyntaxError';
92
+ this.source = source;
93
+ this.position = position;
94
+ }
95
+ }
96
+
97
+ // Built-in function extensions (RFC 9535 section 2.4).
98
+ // Parameter/return types: 'value' = ValueType, 'nodes' = NodesType,
99
+ // 'logical' = LogicalType.
100
+ const FUNCTIONS = {
101
+ length: { params: ['value'], returns: 'value' },
102
+ count: { params: ['nodes'], returns: 'value' },
103
+ match: { params: ['value', 'value'], returns: 'logical' },
104
+ search: { params: ['value', 'value'], returns: 'logical' },
105
+ value: { params: ['nodes'], returns: 'value' },
106
+ };
107
+
108
+ //#endregion
109
+
110
+ //#region parser
111
+
112
+ function isNameFirstCode(c) {
113
+ return (c >= 0x41 && c <= 0x5A) // A-Z
114
+ || (c >= 0x61 && c <= 0x7A) // a-z
115
+ || c === CC_UNDERSCORE
116
+ || c >= 0x80; // any non-ASCII code unit
117
+ }
118
+
119
+ function isNameCharCode(c) {
120
+ return isNameFirstCode(c) || isDigitCode(c);
121
+ }
122
+
123
+ /**
124
+ * Selects a named object member (RFC 9535 name selector).
125
+ * @typedef {Object} JSONPathNameSelector
126
+ * @property {'name'} kind - Discriminator
127
+ * @property {string} name - The member name to select
128
+ */
129
+
130
+ /**
131
+ * Selects all members of an object / all elements of an array
132
+ * (RFC 9535 wildcard selector).
133
+ * @typedef {Object} JSONPathWildcardSelector
134
+ * @property {'wildcard'} kind - Discriminator
135
+ */
136
+
137
+ /**
138
+ * Selects an array element by index; negative indexes count from the end
139
+ * (RFC 9535 index selector).
140
+ * @typedef {Object} JSONPathIndexSelector
141
+ * @property {'index'} kind - Discriminator
142
+ * @property {number} index - The array index to select
143
+ */
144
+
145
+ /**
146
+ * Selects a range of array elements (RFC 9535 array slice selector).
147
+ * `null` means the bound was omitted in the query.
148
+ * @typedef {Object} JSONPathSliceSelector
149
+ * @property {'slice'} kind - Discriminator
150
+ * @property {number | null} start - Slice start, or null when omitted
151
+ * @property {number | null} end - Slice end (exclusive), or null when omitted
152
+ * @property {number | null} step - Slice step, or null when omitted
153
+ */
154
+
155
+ /**
156
+ * Selects children for which a filter expression yields a truthy result
157
+ * (RFC 9535 filter selector).
158
+ * @typedef {Object} JSONPathFilterSelector
159
+ * @property {'filter'} kind - Discriminator
160
+ * @property {object} expr - The parsed filter expression tree
161
+ */
162
+
163
+ /**
164
+ * Any RFC 9535 selector, discriminated by its `kind` property.
165
+ * @typedef {JSONPathNameSelector | JSONPathWildcardSelector | JSONPathIndexSelector | JSONPathSliceSelector | JSONPathFilterSelector} JSONPathSelector
166
+ */
167
+
168
+ /**
169
+ * One segment of a JSONPath query: a child (`.` / `[...]`) or descendant
170
+ * (`..`) step holding one or more selectors.
171
+ * @typedef {Object} JSONPathSegment
172
+ * @property {boolean} descendant - True for a descendant (`..`) segment
173
+ * @property {JSONPathSelector[]} selectors - The segment's selectors
174
+ */
175
+
176
+ /**
177
+ * The parsed AST of a JSONPath query.
178
+ * @typedef {Object} JSONPathAst
179
+ * @property {boolean} relative - True for a relative query (`@`), false for a root query (`$`)
180
+ * @property {JSONPathSegment[]} segments - The query's segments in order
181
+ */
182
+
183
+ /**
184
+ * One result of a JSONPath query in nodes mode: the matched value together
185
+ * with its normalized path (RFC 9535 section 2.7).
186
+ * @typedef {Object} JSONPathNode
187
+ * @property {string} path - The normalized path (e.g. `$['store']['book'][0]`)
188
+ * @property {any} value - The matched value
189
+ */
190
+
191
+ /**
192
+ * A compiled JSONPath query. Calling it returns the matched values; the
193
+ * attached methods expose the other result modes, and `source`/`ast`
194
+ * expose the original query string and its parsed (deeply frozen) AST.
195
+ * @typedef {((data: any) => any[]) & {
196
+ * values: (data: any) => any[],
197
+ * first: (data: any) => any,
198
+ * exists: (data: any) => boolean,
199
+ * nodes: (data: any) => JSONPathNode[],
200
+ * paths: (data: any) => string[],
201
+ * source: string,
202
+ * ast: JSONPathAst,
203
+ * }} JSONPathQuery
204
+ */
205
+
206
+ /**
207
+ * Parse a JSONPath query string into an AST.
208
+ * @param {string} source - The JSONPath expression (e.g. `$.store.book[?@.price < 10].title`)
209
+ * @returns {JSONPathAst} The parsed query AST
210
+ * @throws {JSONPathSyntaxError} When the query violates the RFC 9535 grammar
211
+ */
212
+ export function parseJSONPath(source) {
213
+ if (typeof source !== 'string')
214
+ throw new JSONPathSyntaxError('query must be a string', String(source), 0);
215
+
216
+ const len = source.length;
217
+ let pos = 0;
218
+
219
+ function fail(message, at = pos) {
220
+ throw new JSONPathSyntaxError(message, source, at);
221
+ }
222
+
223
+ function cc(at) {
224
+ return at < len ? source.charCodeAt(at) : -1;
225
+ }
226
+
227
+ function skipWS() {
228
+ while (pos < len) {
229
+ const c = source.charCodeAt(pos);
230
+ if (c !== CC_SPACE && c !== CC_TAB && c !== CC_LF && c !== CC_CR)
231
+ break;
232
+ pos++;
233
+ }
234
+ }
235
+
236
+ //#region scalar tokens
237
+
238
+ function parseIdentifier() {
239
+ const start = pos;
240
+ let c = cc(pos);
241
+ while ((c >= 0x61 && c <= 0x7A) || c === CC_UNDERSCORE || isDigitCode(c)) {
242
+ pos++;
243
+ c = cc(pos);
244
+ }
245
+ return source.slice(start, pos);
246
+ }
247
+
248
+ function parseMemberNameShorthand() {
249
+ const start = pos;
250
+ if (!isNameFirstCode(cc(pos)))
251
+ fail('expected member name');
252
+ while (pos < len) {
253
+ const c = source.charCodeAt(pos);
254
+ if (c >= 0xD800 && c <= 0xDFFF) {
255
+ // queries are sequences of Unicode scalar values (RFC 9535 2.1);
256
+ // raw surrogates must form a well-formed pair
257
+ const d = cc(pos + 1);
258
+ if (c >= 0xDC00 || d < 0xDC00 || d > 0xDFFF)
259
+ fail('lone surrogate in member name');
260
+ pos += 2;
261
+ continue;
262
+ }
263
+ if (!isNameCharCode(c))
264
+ break;
265
+ pos++;
266
+ }
267
+ return source.slice(start, pos);
268
+ }
269
+
270
+ function hex4() {
271
+ let value = 0;
272
+ for (let i = 0; i < 4; i++) {
273
+ const c = cc(pos);
274
+ let d;
275
+ if (c >= CC_0 && c <= CC_9) d = c - CC_0;
276
+ else if (c >= 0x41 && c <= 0x46) d = c - 0x37; // A-F
277
+ else if (c >= 0x61 && c <= 0x66) d = c - 0x57; // a-f
278
+ else return fail('invalid unicode escape');
279
+ value = (value << 4) | d;
280
+ pos++;
281
+ }
282
+ return value;
283
+ }
284
+
285
+ function parseUnicodeEscape() {
286
+ const hi = hex4();
287
+ if (hi >= 0xD800 && hi <= 0xDBFF) {
288
+ // high surrogate must be directly followed by a low surrogate escape
289
+ if (cc(pos) !== CC_BACKSLASH || cc(pos + 1) !== 0x75 /* u */)
290
+ fail('lone surrogate in unicode escape');
291
+ pos += 2;
292
+ const lo = hex4();
293
+ if (lo < 0xDC00 || lo > 0xDFFF)
294
+ fail('invalid low surrogate in unicode escape');
295
+ return String.fromCharCode(hi, lo);
296
+ }
297
+ if (hi >= 0xDC00 && hi <= 0xDFFF)
298
+ fail('lone surrogate in unicode escape');
299
+ return String.fromCharCode(hi);
300
+ }
301
+
302
+ function parseEscape(quote) {
303
+ const c = cc(pos);
304
+ pos++;
305
+ switch (c) {
306
+ case 0x62: return '\b'; // \b
307
+ case 0x66: return '\f'; // \f
308
+ case 0x6E: return '\n'; // \n
309
+ case 0x72: return '\r'; // \r
310
+ case 0x74: return '\t'; // \t
311
+ case CC_SLASH: return '/';
312
+ case CC_BACKSLASH: return '\\';
313
+ case 0x75: return parseUnicodeEscape(); // \uXXXX
314
+ default:
315
+ if (c === quote)
316
+ return String.fromCharCode(quote);
317
+ return fail('invalid escape sequence', pos - 1);
318
+ }
319
+ }
320
+
321
+ function parseStringLiteral() {
322
+ const quote = source.charCodeAt(pos);
323
+ pos++;
324
+ let out = '';
325
+ let chunk = pos;
326
+ while (pos < len) {
327
+ const c = source.charCodeAt(pos);
328
+ if (c === quote) {
329
+ out += source.slice(chunk, pos);
330
+ pos++;
331
+ return out;
332
+ }
333
+ if (c === CC_BACKSLASH) {
334
+ out += source.slice(chunk, pos);
335
+ pos++;
336
+ out += parseEscape(quote);
337
+ chunk = pos;
338
+ continue;
339
+ }
340
+ if (c < CC_SPACE)
341
+ fail('unescaped control character in string literal');
342
+ if (c >= 0xD800 && c <= 0xDFFF) {
343
+ // raw surrogates must form a well-formed pair (RFC 9535 2.1)
344
+ const d = cc(pos + 1);
345
+ if (c >= 0xDC00 || d < 0xDC00 || d > 0xDFFF)
346
+ fail('lone surrogate in string literal');
347
+ pos += 2;
348
+ continue;
349
+ }
350
+ pos++;
351
+ }
352
+ return fail('unterminated string literal');
353
+ }
354
+
355
+ // int per RFC: "0" or ["-"] 1-9 *DIGIT; no leading zeros, no -0
356
+ function parseIntToken() {
357
+ const start = pos;
358
+ let neg = false;
359
+ if (cc(pos) === CC_MINUS) {
360
+ neg = true;
361
+ pos++;
362
+ }
363
+ const first = cc(pos);
364
+ if (!isDigitCode(first))
365
+ fail('expected integer');
366
+ pos++;
367
+ if (first === CC_0) {
368
+ if (isDigitCode(cc(pos)))
369
+ fail('leading zeros are not allowed', start);
370
+ if (neg)
371
+ fail("'-0' is not allowed as an index", start);
372
+ return 0;
373
+ }
374
+ while (isDigitCode(cc(pos)))
375
+ pos++;
376
+ const value = Number(source.slice(start, pos));
377
+ if (!Number.isSafeInteger(value))
378
+ fail('integer out of interoperable range', start);
379
+ return value;
380
+ }
381
+
382
+ // number per RFC: (int / "-0") [frac] [exp]
383
+ function parseNumberLiteral() {
384
+ const start = pos;
385
+ if (cc(pos) === CC_MINUS)
386
+ pos++;
387
+ if (!isDigitCode(cc(pos)))
388
+ fail('expected digit in number literal');
389
+ if (cc(pos) === CC_0) {
390
+ pos++;
391
+ if (isDigitCode(cc(pos)))
392
+ fail('leading zeros are not allowed', start);
393
+ }
394
+ else {
395
+ while (isDigitCode(cc(pos)))
396
+ pos++;
397
+ }
398
+ if (cc(pos) === CC_DOT) {
399
+ pos++;
400
+ if (!isDigitCode(cc(pos)))
401
+ fail('expected digit after decimal point');
402
+ while (isDigitCode(cc(pos)))
403
+ pos++;
404
+ }
405
+ const e = cc(pos);
406
+ if (e === 0x65 || e === 0x45) { // e | E
407
+ pos++;
408
+ const s = cc(pos);
409
+ if (s === 0x2B || s === CC_MINUS)
410
+ pos++;
411
+ if (!isDigitCode(cc(pos)))
412
+ fail('expected digit in exponent');
413
+ while (isDigitCode(cc(pos)))
414
+ pos++;
415
+ }
416
+ return Number(source.slice(start, pos));
417
+ }
418
+
419
+ //#endregion
420
+
421
+ //#region segments & selectors
422
+
423
+ function parseSegments() {
424
+ const segments = [];
425
+ for (;;) {
426
+ const save = pos;
427
+ skipWS();
428
+ const c = cc(pos);
429
+ if (c === CC_LBRACKET) {
430
+ segments.push(parseBracketed(false));
431
+ }
432
+ else if (c === CC_DOT) {
433
+ if (cc(pos + 1) === CC_DOT) {
434
+ pos += 2;
435
+ segments.push(parseDescendant());
436
+ }
437
+ else {
438
+ pos += 1;
439
+ segments.push(parseChildShorthand());
440
+ }
441
+ }
442
+ else {
443
+ pos = save;
444
+ return segments;
445
+ }
446
+ }
447
+ }
448
+
449
+ function parseChildShorthand() {
450
+ // no whitespace allowed after '.'
451
+ if (cc(pos) === CC_STAR) {
452
+ pos++;
453
+ return { descendant: false, selectors: [{ kind: 'wildcard' }] };
454
+ }
455
+ return { descendant: false, selectors: [{ kind: 'name', name: parseMemberNameShorthand() }] };
456
+ }
457
+
458
+ function parseDescendant() {
459
+ // no whitespace allowed after '..'
460
+ const c = cc(pos);
461
+ if (c === CC_LBRACKET)
462
+ return parseBracketed(true);
463
+ if (c === CC_STAR) {
464
+ pos++;
465
+ return { descendant: true, selectors: [{ kind: 'wildcard' }] };
466
+ }
467
+ return { descendant: true, selectors: [{ kind: 'name', name: parseMemberNameShorthand() }] };
468
+ }
469
+
470
+ function parseBracketed(descendant) {
471
+ pos++; // consume '['
472
+ skipWS();
473
+ const selectors = [];
474
+ for (;;) {
475
+ selectors.push(parseSelector());
476
+ skipWS();
477
+ const c = cc(pos);
478
+ if (c === CC_COMMA) {
479
+ pos++;
480
+ skipWS();
481
+ continue;
482
+ }
483
+ if (c === CC_RBRACKET) {
484
+ pos++;
485
+ return { descendant, selectors };
486
+ }
487
+ fail("expected ',' or ']'");
488
+ }
489
+ }
490
+
491
+ function parseSelector() {
492
+ const c = cc(pos);
493
+ if (c === CC_SQUOTE || c === CC_DQUOTE)
494
+ return { kind: 'name', name: parseStringLiteral() };
495
+ if (c === CC_STAR) {
496
+ pos++;
497
+ return { kind: 'wildcard' };
498
+ }
499
+ if (c === CC_QUESTION) {
500
+ pos++;
501
+ skipWS();
502
+ return { kind: 'filter', expr: parseLogicalOr() };
503
+ }
504
+ if (c === CC_COLON || c === CC_MINUS || isDigitCode(c))
505
+ return parseIndexOrSlice();
506
+ return fail('expected a selector');
507
+ }
508
+
509
+ function parseIndexOrSlice() {
510
+ let start = null;
511
+ if (cc(pos) !== CC_COLON)
512
+ start = parseIntToken();
513
+ let save = pos;
514
+ skipWS();
515
+ if (cc(pos) !== CC_COLON) {
516
+ pos = save;
517
+ return { kind: 'index', index: start };
518
+ }
519
+ pos++; // consume ':'
520
+ skipWS();
521
+ let end = null;
522
+ let c = cc(pos);
523
+ if (c === CC_MINUS || isDigitCode(c))
524
+ end = parseIntToken();
525
+ save = pos;
526
+ skipWS();
527
+ let step = null;
528
+ if (cc(pos) === CC_COLON) {
529
+ pos++;
530
+ skipWS();
531
+ c = cc(pos);
532
+ if (c === CC_MINUS || isDigitCode(c))
533
+ step = parseIntToken();
534
+ // no step after ':' is fine; trailing WS belongs to the bracket
535
+ }
536
+ else {
537
+ pos = save;
538
+ }
539
+ return { kind: 'slice', start, end, step };
540
+ }
541
+
542
+ //#endregion
543
+
544
+ //#region filter expressions
545
+
546
+ function parseLogicalOr() {
547
+ let expr = parseLogicalAnd();
548
+ for (;;) {
549
+ const save = pos;
550
+ skipWS();
551
+ if (cc(pos) === CC_PIPE && cc(pos + 1) === CC_PIPE) {
552
+ pos += 2;
553
+ skipWS();
554
+ const right = parseLogicalAnd();
555
+ expr = expr.kind === 'or'
556
+ ? { kind: 'or', operands: [...expr.operands, right] }
557
+ : { kind: 'or', operands: [expr, right] };
558
+ }
559
+ else {
560
+ pos = save;
561
+ return expr;
562
+ }
563
+ }
564
+ }
565
+
566
+ function parseLogicalAnd() {
567
+ let expr = parseBasicExpr();
568
+ for (;;) {
569
+ const save = pos;
570
+ skipWS();
571
+ if (cc(pos) === CC_AMP && cc(pos + 1) === CC_AMP) {
572
+ pos += 2;
573
+ skipWS();
574
+ const right = parseBasicExpr();
575
+ expr = expr.kind === 'and'
576
+ ? { kind: 'and', operands: [...expr.operands, right] }
577
+ : { kind: 'and', operands: [expr, right] };
578
+ }
579
+ else {
580
+ pos = save;
581
+ return expr;
582
+ }
583
+ }
584
+ }
585
+
586
+ function parseBasicExpr() {
587
+ const c = cc(pos);
588
+ if (c === CC_BANG) {
589
+ pos++;
590
+ skipWS();
591
+ return { kind: 'not', operand: parseNegatable() };
592
+ }
593
+ if (c === CC_LPAREN)
594
+ return parseParen();
595
+ return parseComparisonOrTest();
596
+ }
597
+
598
+ function parseNegatable() {
599
+ const c = cc(pos);
600
+ if (c === CC_LPAREN)
601
+ return parseParen();
602
+ if (c === CC_AT || c === CC_DOLLAR)
603
+ return { kind: 'exists', query: parseFilterQuery() };
604
+ if (c >= 0x61 && c <= 0x7A) {
605
+ const at = pos;
606
+ const ident = parseIdentifier();
607
+ if (cc(pos) !== CC_LPAREN)
608
+ fail('expected a test expression after !', at);
609
+ const func = parseFunctionExpr(ident, at);
610
+ if (func.returns === 'value')
611
+ fail('a function of type ValueType cannot be used as a test expression', at);
612
+ return { kind: 'ftest', func };
613
+ }
614
+ return fail('expected a test expression after !');
615
+ }
616
+
617
+ function parseParen() {
618
+ pos++; // consume '('
619
+ skipWS();
620
+ const expr = parseLogicalOr();
621
+ skipWS();
622
+ if (cc(pos) !== CC_RPAREN)
623
+ fail("expected ')'");
624
+ pos++;
625
+ return expr;
626
+ }
627
+
628
+ function tryParseCompOp() {
629
+ const c = cc(pos);
630
+ if (c === CC_EQ) {
631
+ if (cc(pos + 1) !== CC_EQ)
632
+ fail("expected '=='");
633
+ pos += 2;
634
+ return '==';
635
+ }
636
+ if (c === CC_BANG) {
637
+ if (cc(pos + 1) !== CC_EQ)
638
+ return null;
639
+ pos += 2;
640
+ return '!=';
641
+ }
642
+ if (c === CC_LT) {
643
+ if (cc(pos + 1) === CC_EQ) {
644
+ pos += 2;
645
+ return '<=';
646
+ }
647
+ pos += 1;
648
+ return '<';
649
+ }
650
+ if (c === CC_GT) {
651
+ if (cc(pos + 1) === CC_EQ) {
652
+ pos += 2;
653
+ return '>=';
654
+ }
655
+ pos += 1;
656
+ return '>';
657
+ }
658
+ return null;
659
+ }
660
+
661
+ function parseFilterQuery() {
662
+ const relative = cc(pos) === CC_AT;
663
+ pos++; // consume '@' or '$'
664
+ return { relative, segments: parseSegments() };
665
+ }
666
+
667
+ function requireSingular(query, at, what) {
668
+ if (!isSingularSegments(query.segments))
669
+ fail(`${what} requires a singular query`, at);
670
+ return query;
671
+ }
672
+
673
+ function parseComparisonOrTest() {
674
+ const at = pos;
675
+ const c = cc(pos);
676
+ if (c === CC_AT || c === CC_DOLLAR) {
677
+ const query = parseFilterQuery();
678
+ const save = pos;
679
+ skipWS();
680
+ const op = tryParseCompOp();
681
+ if (op === null) {
682
+ pos = save;
683
+ return { kind: 'exists', query };
684
+ }
685
+ requireSingular(query, at, 'a comparison');
686
+ skipWS();
687
+ return { kind: 'cmp', op, left: { kind: 'query', query }, right: parseComparable() };
688
+ }
689
+ if (c === CC_SQUOTE || c === CC_DQUOTE || c === CC_MINUS || isDigitCode(c)) {
690
+ const left = parseLiteralComparable();
691
+ skipWS();
692
+ const op = tryParseCompOp();
693
+ if (op === null)
694
+ fail('a literal must be part of a comparison', at);
695
+ skipWS();
696
+ return { kind: 'cmp', op, left, right: parseComparable() };
697
+ }
698
+ if (c >= 0x61 && c <= 0x7A) {
699
+ const ident = parseIdentifier();
700
+ if (cc(pos) === CC_LPAREN) {
701
+ const func = parseFunctionExpr(ident, at);
702
+ const save = pos;
703
+ skipWS();
704
+ const op = tryParseCompOp();
705
+ if (op === null) {
706
+ pos = save;
707
+ if (func.returns === 'value')
708
+ fail('a function of type ValueType cannot be used as a test expression', at);
709
+ return { kind: 'ftest', func };
710
+ }
711
+ if (func.returns !== 'value')
712
+ fail('only functions of type ValueType can be compared', at);
713
+ skipWS();
714
+ return { kind: 'cmp', op, left: func, right: parseComparable() };
715
+ }
716
+ if (ident === 'true' || ident === 'false' || ident === 'null') {
717
+ skipWS();
718
+ const op = tryParseCompOp();
719
+ if (op === null)
720
+ fail('a literal must be part of a comparison', at);
721
+ skipWS();
722
+ const value = ident === 'true' ? true : ident === 'false' ? false : null;
723
+ return { kind: 'cmp', op, left: { kind: 'literal', value }, right: parseComparable() };
724
+ }
725
+ return fail(`unexpected identifier '${ident}'`, at);
726
+ }
727
+ return fail('expected a filter expression');
728
+ }
729
+
730
+ function parseLiteralComparable() {
731
+ const c = cc(pos);
732
+ if (c === CC_SQUOTE || c === CC_DQUOTE)
733
+ return { kind: 'literal', value: parseStringLiteral() };
734
+ return { kind: 'literal', value: parseNumberLiteral() };
735
+ }
736
+
737
+ function parseComparable() {
738
+ const at = pos;
739
+ const c = cc(pos);
740
+ if (c === CC_AT || c === CC_DOLLAR) {
741
+ const query = requireSingular(parseFilterQuery(), at, 'a comparison');
742
+ return { kind: 'query', query };
743
+ }
744
+ if (c === CC_SQUOTE || c === CC_DQUOTE || c === CC_MINUS || isDigitCode(c))
745
+ return parseLiteralComparable();
746
+ if (c >= 0x61 && c <= 0x7A) {
747
+ const ident = parseIdentifier();
748
+ if (cc(pos) === CC_LPAREN) {
749
+ const func = parseFunctionExpr(ident, at);
750
+ if (func.returns !== 'value')
751
+ fail('only functions of type ValueType can be compared', at);
752
+ return func;
753
+ }
754
+ if (ident === 'true') return { kind: 'literal', value: true };
755
+ if (ident === 'false') return { kind: 'literal', value: false };
756
+ if (ident === 'null') return { kind: 'literal', value: null };
757
+ return fail(`unexpected identifier '${ident}'`, at);
758
+ }
759
+ return fail('expected a comparable expression');
760
+ }
761
+
762
+ function parseFunctionArg() {
763
+ const at = pos;
764
+ const c = cc(pos);
765
+ if (c === CC_AT || c === CC_DOLLAR)
766
+ return { kind: 'query', query: parseFilterQuery() };
767
+ if (c === CC_SQUOTE || c === CC_DQUOTE || c === CC_MINUS || isDigitCode(c))
768
+ return parseLiteralComparable();
769
+ if (c >= 0x61 && c <= 0x7A) {
770
+ const ident = parseIdentifier();
771
+ if (cc(pos) === CC_LPAREN)
772
+ return parseFunctionExpr(ident, at);
773
+ if (ident === 'true') return { kind: 'literal', value: true };
774
+ if (ident === 'false') return { kind: 'literal', value: false };
775
+ if (ident === 'null') return { kind: 'literal', value: null };
776
+ return fail(`unexpected identifier '${ident}'`, at);
777
+ }
778
+ return fail('expected a function argument');
779
+ }
780
+
781
+ function parseFunctionExpr(name, at) {
782
+ const def = FUNCTIONS[name];
783
+ if (def === undefined)
784
+ fail(`unknown function '${name}'`, at);
785
+ pos++; // consume '('
786
+ skipWS();
787
+ const args = [];
788
+ if (cc(pos) !== CC_RPAREN) {
789
+ for (;;) {
790
+ args.push(parseFunctionArg());
791
+ skipWS();
792
+ if (cc(pos) === CC_COMMA) {
793
+ pos++;
794
+ skipWS();
795
+ continue;
796
+ }
797
+ break;
798
+ }
799
+ }
800
+ if (cc(pos) !== CC_RPAREN)
801
+ fail("expected ')'");
802
+ pos++;
803
+ if (args.length !== def.params.length)
804
+ fail(`function '${name}' expects ${def.params.length} argument(s), got ${args.length}`, at);
805
+ for (let i = 0; i < args.length; i++) {
806
+ const param = def.params[i];
807
+ const arg = args[i];
808
+ if (param === 'value') {
809
+ if (arg.kind === 'literal')
810
+ continue;
811
+ if (arg.kind === 'query') {
812
+ requireSingular(arg.query, at, `argument ${i + 1} of '${name}'`);
813
+ continue;
814
+ }
815
+ if (arg.kind === 'func' && arg.returns === 'value')
816
+ continue;
817
+ fail(`argument ${i + 1} of '${name}' must be of type ValueType`, at);
818
+ }
819
+ else { // 'nodes'
820
+ if (arg.kind === 'query')
821
+ continue;
822
+ if (arg.kind === 'func' && arg.returns === 'nodes')
823
+ continue;
824
+ fail(`argument ${i + 1} of '${name}' must be a query (NodesType)`, at);
825
+ }
826
+ }
827
+ return { kind: 'func', name, args, returns: def.returns };
828
+ }
829
+
830
+ //#endregion
831
+
832
+ // jsonpath-query = root-identifier segments
833
+ if (len === 0)
834
+ fail('empty query', 0);
835
+ if (source.charCodeAt(0) !== CC_DOLLAR)
836
+ fail("query must start with '$'", 0);
837
+ pos = 1;
838
+ const segments = parseSegments();
839
+ if (pos !== len)
840
+ fail('unexpected token');
841
+ return { relative: false, segments };
842
+ }
843
+
844
+ //#endregion
845
+
846
+ // The nodes-mode segment compilers (normalized paths, RFC 9535 section
847
+ // 2.7) live in segments.js next to the values-mode ones; nodes mode is
848
+ // still compiled lazily here (see compileJSONPath), so value-only
849
+ // queries never pay for path-string building.
850
+
851
+ //#region public API
852
+
853
+ /**
854
+ * Compile a JSONPath query (RFC 9535) into a reusable query function.
855
+ *
856
+ * The returned function applies the query to a JSON value and returns
857
+ * the resulting nodelist as an array of values. It also carries helper
858
+ * methods:
859
+ *
860
+ * - `query(data)` / `query.values(data)` - array of matched values
861
+ * - `query.first(data)` - first matched value, or `undefined`
862
+ * - `query.exists(data)` - true when the query selects at least one node
863
+ * - `query.nodes(data)` - array of `{ path, value }` with normalized paths
864
+ * - `query.paths(data)` - array of normalized paths (RFC 9535 section 2.7)
865
+ * - `query.source` - the original query string
866
+ * - `query.ast` - the parsed query AST (deeply frozen; the lazily
867
+ * compiled path mode must agree with the eagerly compiled value mode)
868
+ *
869
+ * @param {string} source - The JSONPath expression
870
+ * @returns {JSONPathQuery} The compiled query function
871
+ * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
872
+ * @example
873
+ * const q = compileJSONPath('$.store.book[?@.price < 10].title');
874
+ * q(data); // ['Sayings of the Century', 'Moby Dick']
875
+ * q.paths(data); // ["$['store']['book'][0]['title']", ...]
876
+ */
877
+ export function compileJSONPath(source) {
878
+ const ast = deepFreeze(parseJSONPath(source));
879
+ const segments = ast.segments;
880
+
881
+ let values, first, exists;
882
+ if (isSingularSegments(segments)) {
883
+ const getter = compileSingularGetter(segments, false);
884
+ values = (data) => {
885
+ const v = getter(data, data);
886
+ return v === NOTHING ? [] : [v];
887
+ };
888
+ first = (data) => {
889
+ const v = getter(data, data);
890
+ return v === NOTHING ? undefined : v;
891
+ };
892
+ exists = (data) => getter(data, data) !== NOTHING;
893
+ }
894
+ else {
895
+ const segs = segments.map(compileSegmentV);
896
+ values = (data) => runSegmentsV(segs, data, data);
897
+ first = (data) => {
898
+ const result = runSegmentsV(segs, data, data);
899
+ return result.length !== 0 ? result[0] : undefined;
900
+ };
901
+ exists = (data) => runSegmentsV(segs, data, data).length !== 0;
902
+ }
903
+
904
+ // nodes mode is compiled lazily; value-only queries never pay for it
905
+ let segsP = null;
906
+ function runNodes(data) {
907
+ if (segsP === null)
908
+ segsP = segments.map(compileSegmentP);
909
+ return runSegmentsP(segsP, data, '$', data);
910
+ }
911
+
912
+ const query = (data) => values(data);
913
+ query.values = values;
914
+ query.first = first;
915
+ query.exists = exists;
916
+ query.nodes = (data) => {
917
+ const { vals, paths } = runNodes(data);
918
+ const nodes = new Array(vals.length);
919
+ for (let i = 0; i < vals.length; i++)
920
+ nodes[i] = { path: paths[i], value: vals[i] };
921
+ return nodes;
922
+ };
923
+ query.paths = (data) => runNodes(data).paths;
924
+ query.source = source;
925
+ query.ast = ast;
926
+ return query;
927
+ }
928
+
929
+ const QUERY_CACHE = new Map();
930
+ const QUERY_CACHE_LIMIT = 512;
931
+
932
+ /**
933
+ * Apply a JSONPath query to a JSON value in one call. Compiled queries
934
+ * are cached (FIFO, 512 entries), so repeated calls with the same query
935
+ * string reuse the compiled function.
936
+ * @param {string} source - The JSONPath expression
937
+ * @param {any} data - The JSON value to query
938
+ * @returns {any[]} Array of matched values
939
+ * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
940
+ */
941
+ export function queryJSONPath(source, data) {
942
+ let query = QUERY_CACHE.get(source);
943
+ if (query === undefined) {
944
+ query = compileJSONPath(source);
945
+ if (QUERY_CACHE.size >= QUERY_CACHE_LIMIT)
946
+ QUERY_CACHE.delete(QUERY_CACHE.keys().next().value);
947
+ QUERY_CACHE.set(source, query);
948
+ }
949
+ return query(data);
950
+ }
951
+
952
+ /**
953
+ * Validates a JSONPath expression strictly against the RFC 9535 grammar,
954
+ * including well-typedness of function expressions. Unlike the heuristic
955
+ * `isValidJSONPath` in basic.js, this uses the full parser.
956
+ * @param {string} str - The JSONPath expression to validate
957
+ * @returns {boolean} True when the string is a valid RFC 9535 query
958
+ * @example
959
+ * isValidJSONPathStrict('$.store.book[?@.price < 10]'); // true
960
+ * isValidJSONPathStrict('$.store.book[0 5]'); // false
961
+ * isValidJSONPathStrict('@.name'); // false (queries start at $)
962
+ */
963
+ export function isValidJSONPathStrict(str) {
964
+ if (typeof str !== 'string')
965
+ return false;
966
+ try {
967
+ parseJSONPath(str);
968
+ return true;
969
+ }
970
+ catch {
971
+ return false;
972
+ }
973
+ }
974
+
975
+ //#endregion
976
+
977
+ //#endregion