@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/pointer.js ADDED
@@ -0,0 +1,453 @@
1
+ //#region JSON Pointer (RFC 6901) + Relative JSON Pointer
2
+ // JSON Pointer: https://datatracker.ietf.org/doc/html/rfc6901
3
+ // Relative JSON Pointer:
4
+ // https://datatracker.ietf.org/doc/html/draft-luff-relative-json-pointer-00
5
+ //
6
+ // This module implements JSON Pointer as a two-stage compiler, mirroring
7
+ // the JSONPath engine in path.js:
8
+ //
9
+ // 1. `parseJSONPointer` / `parseRelativeJSONPointer` - strict, single-pass
10
+ // char-code parsers enforcing the full RFC 6901 / draft-luff grammar.
11
+ // 2. `compileJSONPointer` / `compileRelativeJSONPointer` /
12
+ // `compileDataRef` - compile the parsed form into specialized getter
13
+ // closures. All decisions (member name decoding, array index parsing,
14
+ // absolute-vs-relative dispatch) are taken at compile time; resolution
15
+ // allocates nothing and returns the shared `NOTHING` sentinel when the
16
+ // pointer does not address a location.
17
+
18
+ import {
19
+ CC_SLASH,
20
+ CC_HASH,
21
+ CC_TILDE,
22
+ CC_0,
23
+ CC_1,
24
+ isDigitCode,
25
+ } from '@jarenjs/core/scan';
26
+
27
+ import { NOTHING } from './segments.js';
28
+
29
+ /**
30
+ * Sentinel for the absence of a value, as distinct from the JSON value
31
+ * `null`. This is the same sentinel as `JSONPATH_NOTHING` in path.js, so
32
+ * pointer and path results can share checks.
33
+ */
34
+ export const JSONPOINTER_NOTHING = NOTHING;
35
+
36
+ const hasOwn = Object.hasOwn;
37
+
38
+ /**
39
+ * Error thrown when a (relative) JSON Pointer is not valid RFC 6901 /
40
+ * draft-luff-relative-json-pointer syntax.
41
+ */
42
+ export class JSONPointerSyntaxError extends SyntaxError {
43
+ constructor(message, source, position) {
44
+ super(`Invalid JSON Pointer: ${message} at position ${position} in '${source}'`);
45
+ this.name = 'JSONPointerSyntaxError';
46
+ this.source = source;
47
+ this.position = position;
48
+ }
49
+ }
50
+
51
+ //#region parsers
52
+
53
+ /**
54
+ * Scan the segments of a pointer left to right, decoding `~0`/`~1`.
55
+ * `pos` must sit on the first `/` of the pointer part (or at the end of
56
+ * the source for an empty pointer). A `~` not followed by `0` or `1` is
57
+ * a syntax error (RFC 6901 section 3).
58
+ * @param {string} source - The pointer source text
59
+ * @param {number} pos - Index of the first '/' of the pointer part
60
+ * @returns {string[]} The decoded segments
61
+ */
62
+ function scanSegments(source, pos) {
63
+ const len = source.length;
64
+ const segments = [];
65
+ while (pos < len) {
66
+ pos++; // consume '/'
67
+ const start = pos;
68
+ let decoded = null;
69
+ let chunk = start;
70
+ while (pos < len) {
71
+ const c = source.charCodeAt(pos);
72
+ if (c === CC_SLASH)
73
+ break;
74
+ if (c === CC_TILDE) {
75
+ const d = pos + 1 < len ? source.charCodeAt(pos + 1) : -1;
76
+ if (d === CC_0 || d === CC_1) {
77
+ decoded = (decoded === null ? '' : decoded)
78
+ + source.slice(chunk, pos)
79
+ + (d === CC_0 ? '~' : '/');
80
+ pos += 2;
81
+ chunk = pos;
82
+ continue;
83
+ }
84
+ throw new JSONPointerSyntaxError("expected '0' or '1' after '~'", source, pos);
85
+ }
86
+ pos++;
87
+ }
88
+ segments.push(decoded === null
89
+ ? source.slice(start, pos)
90
+ : decoded + source.slice(chunk, pos));
91
+ }
92
+ return segments;
93
+ }
94
+
95
+ /**
96
+ * Parse a JSON Pointer strictly per RFC 6901 into its decoded segments.
97
+ * @param {string} pointer - The JSON Pointer (e.g. `/store/book/0`)
98
+ * @returns {string[]} Array of decoded reference tokens
99
+ * @throws {JSONPointerSyntaxError} When the pointer violates the grammar
100
+ */
101
+ export function parseJSONPointer(pointer) {
102
+ if (typeof pointer !== 'string')
103
+ throw new JSONPointerSyntaxError('pointer must be a string', String(pointer), 0);
104
+ if (pointer.length === 0)
105
+ return [];
106
+ if (pointer.charCodeAt(0) !== CC_SLASH)
107
+ throw new JSONPointerSyntaxError("a non-empty pointer must start with '/'", pointer, 0);
108
+ return scanSegments(pointer, 0);
109
+ }
110
+
111
+ /**
112
+ * A parsed Relative JSON Pointer (draft-luff-relative-json-pointer).
113
+ * @typedef {Object} RelativeJsonPointer
114
+ * @property {number} levels - Number of levels to ascend from the current location
115
+ * @property {boolean} hash - True for the `#` form, which addresses the member name or array index itself
116
+ * @property {string[]} segments - Decoded reference tokens applied after ascending
117
+ */
118
+
119
+ /**
120
+ * A compiled JSON Pointer: returns the value addressed in `root`, or the
121
+ * `JSONPOINTER_NOTHING` sentinel when the pointer does not address a location.
122
+ * @typedef {(root: any) => any} JsonPointerGetter
123
+ */
124
+
125
+ /**
126
+ * A compiled Relative JSON Pointer / data reference: resolves against the
127
+ * RFC 6901 location `dataPath` inside `dataRoot`, returning the addressed
128
+ * value or the `JSONPOINTER_NOTHING` sentinel.
129
+ * @typedef {(dataRoot: any, dataPath: string) => any} RelativeJsonPointerResolver
130
+ */
131
+
132
+ /**
133
+ * Parse a Relative JSON Pointer strictly per
134
+ * draft-luff-relative-json-pointer: a non-negative integer without
135
+ * leading zeros, followed by `#` or a JSON Pointer.
136
+ * @param {string} pointer - The relative pointer (e.g. `1/sibling`, `0#`)
137
+ * @returns {RelativeJsonPointer} The parsed relative pointer
138
+ * @throws {JSONPointerSyntaxError} When the pointer violates the grammar
139
+ */
140
+ export function parseRelativeJSONPointer(pointer) {
141
+ if (typeof pointer !== 'string')
142
+ throw new JSONPointerSyntaxError('pointer must be a string', String(pointer), 0);
143
+ const len = pointer.length;
144
+ if (len === 0)
145
+ throw new JSONPointerSyntaxError('empty relative pointer', pointer, 0);
146
+ const first = pointer.charCodeAt(0);
147
+ if (!isDigitCode(first))
148
+ throw new JSONPointerSyntaxError('expected a non-negative integer', pointer, 0);
149
+ let pos = 1;
150
+ if (first === CC_0) {
151
+ if (pos < len && isDigitCode(pointer.charCodeAt(pos)))
152
+ throw new JSONPointerSyntaxError('leading zeros are not allowed', pointer, 0);
153
+ }
154
+ else {
155
+ while (pos < len && isDigitCode(pointer.charCodeAt(pos)))
156
+ pos++;
157
+ }
158
+ const levels = Number(pointer.slice(0, pos));
159
+ if (!Number.isSafeInteger(levels))
160
+ throw new JSONPointerSyntaxError('level count out of range', pointer, 0);
161
+ if (pos === len)
162
+ return { levels, hash: false, segments: [] };
163
+ const c = pointer.charCodeAt(pos);
164
+ if (c === CC_HASH) {
165
+ if (pos + 1 !== len)
166
+ throw new JSONPointerSyntaxError("'#' must end the pointer", pointer, pos + 1);
167
+ return { levels, hash: true, segments: [] };
168
+ }
169
+ if (c !== CC_SLASH)
170
+ throw new JSONPointerSyntaxError("expected '#' or a JSON Pointer after the level count", pointer, pos);
171
+ return { levels, hash: false, segments: scanSegments(pointer, pos) };
172
+ }
173
+
174
+ //#endregion
175
+
176
+ //#region compiler
177
+
178
+ // Array indexes are bounded by the maximum array length (2^32 - 1), so a
179
+ // valid index has at most 10 digits and is strictly below 2^32 - 1.
180
+ const MAX_ARRAY_INDEX = 4294967294;
181
+
182
+ /**
183
+ * Scan `source[start..end)` as an RFC 6901 array index: `0`, or a digit
184
+ * sequence without leading zeros. Returns -1 when the range is not a
185
+ * valid index (`-` is never a valid read index).
186
+ */
187
+ function scanArrayIndex(source, start, end) {
188
+ const digits = end - start;
189
+ if (digits === 0 || digits > 10)
190
+ return -1;
191
+ const first = source.charCodeAt(start);
192
+ if (!isDigitCode(first))
193
+ return -1;
194
+ if (first === CC_0)
195
+ return digits === 1 ? 0 : -1;
196
+ let index = first - CC_0;
197
+ for (let i = start + 1; i < end; i++) {
198
+ const c = source.charCodeAt(i);
199
+ if (!isDigitCode(c))
200
+ return -1;
201
+ index = index * 10 + (c - CC_0);
202
+ }
203
+ return index <= MAX_ARRAY_INDEX ? index : -1;
204
+ }
205
+
206
+ /**
207
+ * One pointer hop: an array is addressed by the pre-parsed index, an
208
+ * object by the pre-decoded member name (own properties only), anything
209
+ * else has no addressable children. RFC 6901 requires a segment like "2"
210
+ * to address both `{"2": x}` members and array element 2 - one segment,
211
+ * two pre-computed forms.
212
+ */
213
+ function hop(v, name, index) {
214
+ if (typeof v !== 'object' || v === null)
215
+ return NOTHING;
216
+ if (Array.isArray(v))
217
+ return (index >= 0 && index < v.length) ? v[index] : NOTHING;
218
+ return hasOwn(v, name) ? v[name] : NOTHING;
219
+ }
220
+
221
+ function getRoot(root) {
222
+ return root;
223
+ }
224
+
225
+ /**
226
+ * Compile decoded segments into a specialized getter, unrolled by
227
+ * segment count like the validator's composition table.
228
+ * @param {string[]} segments - Decoded reference tokens
229
+ * @returns {(root: any) => any} getter returning the value or NOTHING
230
+ */
231
+ function compileSegmentsGetter(segments) {
232
+ const slen = segments.length;
233
+ if (slen === 0)
234
+ return getRoot;
235
+ const name0 = segments[0];
236
+ const index0 = scanArrayIndex(name0, 0, name0.length);
237
+ if (slen === 1) {
238
+ return function pointerGetter1(root) {
239
+ return hop(root, name0, index0);
240
+ };
241
+ }
242
+ const name1 = segments[1];
243
+ const index1 = scanArrayIndex(name1, 0, name1.length);
244
+ if (slen === 2) {
245
+ return function pointerGetter2(root) {
246
+ const v = hop(root, name0, index0);
247
+ return v === NOTHING ? NOTHING : hop(v, name1, index1);
248
+ };
249
+ }
250
+ const names = segments;
251
+ const indexes = new Array(slen);
252
+ for (let i = 0; i < slen; i++)
253
+ indexes[i] = scanArrayIndex(segments[i], 0, segments[i].length);
254
+ return function pointerGetterN(root) {
255
+ let v = root;
256
+ for (let i = 0; i < slen; i++) {
257
+ v = hop(v, names[i], indexes[i]);
258
+ if (v === NOTHING)
259
+ return NOTHING;
260
+ }
261
+ return v;
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Compile a JSON Pointer (RFC 6901) into a reusable getter.
267
+ *
268
+ * All decisions are taken at compile time: member names are pre-decoded,
269
+ * array indexes pre-parsed, and the getter is specialized by segment
270
+ * count. Resolution allocates nothing.
271
+ *
272
+ * @param {string} pointer - The JSON Pointer (e.g. `/store/book/0`)
273
+ * @returns {JsonPointerGetter} getter returning the addressed value, or
274
+ * `JSONPOINTER_NOTHING` when the pointer does not address a location
275
+ * @throws {JSONPointerSyntaxError} When the pointer is not valid RFC 6901
276
+ * @example
277
+ * const get = compileJSONPointer('/limits/min');
278
+ * get({ limits: { min: 2 } }); // 2
279
+ * get({}); // JSONPOINTER_NOTHING
280
+ */
281
+ export function compileJSONPointer(pointer) {
282
+ return compileSegmentsGetter(parseJSONPointer(pointer));
283
+ }
284
+
285
+ /**
286
+ * Trim `levels` segments off the end of an RFC 6901 location path by
287
+ * scanning backwards for the N-th '/'. Returns the exclusive end index
288
+ * of the trimmed prefix, or -1 when `levels` exceeds the depth.
289
+ */
290
+ function trimLevels(dataPath, levels) {
291
+ let end = dataPath.length;
292
+ for (let i = 0; i < levels; i++) {
293
+ if (end === 0)
294
+ return -1;
295
+ end = dataPath.lastIndexOf('/', end - 1);
296
+ if (end < 0)
297
+ return -1;
298
+ }
299
+ return end;
300
+ }
301
+
302
+ /**
303
+ * Decode `source[start..end)` where `tilde` is the position of the first
304
+ * `~`. Lax decode: invalid escapes are kept literally (location paths are
305
+ * machine-generated; this mirrors the historical decode).
306
+ */
307
+ function decodeSegmentRange(source, start, end, tilde) {
308
+ let out = source.slice(start, tilde);
309
+ let pos = tilde;
310
+ let chunk = tilde;
311
+ while (pos < end) {
312
+ if (source.charCodeAt(pos) === CC_TILDE) {
313
+ const d = pos + 1 < end ? source.charCodeAt(pos + 1) : -1;
314
+ if (d === CC_0 || d === CC_1) {
315
+ out += source.slice(chunk, pos) + (d === CC_0 ? '~' : '/');
316
+ pos += 2;
317
+ chunk = pos;
318
+ continue;
319
+ }
320
+ }
321
+ pos++;
322
+ }
323
+ return out + source.slice(chunk, end);
324
+ }
325
+
326
+ /**
327
+ * The last segment of `dataPath.slice(0, end)`, decoded lazily: the
328
+ * common escape-free case allocates nothing beyond the result slice.
329
+ * At the root (`end === 0`) the name of the location is `''`.
330
+ */
331
+ function lastSegmentOf(dataPath, end) {
332
+ if (end === 0)
333
+ return '';
334
+ const start = dataPath.lastIndexOf('/', end - 1) + 1;
335
+ for (let i = start; i < end; i++) {
336
+ if (dataPath.charCodeAt(i) === CC_TILDE)
337
+ return decodeSegmentRange(dataPath, start, end, i);
338
+ }
339
+ return dataPath.slice(start, end);
340
+ }
341
+
342
+ /**
343
+ * Walk `root` along the location path prefix `path.slice(0, end)`.
344
+ * Segments are decoded lazily per hop (escape-free segments are sliced
345
+ * directly; array indexes are scanned in place without allocating).
346
+ */
347
+ function walkPointerPrefix(root, path, end) {
348
+ let v = root;
349
+ let pos = 0;
350
+ while (pos < end) {
351
+ pos++; // consume '/'
352
+ const start = pos;
353
+ let tilde = -1;
354
+ while (pos < end) {
355
+ const c = path.charCodeAt(pos);
356
+ if (c === CC_SLASH)
357
+ break;
358
+ if (c === CC_TILDE && tilde < 0)
359
+ tilde = pos;
360
+ pos++;
361
+ }
362
+ if (typeof v !== 'object' || v === null)
363
+ return NOTHING;
364
+ if (Array.isArray(v)) {
365
+ const index = scanArrayIndex(path, start, pos);
366
+ if (index < 0 || index >= v.length)
367
+ return NOTHING;
368
+ v = v[index];
369
+ }
370
+ else {
371
+ const name = tilde < 0 ? path.slice(start, pos) : decodeSegmentRange(path, start, pos, tilde);
372
+ if (!hasOwn(v, name))
373
+ return NOTHING;
374
+ v = v[name];
375
+ }
376
+ }
377
+ return v;
378
+ }
379
+
380
+ /**
381
+ * Compile a Relative JSON Pointer into a reusable resolver.
382
+ *
383
+ * The relative part (level count, `#` form, trailing segments) compiles
384
+ * once; per call only `dataPath` - the current location in `dataRoot` as
385
+ * an RFC 6901 pointer - varies. The `#` form resolves to the member name
386
+ * or array index of the location **as a string** (`''` at the root),
387
+ * matching the historical behavior relied on by the validator's `$data`
388
+ * keyword.
389
+ *
390
+ * @param {string} pointer - The relative pointer (e.g. `1/sibling`, `0#`)
391
+ * @returns {RelativeJsonPointerResolver} resolver returning
392
+ * the addressed value, or `JSONPOINTER_NOTHING`
393
+ * @throws {JSONPointerSyntaxError} When the pointer is not valid
394
+ * @example
395
+ * const resolve = compileRelativeJSONPointer('1/limits');
396
+ * resolve({ limits: { min: 2 } , value: 5 }, '/value'); // { min: 2 }
397
+ */
398
+ export function compileRelativeJSONPointer(pointer) {
399
+ const { levels, hash, segments } = parseRelativeJSONPointer(pointer);
400
+ if (hash) {
401
+ return function relativeHashResolver(dataRoot, dataPath) {
402
+ if (typeof dataPath !== 'string')
403
+ dataPath = '';
404
+ else if (dataPath.length !== 0 && dataPath.charCodeAt(0) !== CC_SLASH)
405
+ return NOTHING;
406
+ const end = trimLevels(dataPath, levels);
407
+ if (end < 0)
408
+ return NOTHING;
409
+ return lastSegmentOf(dataPath, end);
410
+ };
411
+ }
412
+ const getter = compileSegmentsGetter(segments);
413
+ return function relativeResolver(dataRoot, dataPath) {
414
+ if (typeof dataPath !== 'string')
415
+ dataPath = '';
416
+ else if (dataPath.length !== 0 && dataPath.charCodeAt(0) !== CC_SLASH)
417
+ return NOTHING;
418
+ const end = trimLevels(dataPath, levels);
419
+ if (end < 0)
420
+ return NOTHING;
421
+ const base = walkPointerPrefix(dataRoot, dataPath, end);
422
+ return base === NOTHING ? NOTHING : getter(base);
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Compile a data reference - the accepted forms of the validator's
428
+ * `data`/`$data` keywords - into a reusable resolver. The dispatch is
429
+ * decided once at compile time: a leading digit is a Relative JSON
430
+ * Pointer, a leading `/` an absolute JSON Pointer, and `''` the root.
431
+ *
432
+ * @param {string} ref - The reference string
433
+ * @returns {RelativeJsonPointerResolver} resolver returning
434
+ * the addressed value, or `JSONPOINTER_NOTHING`
435
+ * @throws {JSONPointerSyntaxError} When the reference is none of the
436
+ * accepted forms
437
+ */
438
+ export function compileDataRef(ref) {
439
+ if (typeof ref !== 'string')
440
+ throw new JSONPointerSyntaxError('a data reference must be a string', String(ref), 0);
441
+ if (ref.length === 0)
442
+ return getRoot;
443
+ const c = ref.charCodeAt(0);
444
+ if (isDigitCode(c))
445
+ return compileRelativeJSONPointer(ref);
446
+ if (c === CC_SLASH)
447
+ return compileSegmentsGetter(scanSegments(ref, 0));
448
+ throw new JSONPointerSyntaxError('a data reference must be empty, a JSON Pointer or a Relative JSON Pointer', ref, 0);
449
+ }
450
+
451
+ //#endregion
452
+
453
+ //#endregion