@jarenjs/json 0.9.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/src/segments.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// (value, normalized-path) pairs. This module is package-internal and is
|
|
8
8
|
// deliberately not listed in the package exports.
|
|
9
9
|
|
|
10
|
-
import { equalsJson } from '@jarenjs/core/object';
|
|
11
|
-
import { countCodePoints
|
|
10
|
+
import { equalsJson, compareJsonScalarLt } from '@jarenjs/core/object';
|
|
11
|
+
import { countCodePoints } from '@jarenjs/core/string';
|
|
12
12
|
import { compileIRegexp } from '@jarenjs/core/text/iregexp';
|
|
13
13
|
import {
|
|
14
14
|
CC_TAB,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
CC_SPACE,
|
|
18
18
|
CC_SQUOTE,
|
|
19
19
|
CC_BACKSLASH,
|
|
20
|
+
CC_0,
|
|
21
|
+
isDigitCode,
|
|
20
22
|
} from '@jarenjs/core/scan';
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -28,6 +30,35 @@ export const NOTHING = Symbol('JSONPath.Nothing');
|
|
|
28
30
|
|
|
29
31
|
const hasOwn = Object.hasOwn;
|
|
30
32
|
|
|
33
|
+
// Array indexes are bounded by the maximum array length (2^32 - 1), so a
|
|
34
|
+
// valid index has at most 10 digits and is strictly below 2^32 - 1.
|
|
35
|
+
const MAX_ARRAY_INDEX = 4294967294;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Scan `source[start..end)` as an RFC 6901 array index: `0`, or a digit
|
|
39
|
+
* sequence without leading zeros. Returns -1 when the range is not a
|
|
40
|
+
* valid index (`-` is never a valid read index). Shared by the JSON
|
|
41
|
+
* Pointer compiler (pointer.js) and the JSON Patch engine (patch.js).
|
|
42
|
+
*/
|
|
43
|
+
export function scanArrayIndex(source, start, end) {
|
|
44
|
+
const digits = end - start;
|
|
45
|
+
if (digits === 0 || digits > 10)
|
|
46
|
+
return -1;
|
|
47
|
+
const first = source.charCodeAt(start);
|
|
48
|
+
if (!isDigitCode(first))
|
|
49
|
+
return -1;
|
|
50
|
+
if (first === CC_0)
|
|
51
|
+
return digits === 1 ? 0 : -1;
|
|
52
|
+
let index = first - CC_0;
|
|
53
|
+
for (let i = start + 1; i < end; i++) {
|
|
54
|
+
const c = source.charCodeAt(i);
|
|
55
|
+
if (!isDigitCode(c))
|
|
56
|
+
return -1;
|
|
57
|
+
index = index * 10 + (c - CC_0);
|
|
58
|
+
}
|
|
59
|
+
return index <= MAX_ARRAY_INDEX ? index : -1;
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
/**
|
|
32
63
|
* Returns true when every segment is a child segment with exactly one
|
|
33
64
|
* name or index selector (a "singular query", RFC 9535 section 2.3.5.1).
|
|
@@ -56,16 +87,6 @@ function cmpEquals(a, b) {
|
|
|
56
87
|
return equalsJson(a, b);
|
|
57
88
|
}
|
|
58
89
|
|
|
59
|
-
// numbers by value, strings by Unicode scalar values (RFC 9535
|
|
60
|
-
// section 2.3.5.2.2); other types do not order
|
|
61
|
-
function cmpLess(a, b) {
|
|
62
|
-
if (typeof a === 'number')
|
|
63
|
-
return typeof b === 'number' && a < b;
|
|
64
|
-
if (typeof a === 'string')
|
|
65
|
-
return typeof b === 'string' && compareCodePoints(a, b) < 0;
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
90
|
function countOwnKeys(obj) {
|
|
70
91
|
let count = 0;
|
|
71
92
|
for (const key in obj) {
|
|
@@ -129,6 +150,13 @@ export function runSegmentsV(segs, start, root) {
|
|
|
129
150
|
/**
|
|
130
151
|
* Compile an existence test for a filter query; singular queries never
|
|
131
152
|
* materialize nodelists.
|
|
153
|
+
*
|
|
154
|
+
* The non-singular case builds its nodelist rather than pulling the
|
|
155
|
+
* lazy chain (compileSegmentG), even though it only needs one node: a
|
|
156
|
+
* filter runs this per candidate node, where nodelists are a handful of
|
|
157
|
+
* items and generator setup costs more than the pushes it saves - it
|
|
158
|
+
* benched ~9x slower on `$.items[?@.tags[*]]` over 2000 items. Laziness
|
|
159
|
+
* pays at the top of a query, not inside a filter.
|
|
132
160
|
* @returns {(current: any, root: any) => boolean}
|
|
133
161
|
*/
|
|
134
162
|
export function compileExists(query) {
|
|
@@ -141,17 +169,86 @@ export function compileExists(query) {
|
|
|
141
169
|
return (current, root) => runSegmentsV(segs, relative ? current : root, root).length > 0;
|
|
142
170
|
}
|
|
143
171
|
|
|
172
|
+
// Collector for the reset hooks of `$`-rooted comparable memos, active
|
|
173
|
+
// while compileFilterPredicate compiles a filter's predicate tree; null
|
|
174
|
+
// outside a filter, where a memo would have no reset and go stale.
|
|
175
|
+
let pendingResets = null;
|
|
176
|
+
|
|
177
|
+
// Hand-back from compileFilterPredicate to the filter selector that ran
|
|
178
|
+
// it - module scope like sliceFrom/sliceTo: read immediately, never kept.
|
|
179
|
+
let filterReset = null;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Compile a filter's predicate expression, collecting the reset hooks of
|
|
183
|
+
* the `$`-rooted comparable memos inside it (see compileComparable) and
|
|
184
|
+
* handing their combined reset - or null when the tree has none - back
|
|
185
|
+
* through `filterReset`. The filter selector runs the reset before each
|
|
186
|
+
* application. A nested filter collects into its own frame, so an inner
|
|
187
|
+
* filter's memos reset per inner application.
|
|
188
|
+
* @returns {(current: any, root: any) => boolean}
|
|
189
|
+
*/
|
|
190
|
+
function compileFilterPredicate(expr) {
|
|
191
|
+
const saved = pendingResets;
|
|
192
|
+
pendingResets = [];
|
|
193
|
+
const pred = compileLogicalExpr(expr);
|
|
194
|
+
const resets = pendingResets;
|
|
195
|
+
pendingResets = saved;
|
|
196
|
+
if (resets.length === 0) {
|
|
197
|
+
filterReset = null;
|
|
198
|
+
}
|
|
199
|
+
else if (resets.length === 1) {
|
|
200
|
+
filterReset = resets[0];
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
filterReset = () => {
|
|
204
|
+
for (let i = 0; i < resets.length; i++)
|
|
205
|
+
resets[i]();
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return pred;
|
|
209
|
+
}
|
|
210
|
+
|
|
144
211
|
// getter producing a ValueType result (a JSON value or NOTHING)
|
|
145
212
|
function compileComparable(node) {
|
|
146
213
|
if (node.kind === 'literal') {
|
|
147
214
|
const value = node.value;
|
|
148
215
|
return () => value;
|
|
149
216
|
}
|
|
150
|
-
if (node.kind === 'query')
|
|
151
|
-
|
|
217
|
+
if (node.kind === 'query') {
|
|
218
|
+
const getter = compileSingularGetter(node.query.segments, node.query.relative);
|
|
219
|
+
if (node.query.relative || pendingResets === null)
|
|
220
|
+
return getter;
|
|
221
|
+
// A `$`-rooted comparable is invariant for one filter application,
|
|
222
|
+
// so it is computed at the first candidate and reused for the rest
|
|
223
|
+
// instead of re-walking from the root per candidate. The filter
|
|
224
|
+
// resets the memo before each application (a caller may mutate the
|
|
225
|
+
// document and re-run the query on the same root identity); the
|
|
226
|
+
// root check covers a reentrant run against a different document.
|
|
227
|
+
let cachedRoot = NOTHING;
|
|
228
|
+
let cachedValue = NOTHING;
|
|
229
|
+
pendingResets.push(() => { cachedRoot = NOTHING; });
|
|
230
|
+
return (current, root) => {
|
|
231
|
+
if (root !== cachedRoot) {
|
|
232
|
+
cachedValue = getter(current, root);
|
|
233
|
+
cachedRoot = root;
|
|
234
|
+
}
|
|
235
|
+
return cachedValue;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
152
238
|
return compileValueFunction(node);
|
|
153
239
|
}
|
|
154
240
|
|
|
241
|
+
// getter producing a NodesType result (an array of the selected values).
|
|
242
|
+
// The argument is a filter query or a nodes-returning extension.
|
|
243
|
+
function compileNodesGetter(arg) {
|
|
244
|
+
if (arg.kind !== 'query')
|
|
245
|
+
return compileUserFunction(arg);
|
|
246
|
+
const query = arg.query;
|
|
247
|
+
const segs = query.segments.map(compileSegmentV);
|
|
248
|
+
const relative = query.relative;
|
|
249
|
+
return (current, root) => runSegmentsV(segs, relative ? current : root, root);
|
|
250
|
+
}
|
|
251
|
+
|
|
155
252
|
function compileValueFunction(func) {
|
|
156
253
|
switch (func.name) {
|
|
157
254
|
case 'length': {
|
|
@@ -168,32 +265,97 @@ function compileValueFunction(func) {
|
|
|
168
265
|
};
|
|
169
266
|
}
|
|
170
267
|
case 'count': {
|
|
171
|
-
const
|
|
172
|
-
if (isSingularSegments(query.segments)) {
|
|
173
|
-
const getter = compileSingularGetter(query.segments, query.relative);
|
|
268
|
+
const arg = func.args[0];
|
|
269
|
+
if (arg.kind === 'query' && isSingularSegments(arg.query.segments)) {
|
|
270
|
+
const getter = compileSingularGetter(arg.query.segments, arg.query.relative);
|
|
174
271
|
return (current, root) => (getter(current, root) === NOTHING ? 0 : 1);
|
|
175
272
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
return (current, root) => runSegmentsV(segs, relative ? current : root, root).length;
|
|
273
|
+
const get = compileNodesGetter(arg);
|
|
274
|
+
return (current, root) => get(current, root).length;
|
|
179
275
|
}
|
|
180
276
|
case 'value': {
|
|
181
|
-
const
|
|
182
|
-
if (isSingularSegments(query.segments))
|
|
183
|
-
return compileSingularGetter(query.segments, query.relative);
|
|
184
|
-
|
|
185
|
-
|
|
277
|
+
const arg = func.args[0];
|
|
278
|
+
if (arg.kind === 'query' && isSingularSegments(arg.query.segments))
|
|
279
|
+
return compileSingularGetter(arg.query.segments, arg.query.relative);
|
|
280
|
+
// eager for the same reason as compileExists: this runs per
|
|
281
|
+
// candidate node inside a filter
|
|
282
|
+
const get = compileNodesGetter(arg);
|
|
186
283
|
return (current, root) => {
|
|
187
|
-
const result =
|
|
284
|
+
const result = get(current, root);
|
|
188
285
|
return result.length === 1 ? result[0] : NOTHING;
|
|
189
286
|
};
|
|
190
287
|
}
|
|
191
|
-
/* c8 ignore next 2 -- guarded by the parser's well-typedness checks */
|
|
192
288
|
default:
|
|
193
|
-
|
|
289
|
+
return compileUserFunction(func);
|
|
194
290
|
}
|
|
195
291
|
}
|
|
196
292
|
|
|
293
|
+
// Compile a registered function extension (RFC 9535 section 2.4).
|
|
294
|
+
// Arguments are marshalled to their declared types - ValueType is a
|
|
295
|
+
// JSON value or NOTHING, NodesType an array of values, LogicalType a
|
|
296
|
+
// boolean - and the result is checked against the declared return type,
|
|
297
|
+
// because a registry entry that lies about its type would otherwise
|
|
298
|
+
// corrupt the comparison rules further up.
|
|
299
|
+
function compileUserFunction(func) {
|
|
300
|
+
/* c8 ignore next 2 -- guarded by the parser's well-typedness checks */
|
|
301
|
+
if (typeof func.evaluate !== 'function')
|
|
302
|
+
throw new Error(`JSONPath: function '${func.name}' has no implementation`);
|
|
303
|
+
const evaluate = func.evaluate;
|
|
304
|
+
const name = func.name;
|
|
305
|
+
const params = func.params;
|
|
306
|
+
const gets = new Array(func.args.length);
|
|
307
|
+
for (let i = 0; i < gets.length; i++) {
|
|
308
|
+
const arg = func.args[i];
|
|
309
|
+
gets[i] = params[i] === 'value'
|
|
310
|
+
? compileComparable(arg)
|
|
311
|
+
: params[i] === 'nodes'
|
|
312
|
+
? compileNodesGetter(arg)
|
|
313
|
+
: compileLogicalExpr(arg.expr);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let call;
|
|
317
|
+
switch (gets.length) {
|
|
318
|
+
case 0:
|
|
319
|
+
call = () => evaluate();
|
|
320
|
+
break;
|
|
321
|
+
case 1: {
|
|
322
|
+
const a = gets[0];
|
|
323
|
+
call = (c, r) => evaluate(a(c, r));
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 2: {
|
|
327
|
+
const a = gets[0];
|
|
328
|
+
const b = gets[1];
|
|
329
|
+
call = (c, r) => evaluate(a(c, r), b(c, r));
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
default:
|
|
333
|
+
call = (c, r) => {
|
|
334
|
+
const argv = new Array(gets.length);
|
|
335
|
+
for (let i = 0; i < gets.length; i++)
|
|
336
|
+
argv[i] = gets[i](c, r);
|
|
337
|
+
return evaluate(...argv);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (func.returns === 'logical')
|
|
342
|
+
return (c, r) => call(c, r) === true;
|
|
343
|
+
if (func.returns === 'value') {
|
|
344
|
+
// a function may legitimately return Nothing; `undefined` is not a
|
|
345
|
+
// JSON value, so it is the natural spelling of it
|
|
346
|
+
return (c, r) => {
|
|
347
|
+
const v = call(c, r);
|
|
348
|
+
return v === undefined ? NOTHING : v;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return (c, r) => {
|
|
352
|
+
const v = call(c, r);
|
|
353
|
+
if (!Array.isArray(v))
|
|
354
|
+
throw new TypeError(`JSONPath: function '${name}' must return an array of nodes (NodesType)`);
|
|
355
|
+
return v;
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
197
359
|
function compileRegexTest(func, fullMatch) {
|
|
198
360
|
const inputGet = compileComparable(func.args[0]);
|
|
199
361
|
const patternArg = func.args[1];
|
|
@@ -236,20 +398,20 @@ function compileComparison(expr) {
|
|
|
236
398
|
case '!=':
|
|
237
399
|
return (c, r) => !cmpEquals(left(c, r), right(c, r));
|
|
238
400
|
case '<':
|
|
239
|
-
return (c, r) =>
|
|
401
|
+
return (c, r) => compareJsonScalarLt(left(c, r), right(c, r));
|
|
240
402
|
case '>':
|
|
241
|
-
return (c, r) =>
|
|
403
|
+
return (c, r) => compareJsonScalarLt(right(c, r), left(c, r));
|
|
242
404
|
case '<=':
|
|
243
405
|
return (c, r) => {
|
|
244
406
|
const a = left(c, r);
|
|
245
407
|
const b = right(c, r);
|
|
246
|
-
return
|
|
408
|
+
return compareJsonScalarLt(a, b) || cmpEquals(a, b);
|
|
247
409
|
};
|
|
248
410
|
default: // '>='
|
|
249
411
|
return (c, r) => {
|
|
250
412
|
const a = left(c, r);
|
|
251
413
|
const b = right(c, r);
|
|
252
|
-
return
|
|
414
|
+
return compareJsonScalarLt(b, a) || cmpEquals(a, b);
|
|
253
415
|
};
|
|
254
416
|
}
|
|
255
417
|
}
|
|
@@ -288,8 +450,17 @@ export function compileLogicalExpr(expr) {
|
|
|
288
450
|
}
|
|
289
451
|
case 'exists':
|
|
290
452
|
return compileExists(expr.query);
|
|
291
|
-
case 'ftest':
|
|
292
|
-
|
|
453
|
+
case 'ftest': {
|
|
454
|
+
const func = expr.func;
|
|
455
|
+
if (func.name === 'match' || func.name === 'search')
|
|
456
|
+
return compileRegexTest(func, func.name === 'match');
|
|
457
|
+
// a registered extension as a test expression: LogicalType is the
|
|
458
|
+
// answer itself, NodesType is true for a non-empty nodelist
|
|
459
|
+
const fn = compileUserFunction(func);
|
|
460
|
+
if (func.returns === 'logical')
|
|
461
|
+
return fn;
|
|
462
|
+
return (c, r) => fn(c, r).length > 0;
|
|
463
|
+
}
|
|
293
464
|
default: // 'cmp'
|
|
294
465
|
return compileComparison(expr);
|
|
295
466
|
}
|
|
@@ -299,6 +470,29 @@ export function compileLogicalExpr(expr) {
|
|
|
299
470
|
|
|
300
471
|
//#region segment compilation (values mode)
|
|
301
472
|
|
|
473
|
+
// Resolve a slice selector's bounds against an array length (RFC 9535
|
|
474
|
+
// section 2.3.4.2.2) for a `for (i = sliceFrom; i != sliceTo; i += step)`
|
|
475
|
+
// walk - exclusive at `sliceTo` in both directions. The two results are
|
|
476
|
+
// handed back through module scope rather than an object or a pair,
|
|
477
|
+
// because this runs once per input node on every slice selector in all
|
|
478
|
+
// three segment modes and must not allocate. Read them immediately; no
|
|
479
|
+
// slice applier calls anything in between.
|
|
480
|
+
let sliceFrom = 0;
|
|
481
|
+
let sliceTo = 0;
|
|
482
|
+
|
|
483
|
+
function sliceBounds(start, end, step, len) {
|
|
484
|
+
const s = start === null ? (step > 0 ? 0 : len - 1) : (start < 0 ? len + start : start);
|
|
485
|
+
const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
|
|
486
|
+
if (step > 0) {
|
|
487
|
+
sliceFrom = s < 0 ? 0 : (s > len ? len : s);
|
|
488
|
+
sliceTo = e < 0 ? 0 : (e > len ? len : e);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
sliceFrom = s < -1 ? -1 : (s > len - 1 ? len - 1 : s);
|
|
492
|
+
sliceTo = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
302
496
|
// selector-node functions: (value, output, root) => void
|
|
303
497
|
|
|
304
498
|
export function compileSelectorNodeV(sel) {
|
|
@@ -351,26 +545,23 @@ export function compileSelectorNodeV(sel) {
|
|
|
351
545
|
const len = v.length;
|
|
352
546
|
if (len === 0)
|
|
353
547
|
return;
|
|
354
|
-
|
|
355
|
-
const s = start === null ? (step > 0 ? 0 : len - 1) : (start < 0 ? len + start : start);
|
|
356
|
-
const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
|
|
548
|
+
sliceBounds(start, end, step, len);
|
|
357
549
|
if (step > 0) {
|
|
358
|
-
|
|
359
|
-
const upper = e < 0 ? 0 : (e > len ? len : e);
|
|
360
|
-
for (let i = lower; i < upper; i += step)
|
|
550
|
+
for (let i = sliceFrom; i < sliceTo; i += step)
|
|
361
551
|
out.push(v[i]);
|
|
362
552
|
}
|
|
363
553
|
else {
|
|
364
|
-
|
|
365
|
-
const lower = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
|
|
366
|
-
for (let i = upper; i > lower; i += step)
|
|
554
|
+
for (let i = sliceFrom; i > sliceTo; i += step)
|
|
367
555
|
out.push(v[i]);
|
|
368
556
|
}
|
|
369
557
|
};
|
|
370
558
|
}
|
|
371
559
|
default: { // 'filter'
|
|
372
|
-
const pred =
|
|
560
|
+
const pred = compileFilterPredicate(sel.expr);
|
|
561
|
+
const reset = filterReset;
|
|
373
562
|
return (v, out, root) => {
|
|
563
|
+
if (reset !== null)
|
|
564
|
+
reset();
|
|
374
565
|
if (Array.isArray(v)) {
|
|
375
566
|
for (let i = 0; i < v.length; i++) {
|
|
376
567
|
if (pred(v[i], root))
|
|
@@ -402,15 +593,21 @@ export function descendV(v, output, root, apply) {
|
|
|
402
593
|
}
|
|
403
594
|
}
|
|
404
595
|
|
|
596
|
+
// Combine a segment's selectors into one (value, output, root) => void
|
|
597
|
+
// applier; a single selector is its own applier.
|
|
598
|
+
function compileSelectorsV(seg) {
|
|
599
|
+
const fns = seg.selectors.map(compileSelectorNodeV);
|
|
600
|
+
if (fns.length === 1)
|
|
601
|
+
return fns[0];
|
|
602
|
+
return (v, out, root) => {
|
|
603
|
+
for (let i = 0; i < fns.length; i++)
|
|
604
|
+
fns[i](v, out, root);
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
405
608
|
// segment functions: (input, output, root) => void
|
|
406
609
|
export function compileSegmentV(seg) {
|
|
407
|
-
const
|
|
408
|
-
const apply = fns.length === 1
|
|
409
|
-
? fns[0]
|
|
410
|
-
: (v, out, root) => {
|
|
411
|
-
for (let i = 0; i < fns.length; i++)
|
|
412
|
-
fns[i](v, out, root);
|
|
413
|
-
};
|
|
610
|
+
const apply = compileSelectorsV(seg);
|
|
414
611
|
if (seg.descendant) {
|
|
415
612
|
return (input, output, root) => {
|
|
416
613
|
for (let i = 0; i < input.length; i++)
|
|
@@ -425,11 +622,161 @@ export function compileSegmentV(seg) {
|
|
|
425
622
|
|
|
426
623
|
//#endregion
|
|
427
624
|
|
|
625
|
+
//#region segment compilation (lazy values mode)
|
|
626
|
+
// The same selectors as values mode, pulled one node at a time instead
|
|
627
|
+
// of pushed into a nodelist. A consumer that stops early (`first`,
|
|
628
|
+
// `exists`, `value()`'s two-node test) never visits the rest of the
|
|
629
|
+
// document: a filter evaluates its predicate only until a node passes,
|
|
630
|
+
// a wildcard reads only the children actually pulled, and a descendant
|
|
631
|
+
// segment abandons the walk mid-subtree.
|
|
632
|
+
//
|
|
633
|
+
// Nothing here is buffered - every applier yields directly - so the
|
|
634
|
+
// laziness is per node, not per segment. Enumeration order is identical
|
|
635
|
+
// to runSegmentsV: a segment maps each input node to its outputs in
|
|
636
|
+
// order and concatenates them, so pulling the chain depth-first visits
|
|
637
|
+
// exactly the sequence values mode builds breadth-first.
|
|
638
|
+
|
|
639
|
+
// selector-node functions: (value, root) => Generator<any>
|
|
640
|
+
|
|
641
|
+
function compileSelectorNodeG(sel) {
|
|
642
|
+
switch (sel.kind) {
|
|
643
|
+
case 'name': {
|
|
644
|
+
const name = sel.name;
|
|
645
|
+
return function* nameG(v) {
|
|
646
|
+
if (typeof v === 'object' && v !== null && !Array.isArray(v) && hasOwn(v, name))
|
|
647
|
+
yield v[name];
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
case 'index': {
|
|
651
|
+
const index = sel.index;
|
|
652
|
+
return function* indexG(v) {
|
|
653
|
+
if (!Array.isArray(v))
|
|
654
|
+
return;
|
|
655
|
+
const idx = index < 0 ? v.length + index : index;
|
|
656
|
+
if (idx >= 0 && idx < v.length)
|
|
657
|
+
yield v[idx];
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
case 'wildcard':
|
|
661
|
+
return function* wildcardG(v) {
|
|
662
|
+
if (Array.isArray(v)) {
|
|
663
|
+
yield* v;
|
|
664
|
+
}
|
|
665
|
+
else if (typeof v === 'object' && v !== null) {
|
|
666
|
+
for (const key in v) {
|
|
667
|
+
if (hasOwn(v, key))
|
|
668
|
+
yield v[key];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
case 'slice': {
|
|
673
|
+
const start = sel.start;
|
|
674
|
+
const end = sel.end;
|
|
675
|
+
const step = sel.step === null ? 1 : sel.step;
|
|
676
|
+
if (step === 0)
|
|
677
|
+
return function* emptySliceG() { };
|
|
678
|
+
return function* sliceG(v) {
|
|
679
|
+
if (!Array.isArray(v))
|
|
680
|
+
return;
|
|
681
|
+
const len = v.length;
|
|
682
|
+
if (len === 0)
|
|
683
|
+
return;
|
|
684
|
+
sliceBounds(start, end, step, len);
|
|
685
|
+
// read the bounds out before the first yield: a suspended
|
|
686
|
+
// generator must not depend on the shared scratch surviving
|
|
687
|
+
const from = sliceFrom;
|
|
688
|
+
const to = sliceTo;
|
|
689
|
+
if (step > 0) {
|
|
690
|
+
for (let i = from; i < to; i += step)
|
|
691
|
+
yield v[i];
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
for (let i = from; i > to; i += step)
|
|
695
|
+
yield v[i];
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
default: { // 'filter'
|
|
700
|
+
const pred = compileFilterPredicate(sel.expr);
|
|
701
|
+
const reset = filterReset;
|
|
702
|
+
return function* filterG(v, root) {
|
|
703
|
+
if (reset !== null)
|
|
704
|
+
reset();
|
|
705
|
+
if (Array.isArray(v)) {
|
|
706
|
+
for (let i = 0; i < v.length; i++) {
|
|
707
|
+
if (pred(v[i], root))
|
|
708
|
+
yield v[i];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
else if (typeof v === 'object' && v !== null) {
|
|
712
|
+
for (const key in v) {
|
|
713
|
+
if (hasOwn(v, key) && pred(v[key], root))
|
|
714
|
+
yield v[key];
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function* descendG(v, root, apply) {
|
|
723
|
+
yield* apply(v, root);
|
|
724
|
+
if (Array.isArray(v)) {
|
|
725
|
+
for (let i = 0; i < v.length; i++)
|
|
726
|
+
yield* descendG(v[i], root, apply);
|
|
727
|
+
}
|
|
728
|
+
else if (typeof v === 'object' && v !== null) {
|
|
729
|
+
for (const key in v) {
|
|
730
|
+
if (hasOwn(v, key))
|
|
731
|
+
yield* descendG(v[key], root, apply);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Compile a segment into its lazy form: a generator function yielding
|
|
738
|
+
* the nodes one input value contributes, in document order.
|
|
739
|
+
* @param {object} seg - a parsed query segment
|
|
740
|
+
* @returns {(value: any, root: any) => Generator<any>}
|
|
741
|
+
*/
|
|
742
|
+
export function compileSegmentG(seg) {
|
|
743
|
+
const fns = seg.selectors.map(compileSelectorNodeG);
|
|
744
|
+
const apply = fns.length === 1
|
|
745
|
+
? fns[0]
|
|
746
|
+
: function* applyG(v, root) {
|
|
747
|
+
for (let i = 0; i < fns.length; i++)
|
|
748
|
+
yield* fns[i](v, root);
|
|
749
|
+
};
|
|
750
|
+
if (seg.descendant)
|
|
751
|
+
return function* segmentDescendantG(v, root) { yield* descendG(v, root, apply); };
|
|
752
|
+
return apply;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Lazily enumerate the nodelist a compiled segment chain selects,
|
|
757
|
+
* yielding values in document order.
|
|
758
|
+
* @param {Function[]} gens - segment generators from compileSegmentG
|
|
759
|
+
* @param {number} i - the segment to apply (0 to start the chain)
|
|
760
|
+
* @param {any} value - the value this segment applies to
|
|
761
|
+
* @param {any} root - the query root (`$` inside embedded filters)
|
|
762
|
+
* @returns {Generator<any>}
|
|
763
|
+
*/
|
|
764
|
+
export function* runSegmentsG(gens, i, value, root) {
|
|
765
|
+
if (i === gens.length) {
|
|
766
|
+
yield value;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
for (const v of gens[i](value, root))
|
|
770
|
+
yield* runSegmentsG(gens, i + 1, v, root);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
//#endregion
|
|
774
|
+
|
|
428
775
|
//#region segment compilation (nodes mode, normalized paths)
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
776
|
+
// The selector/segment compilers producing (value, normalized-path)
|
|
777
|
+
// pairs per RFC 9535 section 2.7. path.js imports them for
|
|
778
|
+
// `query.nodes()`/`query.paths()`; nodes mode stays lazily compiled
|
|
779
|
+
// there, so value-only queries never pay for it.
|
|
433
780
|
|
|
434
781
|
// eslint-disable-next-line no-control-regex
|
|
435
782
|
export const RE_NAME_NEEDS_ESCAPE = /['\\\u0000-\u001f]/;
|
|
@@ -516,20 +863,15 @@ export function compileSelectorNodeP(sel) {
|
|
|
516
863
|
const len = v.length;
|
|
517
864
|
if (len === 0)
|
|
518
865
|
return;
|
|
519
|
-
|
|
520
|
-
const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
|
|
866
|
+
sliceBounds(start, end, step, len);
|
|
521
867
|
if (step > 0) {
|
|
522
|
-
|
|
523
|
-
const upper = e < 0 ? 0 : (e > len ? len : e);
|
|
524
|
-
for (let i = lower; i < upper; i += step) {
|
|
868
|
+
for (let i = sliceFrom; i < sliceTo; i += step) {
|
|
525
869
|
outV.push(v[i]);
|
|
526
870
|
outP.push(p + '[' + i + ']');
|
|
527
871
|
}
|
|
528
872
|
}
|
|
529
873
|
else {
|
|
530
|
-
|
|
531
|
-
const lower = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
|
|
532
|
-
for (let i = upper; i > lower; i += step) {
|
|
874
|
+
for (let i = sliceFrom; i > sliceTo; i += step) {
|
|
533
875
|
outV.push(v[i]);
|
|
534
876
|
outP.push(p + '[' + i + ']');
|
|
535
877
|
}
|
|
@@ -537,8 +879,11 @@ export function compileSelectorNodeP(sel) {
|
|
|
537
879
|
};
|
|
538
880
|
}
|
|
539
881
|
default: { // 'filter'
|
|
540
|
-
const pred =
|
|
882
|
+
const pred = compileFilterPredicate(sel.expr);
|
|
883
|
+
const reset = filterReset;
|
|
541
884
|
return (v, p, outV, outP, root) => {
|
|
885
|
+
if (reset !== null)
|
|
886
|
+
reset();
|
|
542
887
|
if (Array.isArray(v)) {
|
|
543
888
|
for (let i = 0; i < v.length; i++) {
|
|
544
889
|
if (pred(v[i], root)) {
|