@jarenjs/json 0.9.2 → 0.34.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 (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
package/src/write.js ADDED
@@ -0,0 +1,660 @@
1
+ //#region Write operations
2
+ // Standalone compiled write operations: set / insert / remove at a
3
+ // JSON Pointer (RFC 6901), a normalized path (RFC 9535 section 2.7,
4
+ // or any singular query), or at every node an arbitrary JSONPath query
5
+ // selects - with copy-on-write application throughout.
6
+ //
7
+ // The pointer-addressed writers follow the two-stage house pipeline:
8
+ // the target (pointer or singular query - dispatch decided once on the
9
+ // first character) parses into the cow.js step encoding and the writer
10
+ // closes over it. The JSONPath writers compile the query once; applying
11
+ // runs it in nodes mode and rewrites the matched locations in REVERSE
12
+ // document order - descendants before ancestors, later siblings before
13
+ // earlier ones - so array-index shifts from inserts/removes never
14
+ // invalidate the remaining locations, and an ancestor rewrite
15
+ // deterministically wins over rewrites inside it.
16
+ //
17
+ // Application is copy-on-write via the shared owned-set machinery
18
+ // (cow.js): the input document is never mutated, only the written spine
19
+ // is cloned (once, however many locations share it), untouched subtrees
20
+ // are shared with the result. `{ mutate: true }` patches in place.
21
+ //
22
+ // Error codes:
23
+ // JW0001 - the write target has the wrong form (not a pointer or a
24
+ // singular JSONPath query)
25
+ // JW2001 - the target location does not exist
26
+ // JW2002 - invalid array position
27
+ // JW2003 - the root of the document cannot be removed
28
+
29
+ import {
30
+ CC_DOLLAR,
31
+ CC_SQUOTE,
32
+ CC_BACKSLASH,
33
+ CC_RBRACKET,
34
+ CC_0,
35
+ } from '@jarenjs/core/scan';
36
+
37
+ import {
38
+ parseJSONPointer,
39
+ } from './pointer.js';
40
+
41
+ import { parseJSONPath, compileJSONPath } from './path.js';
42
+
43
+ import { scanArrayIndex, isSingularSegments } from './segments.js';
44
+
45
+ import { isJsonObject, setObjectMember } from '@jarenjs/core/object';
46
+ import { CodedError } from '@jarenjs/core/errors';
47
+
48
+ import {
49
+ makeState,
50
+ ownedRoot,
51
+ ownedChild,
52
+ stepArrayIndex,
53
+ } from './cow.js';
54
+
55
+ const hasOwn = Object.hasOwn;
56
+
57
+ //#region errors
58
+
59
+ /**
60
+ * Error thrown when a write target is malformed (`JW0001`) or a write
61
+ * fails to apply (`JW2xxx`). `dataPath` is the write target as given
62
+ * (a JSON Pointer or JSONPath), or the normalized path of the failing
63
+ * location for query-selected writes — rendered `in data <path>` per
64
+ * the `@jarenjs/core` coded contract, because it locates *data*, not a
65
+ * document.
66
+ */
67
+ export class JsonWriteError extends CodedError {
68
+ constructor(code, reason, dataPath) {
69
+ super('JsonWriteError', code, reason, { dataPath });
70
+ }
71
+ }
72
+
73
+ function writeError(code, message, dataPath) {
74
+ return new JsonWriteError(code, message, dataPath);
75
+ }
76
+
77
+ //#endregion
78
+
79
+ //#region targets
80
+
81
+ /**
82
+ * Parse a write target into the cow.js step encoding. A target starting
83
+ * with `$` is a JSONPath and must be singular (normalized paths are);
84
+ * `''` or a leading `/` is an RFC 6901 pointer (one token, two forms).
85
+ * The dispatch is decided once at compile time.
86
+ */
87
+ function parseWriteTarget(target) {
88
+ if (typeof target !== 'string')
89
+ throw writeError('JW0001', 'a write target must be a string', String(target));
90
+ if (target.length !== 0 && target.charCodeAt(0) === CC_DOLLAR) {
91
+ const { segments } = parseJSONPath(target);
92
+ if (!isSingularSegments(segments))
93
+ throw writeError('JW0001', 'a write target must be a singular JSONPath query', target);
94
+ const len = segments.length;
95
+ const names = new Array(len);
96
+ const indexes = new Array(len);
97
+ for (let i = 0; i < len; i++) {
98
+ const sel = segments[i].selectors[0];
99
+ if (sel.kind === 'name') {
100
+ names[i] = sel.name;
101
+ indexes[i] = -1;
102
+ }
103
+ else {
104
+ names[i] = null;
105
+ indexes[i] = sel.index;
106
+ }
107
+ }
108
+ return { pointer: target, names, indexes, len };
109
+ }
110
+ const names = parseJSONPointer(target);
111
+ const len = names.length;
112
+ const indexes = new Array(len);
113
+ for (let i = 0; i < len; i++)
114
+ indexes[i] = scanArrayIndex(names[i], 0, names[i].length);
115
+ return { pointer: target, names, indexes, len };
116
+ }
117
+
118
+ function parseMutate(options) {
119
+ return options !== undefined && options !== null && options.mutate === true;
120
+ }
121
+
122
+ function parseParents(options) {
123
+ if (options === undefined || options === null || options.parents === undefined)
124
+ return false;
125
+ if (options.parents === 'reject')
126
+ return false;
127
+ if (options.parents === 'create')
128
+ return true;
129
+ throw new TypeError(`unknown 'parents' option '${options.parents}'`);
130
+ }
131
+
132
+ //#endregion
133
+
134
+ //#region walk and leaf operations
135
+
136
+ // Walk to the parent of the target location, cloning the spine on first
137
+ // touch. Steps use the cow.js encoding; a miss raises JW2001.
138
+ function walkOwnedParent(state, names, indexes, plen, dataPath) {
139
+ let v = ownedRoot(state);
140
+ for (let i = 0; i < plen; i++) {
141
+ if (Array.isArray(v)) {
142
+ const idx = stepArrayIndex(v, names[i], indexes[i]);
143
+ if (idx < 0 || idx >= v.length)
144
+ throw writeError('JW2001', 'the location does not exist', dataPath);
145
+ v = ownedChild(state, v, v[idx], idx);
146
+ }
147
+ else if (typeof v === 'object' && v !== null) {
148
+ const name = names[i];
149
+ if (name === null || !hasOwn(v, name))
150
+ throw writeError('JW2001', 'the location does not exist', dataPath);
151
+ v = ownedChild(state, v, v[name], name);
152
+ }
153
+ else {
154
+ throw writeError('JW2001', 'the location does not exist', dataPath);
155
+ }
156
+ }
157
+ return v;
158
+ }
159
+
160
+ /** A value may be an updater function `(oldValue, location) => next`. */
161
+ function resolveValue(value, oldValue, location) {
162
+ return typeof value === 'function' ? value(oldValue, location) : value;
163
+ }
164
+
165
+ // The container a created step should hold, decided by the FOLLOWING
166
+ // step: an array for index-shaped steps ('-', a typed index, or a
167
+ // numeric token), an object otherwise - the same inference schema-less
168
+ // form data uses (@jarenjs/forms).
169
+ function createdContainer(names, indexes, step) {
170
+ return names[step] === null || names[step] === '-' || indexes[step] >= 0
171
+ ? []
172
+ : {};
173
+ }
174
+
175
+ // walkOwnedParent with `parents: 'create'` semantics: missing members
176
+ // and one-past-the-end array slots grow fresh containers; a scalar (or
177
+ // null) on the spine is REPLACED by a fresh container. Fresh containers
178
+ // are owned by construction, so later writes mutate them in place.
179
+ function walkCreateParent(state, names, indexes, plen, dataPath) {
180
+ let v = ownedRoot(state);
181
+ if (v === null || typeof v !== 'object') {
182
+ const fresh = createdContainer(names, indexes, 0);
183
+ if (state.owned !== null)
184
+ state.owned.add(fresh);
185
+ state.root = fresh;
186
+ v = fresh;
187
+ }
188
+ for (let i = 0; i < plen; i++) {
189
+ if (Array.isArray(v)) {
190
+ const idx = names[i] === '-' ? v.length : stepArrayIndex(v, names[i], indexes[i]);
191
+ if (idx < 0 || idx > v.length)
192
+ throw writeError('JW2002', `invalid array position '${names[i] === null ? indexes[i] : names[i]}'`, dataPath);
193
+ const child = idx === v.length ? undefined : v[idx];
194
+ if (child !== null && typeof child === 'object') {
195
+ v = ownedChild(state, v, child, idx);
196
+ }
197
+ else {
198
+ const fresh = createdContainer(names, indexes, i + 1);
199
+ if (state.owned !== null)
200
+ state.owned.add(fresh);
201
+ v[idx] = fresh;
202
+ v = fresh;
203
+ }
204
+ }
205
+ else {
206
+ const name = names[i];
207
+ if (name === null)
208
+ throw writeError('JW2001', 'a typed index step cannot address an object member', dataPath);
209
+ const child = hasOwn(v, name) ? v[name] : undefined;
210
+ if (child !== null && typeof child === 'object') {
211
+ v = ownedChild(state, v, child, name);
212
+ }
213
+ else {
214
+ const fresh = createdContainer(names, indexes, i + 1);
215
+ if (state.owned !== null)
216
+ state.owned.add(fresh);
217
+ setObjectMember(v, name, fresh);
218
+ v = fresh;
219
+ }
220
+ }
221
+ }
222
+ return v;
223
+ }
224
+
225
+ // set semantics: replace the element / member, create the member when
226
+ // absent, extend an array by one at index === length (or '-').
227
+ function leafSet(parent, name, index, value, dataPath) {
228
+ if (Array.isArray(parent)) {
229
+ const len = parent.length;
230
+ const idx = name === '-' ? len : stepArrayIndex(parent, name, index);
231
+ if (idx < 0 || idx > len)
232
+ throw writeError('JW2002', `invalid array position '${name === null ? index : name}'`, dataPath);
233
+ if (idx === len)
234
+ parent.push(resolveValue(value, undefined, dataPath));
235
+ else
236
+ parent[idx] = resolveValue(value, parent[idx], dataPath);
237
+ return;
238
+ }
239
+ if (isJsonObject(parent)) {
240
+ if (name === null)
241
+ throw writeError('JW2001', 'the location does not exist', dataPath);
242
+ const old = hasOwn(parent, name) ? parent[name] : undefined;
243
+ setObjectMember(parent, name, resolveValue(value, old, dataPath));
244
+ return;
245
+ }
246
+ throw writeError('JW2001', 'the location does not exist', dataPath);
247
+ }
248
+
249
+ // insert semantics: RFC 6902 `add` - array insert with shift ('-'
250
+ // appends), object member set-or-create.
251
+ function leafInsert(parent, name, index, value, dataPath) {
252
+ if (Array.isArray(parent)) {
253
+ const len = parent.length;
254
+ const idx = name === '-' ? len : stepArrayIndex(parent, name, index);
255
+ if (idx < 0 || idx > len)
256
+ throw writeError('JW2002', `invalid array position '${name === null ? index : name}'`, dataPath);
257
+ if (idx === len)
258
+ parent.push(value);
259
+ else
260
+ parent.splice(idx, 0, value);
261
+ return;
262
+ }
263
+ if (isJsonObject(parent)) {
264
+ if (name === null)
265
+ throw writeError('JW2001', 'the location does not exist', dataPath);
266
+ setObjectMember(parent, name, value);
267
+ return;
268
+ }
269
+ throw writeError('JW2001', 'the location does not exist', dataPath);
270
+ }
271
+
272
+ // remove semantics: delete the element (with shift) / member. `lenient`
273
+ // turns a missing location into a no-op (the query-selected writers).
274
+ function leafRemove(parent, name, index, dataPath, lenient) {
275
+ if (Array.isArray(parent)) {
276
+ const idx = name === '-' ? -1 : stepArrayIndex(parent, name, index);
277
+ if (idx < 0 || idx >= parent.length) {
278
+ if (lenient)
279
+ return;
280
+ throw writeError(idx < 0 ? 'JW2002' : 'JW2001',
281
+ idx < 0 ? `invalid array position '${name === null ? index : name}'` : 'the location does not exist',
282
+ dataPath);
283
+ }
284
+ parent.splice(idx, 1);
285
+ return;
286
+ }
287
+ if (isJsonObject(parent) && name !== null && hasOwn(parent, name)) {
288
+ delete parent[name];
289
+ return;
290
+ }
291
+ if (!lenient)
292
+ throw writeError('JW2001', 'the location does not exist', dataPath);
293
+ }
294
+
295
+ //#endregion
296
+
297
+ //#region pointer-addressed writers
298
+
299
+ /**
300
+ * Options for the compiled write operations.
301
+ * @typedef {Object} JsonWriteOptions
302
+ * @property {boolean} [mutate] - Apply in place instead of copy-on-write.
303
+ * @property {'reject'|'create'} [parents] - What a missing spine means
304
+ * for setters and inserters: `'reject'` (default) raises `JW2001`;
305
+ * `'create'` grows fresh containers along the way — an array when the
306
+ * following step is index-shaped (`-`, a typed index, or a numeric
307
+ * token), an object otherwise — and REPLACES a scalar or `null` found
308
+ * on the spine. The schema-less form-data discipline (@jarenjs/forms
309
+ * `setValueAtPointer` is this option plus undefined-deletes).
310
+ * @property {Record<string, import('./path.js').JSONPathFunction>} [pathFunctions]
311
+ * Custom JSONPath function extensions, for the JSONPath-addressed
312
+ * writers only. A pointer-addressed target is a singular query, which
313
+ * has no filters and so no function calls.
314
+ */
315
+
316
+ /**
317
+ * A compiled setter/inserter: applies the write and returns the new
318
+ * document. `value` may be an updater function `(oldValue, location) =>
319
+ * next` for setters.
320
+ * @typedef {(root: any, value: any) => any} JsonWriter
321
+ */
322
+
323
+ /**
324
+ * A compiled remover: removes the location and returns the new document.
325
+ * @typedef {(root: any) => any} JsonRemover
326
+ */
327
+
328
+ /**
329
+ * Compile a `set` at a JSON Pointer, a normalized path, or any singular
330
+ * JSONPath query. Set replaces the addressed element or member, creates
331
+ * the member when absent (parents must exist), and extends an array by
332
+ * one at index == length (`/arr/-` appends). `value` may be an updater
333
+ * function `(oldValue, location) => next`.
334
+ *
335
+ * Application is copy-on-write: the input is never mutated and
336
+ * untouched subtrees are shared with the result.
337
+ *
338
+ * @param {string} target - The write target (e.g. `/user/name`, `$['user']['name']`)
339
+ * @param {JsonWriteOptions} [options] - Write options
340
+ * @returns {JsonWriter} `(root, value) => newRoot`
341
+ * @throws {JsonWriteError} `JW0001` when the target is not a pointer or
342
+ * singular query
343
+ * @example
344
+ * const setZip = compileJSONPointerSetter('/address/zip');
345
+ * setZip(doc, '10999'); // doc untouched, spine cloned once
346
+ */
347
+ export function compileJSONPointerSetter(target, options = undefined) {
348
+ const t = parseWriteTarget(target);
349
+ const mutate = parseMutate(options);
350
+ const create = parseParents(options);
351
+ if (t.len === 0)
352
+ return (root, value) => resolveValue(value, root, t.pointer);
353
+ const plen = t.len - 1;
354
+ const walk = create ? walkCreateParent : walkOwnedParent;
355
+ return function setAt(root, value) {
356
+ const state = makeState(root, mutate ? null : new Set());
357
+ const parent = walk(state, t.names, t.indexes, plen, t.pointer);
358
+ leafSet(parent, t.names[plen], t.indexes[plen], value, t.pointer);
359
+ return state.root;
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Compile an `insert` at a JSON Pointer, a normalized path, or any
365
+ * singular JSONPath query - RFC 6902 `add` semantics: array elements
366
+ * shift right (`/arr/-` appends), object members are set-or-created.
367
+ *
368
+ * @param {string} target - The write target
369
+ * @param {JsonWriteOptions} [options] - Write options
370
+ * @returns {JsonWriter} `(root, value) => newRoot`
371
+ * @throws {JsonWriteError} `JW0001` when the target is not a pointer or
372
+ * singular query
373
+ */
374
+ export function compileJSONPointerInserter(target, options = undefined) {
375
+ const t = parseWriteTarget(target);
376
+ const mutate = parseMutate(options);
377
+ const create = parseParents(options);
378
+ if (t.len === 0)
379
+ return (root, value) => value;
380
+ const plen = t.len - 1;
381
+ const walk = create ? walkCreateParent : walkOwnedParent;
382
+ return function insertAt(root, value) {
383
+ const state = makeState(root, mutate ? null : new Set());
384
+ const parent = walk(state, t.names, t.indexes, plen, t.pointer);
385
+ leafInsert(parent, t.names[plen], t.indexes[plen], value, t.pointer);
386
+ return state.root;
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Compile a `remove` at a JSON Pointer, a normalized path, or any
392
+ * singular JSONPath query. The location must exist (`JW2001`); array
393
+ * elements shift left.
394
+ *
395
+ * @param {string} target - The write target
396
+ * @param {JsonWriteOptions} [options] - Write options
397
+ * @returns {JsonRemover} `(root) => newRoot`
398
+ * @throws {JsonWriteError} `JW0001` when the target is not a pointer or
399
+ * singular query
400
+ */
401
+ export function compileJSONPointerRemover(target, options = undefined) {
402
+ const t = parseWriteTarget(target);
403
+ const mutate = parseMutate(options);
404
+ if (t.len === 0) {
405
+ return () => {
406
+ throw writeError('JW2003', 'the root of the document cannot be removed', t.pointer);
407
+ };
408
+ }
409
+ const plen = t.len - 1;
410
+ return function removeAt(root) {
411
+ const state = makeState(root, mutate ? null : new Set());
412
+ const parent = walkOwnedParent(state, t.names, t.indexes, plen, t.pointer);
413
+ leafRemove(parent, t.names[plen], t.indexes[plen], t.pointer, false);
414
+ return state.root;
415
+ };
416
+ }
417
+
418
+ /**
419
+ * One-shot `set` at a pointer / normalized path / singular query. On hot
420
+ * paths prefer `compileJSONPointerSetter` and reuse the writer.
421
+ * @param {any} root - The document (never mutated unless `options.mutate`)
422
+ * @param {string} target - The write target
423
+ * @param {any} value - The value, or an updater `(oldValue, location) => next`
424
+ * @param {JsonWriteOptions} [options] - Write options
425
+ * @returns {any} The new document
426
+ */
427
+ export function setAtJSONPointer(root, target, value, options = undefined) {
428
+ return compileJSONPointerSetter(target, options)(root, value);
429
+ }
430
+
431
+ /**
432
+ * One-shot `insert` at a pointer / normalized path / singular query.
433
+ * @param {any} root - The document (never mutated unless `options.mutate`)
434
+ * @param {string} target - The write target
435
+ * @param {any} value - The value to insert
436
+ * @param {JsonWriteOptions} [options] - Write options
437
+ * @returns {any} The new document
438
+ */
439
+ export function insertAtJSONPointer(root, target, value, options = undefined) {
440
+ return compileJSONPointerInserter(target, options)(root, value);
441
+ }
442
+
443
+ /**
444
+ * One-shot `remove` at a pointer / normalized path / singular query.
445
+ * @param {any} root - The document (never mutated unless `options.mutate`)
446
+ * @param {string} target - The write target
447
+ * @param {JsonWriteOptions} [options] - Write options
448
+ * @returns {any} The new document
449
+ */
450
+ export function removeAtJSONPointer(root, target, options = undefined) {
451
+ return compileJSONPointerRemover(target, options)(root);
452
+ }
453
+
454
+ //#endregion
455
+
456
+ //#region JSONPath-selected writers
457
+
458
+ // Scan a normalized path produced by the nodes-mode engine back into
459
+ // typed steps. The engine only emits `['name']` (with the section 2.7
460
+ // escape set) and `[index]` selectors, so this scanner is total for its
461
+ // input; it is never exposed to user text.
462
+ function scanNormalizedSteps(path) {
463
+ const names = [];
464
+ const indexes = [];
465
+ const len = path.length;
466
+ let pos = 1; // skip '$'
467
+ while (pos < len) {
468
+ pos++; // consume '['
469
+ if (path.charCodeAt(pos) === CC_SQUOTE) {
470
+ pos++;
471
+ let name = '';
472
+ let chunk = pos;
473
+ while (path.charCodeAt(pos) !== CC_SQUOTE) {
474
+ if (path.charCodeAt(pos) === CC_BACKSLASH) {
475
+ name += path.slice(chunk, pos);
476
+ const esc = path.charCodeAt(pos + 1);
477
+ if (esc === 0x75 /* 'u' */) {
478
+ name += String.fromCharCode(parseInt(path.slice(pos + 2, pos + 6), 16));
479
+ pos += 6;
480
+ }
481
+ else {
482
+ name += esc === 0x62 ? '\b'
483
+ : esc === 0x74 ? '\t'
484
+ : esc === 0x6E ? '\n'
485
+ : esc === 0x66 ? '\f'
486
+ : esc === 0x72 ? '\r'
487
+ : String.fromCharCode(esc); // \' and \\
488
+ pos += 2;
489
+ }
490
+ chunk = pos;
491
+ }
492
+ else {
493
+ pos++;
494
+ }
495
+ }
496
+ names.push(name + path.slice(chunk, pos));
497
+ indexes.push(-1);
498
+ pos += 2; // consume "']"
499
+ }
500
+ else {
501
+ let index = 0;
502
+ while (path.charCodeAt(pos) !== CC_RBRACKET) {
503
+ index = index * 10 + (path.charCodeAt(pos) - CC_0);
504
+ pos++;
505
+ }
506
+ names.push(null);
507
+ indexes.push(index);
508
+ pos++; // consume ']'
509
+ }
510
+ }
511
+ return { names, indexes, len: names.length };
512
+ }
513
+
514
+ // The shared apply loop of the query-selected writers: run the compiled
515
+ // query in nodes mode, then rewrite the matched locations in reverse
516
+ // document order (dedupe first - RFC 9535 nodelists may repeat a node).
517
+ function applyAtNodes(query, root, mutate, leaf) {
518
+ const paths = query.paths(root);
519
+ if (paths.length === 0)
520
+ return root;
521
+ const state = makeState(root, mutate ? null : new Set());
522
+ const seen = paths.length > 1 ? new Set() : null;
523
+ for (let i = paths.length - 1; i >= 0; i--) {
524
+ const path = paths[i];
525
+ if (seen !== null) {
526
+ if (seen.has(path))
527
+ continue;
528
+ seen.add(path);
529
+ }
530
+ leaf(state, path);
531
+ }
532
+ return state.root;
533
+ }
534
+
535
+ /**
536
+ * Compile a `set` at every node a JSONPath query selects. Applying runs
537
+ * the query against the document and replaces each matched node; `value`
538
+ * may be an updater function `(oldValue, normalizedPath) => next`.
539
+ * Matching nothing is a no-op. Locations are rewritten in reverse
540
+ * document order, so when matches nest, the ancestor's rewrite wins.
541
+ *
542
+ * @param {string} path - The JSONPath query (e.g. `$..price`)
543
+ * @param {JsonWriteOptions} [options] - Write options
544
+ * @returns {JsonWriter} `(root, value) => newRoot`
545
+ * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
546
+ * @example
547
+ * const addVat = compileJSONPathSetter('$..price');
548
+ * addVat(doc, (price) => price * 1.21);
549
+ */
550
+ export function compileJSONPathSetter(path, options = undefined) {
551
+ const query = compileJSONPath(path, options);
552
+ const mutate = parseMutate(options);
553
+ return function setAtMatches(root, value) {
554
+ return applyAtNodes(query, root, mutate, (state, p) => {
555
+ if (p === '$') {
556
+ state.root = resolveValue(value, state.root, p);
557
+ return;
558
+ }
559
+ const t = scanNormalizedSteps(p);
560
+ const parent = walkOwnedParent(state, t.names, t.indexes, t.len - 1, p);
561
+ leafSet(parent, t.names[t.len - 1], t.indexes[t.len - 1], value, p);
562
+ });
563
+ };
564
+ }
565
+
566
+ /**
567
+ * Compile an `insert` at every node a JSONPath query selects - RFC 6902
568
+ * `add` semantics per location: the value is inserted *before* each
569
+ * matched array element (later siblings shift right), and replaces
570
+ * matched object members. Matching nothing is a no-op.
571
+ *
572
+ * @param {string} path - The JSONPath query (e.g. `$.list[0]`)
573
+ * @param {JsonWriteOptions} [options] - Write options
574
+ * @returns {JsonWriter} `(root, value) => newRoot`
575
+ * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
576
+ */
577
+ export function compileJSONPathInserter(path, options = undefined) {
578
+ const query = compileJSONPath(path, options);
579
+ const mutate = parseMutate(options);
580
+ return function insertAtMatches(root, value) {
581
+ return applyAtNodes(query, root, mutate, (state, p) => {
582
+ if (p === '$') {
583
+ state.root = value;
584
+ return;
585
+ }
586
+ const t = scanNormalizedSteps(p);
587
+ const parent = walkOwnedParent(state, t.names, t.indexes, t.len - 1, p);
588
+ leafInsert(parent, t.names[t.len - 1], t.indexes[t.len - 1], value, p);
589
+ });
590
+ };
591
+ }
592
+
593
+ /**
594
+ * Compile a `remove` of every node a JSONPath query selects. Array
595
+ * elements are removed with shift; removals apply in reverse document
596
+ * order, so multiple removals from one array (and nested removals)
597
+ * compose correctly. Matching nothing is a no-op; selecting the root
598
+ * raises `JW2003`.
599
+ *
600
+ * @param {string} path - The JSONPath query (e.g. `$.store.book[?@.price > 20]`)
601
+ * @param {JsonWriteOptions} [options] - Write options
602
+ * @returns {JsonRemover} `(root) => newRoot`
603
+ * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
604
+ * @example
605
+ * const dropExpensive = compileJSONPathRemover('$.store.book[?@.price > 20]');
606
+ * dropExpensive(doc); // matched books removed, everything else shared
607
+ */
608
+ export function compileJSONPathRemover(path, options = undefined) {
609
+ const query = compileJSONPath(path, options);
610
+ const mutate = parseMutate(options);
611
+ return function removeMatches(root) {
612
+ return applyAtNodes(query, root, mutate, (state, p) => {
613
+ if (p === '$')
614
+ throw writeError('JW2003', 'the root of the document cannot be removed', p);
615
+ const t = scanNormalizedSteps(p);
616
+ const parent = walkOwnedParent(state, t.names, t.indexes, t.len - 1, p);
617
+ leafRemove(parent, t.names[t.len - 1], t.indexes[t.len - 1], p, true);
618
+ });
619
+ };
620
+ }
621
+
622
+ /**
623
+ * One-shot `set` at every node a JSONPath query selects. On hot paths
624
+ * prefer `compileJSONPathSetter` and reuse the writer.
625
+ * @param {any} root - The document (never mutated unless `options.mutate`)
626
+ * @param {string} path - The JSONPath query
627
+ * @param {any} value - The value, or an updater `(oldValue, normalizedPath) => next`
628
+ * @param {JsonWriteOptions} [options] - Write options
629
+ * @returns {any} The new document
630
+ */
631
+ export function setAtJSONPath(root, path, value, options = undefined) {
632
+ return compileJSONPathSetter(path, options)(root, value);
633
+ }
634
+
635
+ /**
636
+ * One-shot `insert` at every node a JSONPath query selects.
637
+ * @param {any} root - The document (never mutated unless `options.mutate`)
638
+ * @param {string} path - The JSONPath query
639
+ * @param {any} value - The value to insert
640
+ * @param {JsonWriteOptions} [options] - Write options
641
+ * @returns {any} The new document
642
+ */
643
+ export function insertAtJSONPath(root, path, value, options = undefined) {
644
+ return compileJSONPathInserter(path, options)(root, value);
645
+ }
646
+
647
+ /**
648
+ * One-shot `remove` of every node a JSONPath query selects.
649
+ * @param {any} root - The document (never mutated unless `options.mutate`)
650
+ * @param {string} path - The JSONPath query
651
+ * @param {JsonWriteOptions} [options] - Write options
652
+ * @returns {any} The new document
653
+ */
654
+ export function removeAtJSONPath(root, path, options = undefined) {
655
+ return compileJSONPathRemover(path, options)(root);
656
+ }
657
+
658
+ //#endregion
659
+
660
+ //#endregion