@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.
- package/ARCHITECTURE.md +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
package/src/segments.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
//#region JSONPath segment engine (package-internal)
|
|
2
|
+
// Runtime segment machinery shared by the JSONPath compiler (path.js) and
|
|
3
|
+
// the query engine (query/). Extracted verbatim from path.js, in two
|
|
4
|
+
// installments: values mode first, then the nodes-mode (normalized paths,
|
|
5
|
+
// RFC 9535 section 2.7) compilers - so consumers beyond path.js (the JSLT
|
|
6
|
+
// dispatcher's positional matching) can run selectors producing
|
|
7
|
+
// (value, normalized-path) pairs. This module is package-internal and is
|
|
8
|
+
// deliberately not listed in the package exports.
|
|
9
|
+
|
|
10
|
+
import { equalsJson } from '@jarenjs/core/object';
|
|
11
|
+
import { countCodePoints, compareCodePoints } from '@jarenjs/core/string';
|
|
12
|
+
import { compileIRegexp } from '@jarenjs/core/text/iregexp';
|
|
13
|
+
import {
|
|
14
|
+
CC_TAB,
|
|
15
|
+
CC_LF,
|
|
16
|
+
CC_CR,
|
|
17
|
+
CC_SPACE,
|
|
18
|
+
CC_SQUOTE,
|
|
19
|
+
CC_BACKSLASH,
|
|
20
|
+
} from '@jarenjs/core/scan';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sentinel for the absence of a value ("Nothing" in RFC 9535 terms), as
|
|
24
|
+
* distinct from the JSON value `null`. Re-exported from path.js as
|
|
25
|
+
* `JSONPATH_NOTHING`.
|
|
26
|
+
*/
|
|
27
|
+
export const NOTHING = Symbol('JSONPath.Nothing');
|
|
28
|
+
|
|
29
|
+
const hasOwn = Object.hasOwn;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns true when every segment is a child segment with exactly one
|
|
33
|
+
* name or index selector (a "singular query", RFC 9535 section 2.3.5.1).
|
|
34
|
+
* @param {object[]} segments - Parsed query segments
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function isSingularSegments(segments) {
|
|
38
|
+
for (let i = 0; i < segments.length; i++) {
|
|
39
|
+
const seg = segments[i];
|
|
40
|
+
if (seg.descendant || seg.selectors.length !== 1)
|
|
41
|
+
return false;
|
|
42
|
+
const kind = seg.selectors[0].kind;
|
|
43
|
+
if (kind !== 'name' && kind !== 'index')
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#region filter compilation
|
|
50
|
+
|
|
51
|
+
// structural equality per RFC 9535 section 2.3.5.2.2 (equalsJson),
|
|
52
|
+
// lifted over the JSONPath-specific NOTHING sentinel
|
|
53
|
+
function cmpEquals(a, b) {
|
|
54
|
+
if (a === NOTHING || b === NOTHING)
|
|
55
|
+
return a === b;
|
|
56
|
+
return equalsJson(a, b);
|
|
57
|
+
}
|
|
58
|
+
|
|
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
|
+
function countOwnKeys(obj) {
|
|
70
|
+
let count = 0;
|
|
71
|
+
for (const key in obj) {
|
|
72
|
+
if (hasOwn(obj, key))
|
|
73
|
+
count++;
|
|
74
|
+
}
|
|
75
|
+
return count;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compile a singular query into a direct property walk.
|
|
80
|
+
* @returns {(current: any, root: any) => any} getter returning the value or NOTHING
|
|
81
|
+
*/
|
|
82
|
+
export function compileSingularGetter(segments, relative) {
|
|
83
|
+
// steps: strings are member names, numbers are array indexes
|
|
84
|
+
const steps = new Array(segments.length);
|
|
85
|
+
for (let i = 0; i < segments.length; i++) {
|
|
86
|
+
const sel = segments[i].selectors[0];
|
|
87
|
+
steps[i] = sel.kind === 'name' ? sel.name : sel.index;
|
|
88
|
+
}
|
|
89
|
+
const slen = steps.length;
|
|
90
|
+
return function singularGetter(current, root) {
|
|
91
|
+
let v = relative ? current : root;
|
|
92
|
+
for (let i = 0; i < slen; i++) {
|
|
93
|
+
const step = steps[i];
|
|
94
|
+
if (typeof step === 'string') {
|
|
95
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v) || !hasOwn(v, step))
|
|
96
|
+
return NOTHING;
|
|
97
|
+
v = v[step];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!Array.isArray(v))
|
|
101
|
+
return NOTHING;
|
|
102
|
+
const idx = step < 0 ? v.length + step : step;
|
|
103
|
+
if (idx < 0 || idx >= v.length)
|
|
104
|
+
return NOTHING;
|
|
105
|
+
v = v[idx];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return v;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run a chain of compiled value-mode segment functions over a start value.
|
|
114
|
+
* @returns {any[]} the resulting nodelist as an array of values
|
|
115
|
+
*/
|
|
116
|
+
export function runSegmentsV(segs, start, root) {
|
|
117
|
+
let vals = [start];
|
|
118
|
+
const slen = segs.length;
|
|
119
|
+
for (let i = 0; i < slen; i++) {
|
|
120
|
+
if (vals.length === 0)
|
|
121
|
+
return vals;
|
|
122
|
+
const out = [];
|
|
123
|
+
segs[i](vals, out, root);
|
|
124
|
+
vals = out;
|
|
125
|
+
}
|
|
126
|
+
return vals;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compile an existence test for a filter query; singular queries never
|
|
131
|
+
* materialize nodelists.
|
|
132
|
+
* @returns {(current: any, root: any) => boolean}
|
|
133
|
+
*/
|
|
134
|
+
export function compileExists(query) {
|
|
135
|
+
if (isSingularSegments(query.segments)) {
|
|
136
|
+
const getter = compileSingularGetter(query.segments, query.relative);
|
|
137
|
+
return (current, root) => getter(current, root) !== NOTHING;
|
|
138
|
+
}
|
|
139
|
+
const segs = query.segments.map(compileSegmentV);
|
|
140
|
+
const relative = query.relative;
|
|
141
|
+
return (current, root) => runSegmentsV(segs, relative ? current : root, root).length > 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// getter producing a ValueType result (a JSON value or NOTHING)
|
|
145
|
+
function compileComparable(node) {
|
|
146
|
+
if (node.kind === 'literal') {
|
|
147
|
+
const value = node.value;
|
|
148
|
+
return () => value;
|
|
149
|
+
}
|
|
150
|
+
if (node.kind === 'query')
|
|
151
|
+
return compileSingularGetter(node.query.segments, node.query.relative);
|
|
152
|
+
return compileValueFunction(node);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compileValueFunction(func) {
|
|
156
|
+
switch (func.name) {
|
|
157
|
+
case 'length': {
|
|
158
|
+
const getter = compileComparable(func.args[0]);
|
|
159
|
+
return (current, root) => {
|
|
160
|
+
const v = getter(current, root);
|
|
161
|
+
if (typeof v === 'string')
|
|
162
|
+
return countCodePoints(v);
|
|
163
|
+
if (Array.isArray(v))
|
|
164
|
+
return v.length;
|
|
165
|
+
if (typeof v === 'object' && v !== null)
|
|
166
|
+
return countOwnKeys(v);
|
|
167
|
+
return NOTHING;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
case 'count': {
|
|
171
|
+
const query = func.args[0].query;
|
|
172
|
+
if (isSingularSegments(query.segments)) {
|
|
173
|
+
const getter = compileSingularGetter(query.segments, query.relative);
|
|
174
|
+
return (current, root) => (getter(current, root) === NOTHING ? 0 : 1);
|
|
175
|
+
}
|
|
176
|
+
const segs = query.segments.map(compileSegmentV);
|
|
177
|
+
const relative = query.relative;
|
|
178
|
+
return (current, root) => runSegmentsV(segs, relative ? current : root, root).length;
|
|
179
|
+
}
|
|
180
|
+
case 'value': {
|
|
181
|
+
const query = func.args[0].query;
|
|
182
|
+
if (isSingularSegments(query.segments))
|
|
183
|
+
return compileSingularGetter(query.segments, query.relative);
|
|
184
|
+
const segs = query.segments.map(compileSegmentV);
|
|
185
|
+
const relative = query.relative;
|
|
186
|
+
return (current, root) => {
|
|
187
|
+
const result = runSegmentsV(segs, relative ? current : root, root);
|
|
188
|
+
return result.length === 1 ? result[0] : NOTHING;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/* c8 ignore next 2 -- guarded by the parser's well-typedness checks */
|
|
192
|
+
default:
|
|
193
|
+
throw new Error(`JSONPath: function '${func.name}' does not return ValueType`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function compileRegexTest(func, fullMatch) {
|
|
198
|
+
const inputGet = compileComparable(func.args[0]);
|
|
199
|
+
const patternArg = func.args[1];
|
|
200
|
+
if (patternArg.kind === 'literal') {
|
|
201
|
+
if (typeof patternArg.value !== 'string')
|
|
202
|
+
return () => false;
|
|
203
|
+
const re = compileIRegexp(patternArg.value, fullMatch);
|
|
204
|
+
if (re === null)
|
|
205
|
+
return () => false;
|
|
206
|
+
return (current, root) => {
|
|
207
|
+
const s = inputGet(current, root);
|
|
208
|
+
return typeof s === 'string' && re.test(s);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const patternGet = compileComparable(patternArg);
|
|
212
|
+
// monomorphic per-callsite cache: filters usually see one pattern
|
|
213
|
+
let lastPattern = null;
|
|
214
|
+
let lastRegExp = null;
|
|
215
|
+
return (current, root) => {
|
|
216
|
+
const s = inputGet(current, root);
|
|
217
|
+
if (typeof s !== 'string')
|
|
218
|
+
return false;
|
|
219
|
+
const p = patternGet(current, root);
|
|
220
|
+
if (typeof p !== 'string')
|
|
221
|
+
return false;
|
|
222
|
+
if (p !== lastPattern) {
|
|
223
|
+
lastPattern = p;
|
|
224
|
+
lastRegExp = compileIRegexp(p, fullMatch);
|
|
225
|
+
}
|
|
226
|
+
return lastRegExp !== null && lastRegExp.test(s);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function compileComparison(expr) {
|
|
231
|
+
const left = compileComparable(expr.left);
|
|
232
|
+
const right = compileComparable(expr.right);
|
|
233
|
+
switch (expr.op) {
|
|
234
|
+
case '==':
|
|
235
|
+
return (c, r) => cmpEquals(left(c, r), right(c, r));
|
|
236
|
+
case '!=':
|
|
237
|
+
return (c, r) => !cmpEquals(left(c, r), right(c, r));
|
|
238
|
+
case '<':
|
|
239
|
+
return (c, r) => cmpLess(left(c, r), right(c, r));
|
|
240
|
+
case '>':
|
|
241
|
+
return (c, r) => cmpLess(right(c, r), left(c, r));
|
|
242
|
+
case '<=':
|
|
243
|
+
return (c, r) => {
|
|
244
|
+
const a = left(c, r);
|
|
245
|
+
const b = right(c, r);
|
|
246
|
+
return cmpLess(a, b) || cmpEquals(a, b);
|
|
247
|
+
};
|
|
248
|
+
default: // '>='
|
|
249
|
+
return (c, r) => {
|
|
250
|
+
const a = left(c, r);
|
|
251
|
+
const b = right(c, r);
|
|
252
|
+
return cmpLess(b, a) || cmpEquals(a, b);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Compile a filter logical expression to a predicate.
|
|
259
|
+
* @returns {(current: any, root: any) => boolean}
|
|
260
|
+
*/
|
|
261
|
+
export function compileLogicalExpr(expr) {
|
|
262
|
+
switch (expr.kind) {
|
|
263
|
+
case 'or': {
|
|
264
|
+
const fns = expr.operands.map(compileLogicalExpr);
|
|
265
|
+
const flen = fns.length;
|
|
266
|
+
return (c, r) => {
|
|
267
|
+
for (let i = 0; i < flen; i++) {
|
|
268
|
+
if (fns[i](c, r))
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case 'and': {
|
|
275
|
+
const fns = expr.operands.map(compileLogicalExpr);
|
|
276
|
+
const flen = fns.length;
|
|
277
|
+
return (c, r) => {
|
|
278
|
+
for (let i = 0; i < flen; i++) {
|
|
279
|
+
if (!fns[i](c, r))
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
case 'not': {
|
|
286
|
+
const fn = compileLogicalExpr(expr.operand);
|
|
287
|
+
return (c, r) => !fn(c, r);
|
|
288
|
+
}
|
|
289
|
+
case 'exists':
|
|
290
|
+
return compileExists(expr.query);
|
|
291
|
+
case 'ftest':
|
|
292
|
+
return compileRegexTest(expr.func, expr.func.name === 'match');
|
|
293
|
+
default: // 'cmp'
|
|
294
|
+
return compileComparison(expr);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
|
|
300
|
+
//#region segment compilation (values mode)
|
|
301
|
+
|
|
302
|
+
// selector-node functions: (value, output, root) => void
|
|
303
|
+
|
|
304
|
+
export function compileSelectorNodeV(sel) {
|
|
305
|
+
switch (sel.kind) {
|
|
306
|
+
case 'name': {
|
|
307
|
+
const name = sel.name;
|
|
308
|
+
return (v, out) => {
|
|
309
|
+
if (typeof v === 'object' && v !== null && !Array.isArray(v) && hasOwn(v, name))
|
|
310
|
+
out.push(v[name]);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
case 'index': {
|
|
314
|
+
const index = sel.index;
|
|
315
|
+
if (index >= 0) {
|
|
316
|
+
return (v, out) => {
|
|
317
|
+
if (Array.isArray(v) && index < v.length)
|
|
318
|
+
out.push(v[index]);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return (v, out) => {
|
|
322
|
+
if (Array.isArray(v)) {
|
|
323
|
+
const idx = v.length + index;
|
|
324
|
+
if (idx >= 0)
|
|
325
|
+
out.push(v[idx]);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
case 'wildcard':
|
|
330
|
+
return (v, out) => {
|
|
331
|
+
if (Array.isArray(v)) {
|
|
332
|
+
for (let i = 0; i < v.length; i++)
|
|
333
|
+
out.push(v[i]);
|
|
334
|
+
}
|
|
335
|
+
else if (typeof v === 'object' && v !== null) {
|
|
336
|
+
for (const key in v) {
|
|
337
|
+
if (hasOwn(v, key))
|
|
338
|
+
out.push(v[key]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
case 'slice': {
|
|
343
|
+
const start = sel.start;
|
|
344
|
+
const end = sel.end;
|
|
345
|
+
const step = sel.step === null ? 1 : sel.step;
|
|
346
|
+
if (step === 0)
|
|
347
|
+
return () => { };
|
|
348
|
+
return (v, out) => {
|
|
349
|
+
if (!Array.isArray(v))
|
|
350
|
+
return;
|
|
351
|
+
const len = v.length;
|
|
352
|
+
if (len === 0)
|
|
353
|
+
return;
|
|
354
|
+
// bounds per RFC 9535 section 2.3.4.2.2
|
|
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);
|
|
357
|
+
if (step > 0) {
|
|
358
|
+
const lower = s < 0 ? 0 : (s > len ? len : s);
|
|
359
|
+
const upper = e < 0 ? 0 : (e > len ? len : e);
|
|
360
|
+
for (let i = lower; i < upper; i += step)
|
|
361
|
+
out.push(v[i]);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const upper = s < -1 ? -1 : (s > len - 1 ? len - 1 : s);
|
|
365
|
+
const lower = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
|
|
366
|
+
for (let i = upper; i > lower; i += step)
|
|
367
|
+
out.push(v[i]);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
default: { // 'filter'
|
|
372
|
+
const pred = compileLogicalExpr(sel.expr);
|
|
373
|
+
return (v, out, root) => {
|
|
374
|
+
if (Array.isArray(v)) {
|
|
375
|
+
for (let i = 0; i < v.length; i++) {
|
|
376
|
+
if (pred(v[i], root))
|
|
377
|
+
out.push(v[i]);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (typeof v === 'object' && v !== null) {
|
|
381
|
+
for (const key in v) {
|
|
382
|
+
if (hasOwn(v, key) && pred(v[key], root))
|
|
383
|
+
out.push(v[key]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function descendV(v, output, root, apply) {
|
|
392
|
+
apply(v, output, root);
|
|
393
|
+
if (Array.isArray(v)) {
|
|
394
|
+
for (let i = 0; i < v.length; i++)
|
|
395
|
+
descendV(v[i], output, root, apply);
|
|
396
|
+
}
|
|
397
|
+
else if (typeof v === 'object' && v !== null) {
|
|
398
|
+
for (const key in v) {
|
|
399
|
+
if (hasOwn(v, key))
|
|
400
|
+
descendV(v[key], output, root, apply);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// segment functions: (input, output, root) => void
|
|
406
|
+
export function compileSegmentV(seg) {
|
|
407
|
+
const fns = seg.selectors.map(compileSelectorNodeV);
|
|
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
|
+
};
|
|
414
|
+
if (seg.descendant) {
|
|
415
|
+
return (input, output, root) => {
|
|
416
|
+
for (let i = 0; i < input.length; i++)
|
|
417
|
+
descendV(input[i], output, root, apply);
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return (input, output, root) => {
|
|
421
|
+
for (let i = 0; i < input.length; i++)
|
|
422
|
+
apply(input[i], output, root);
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
|
|
428
|
+
//#region segment compilation (nodes mode, normalized paths)
|
|
429
|
+
// Moved verbatim from path.js: the selector/segment compilers producing
|
|
430
|
+
// (value, normalized-path) pairs per RFC 9535 section 2.7. path.js
|
|
431
|
+
// imports them back for `query.nodes()`/`query.paths()`; nodes mode
|
|
432
|
+
// stays lazily compiled there, so value-only queries never pay for it.
|
|
433
|
+
|
|
434
|
+
// eslint-disable-next-line no-control-regex
|
|
435
|
+
export const RE_NAME_NEEDS_ESCAPE = /['\\\u0000-\u001f]/;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Escape a member name for use inside a normalized path name selector
|
|
439
|
+
* (RFC 9535 section 2.7).
|
|
440
|
+
*/
|
|
441
|
+
export function escapeNormalizedName(name) {
|
|
442
|
+
if (!RE_NAME_NEEDS_ESCAPE.test(name))
|
|
443
|
+
return name;
|
|
444
|
+
let out = '';
|
|
445
|
+
for (let i = 0; i < name.length; i++) {
|
|
446
|
+
const c = name.charCodeAt(i);
|
|
447
|
+
if (c === CC_SQUOTE) out += "\\'";
|
|
448
|
+
else if (c === CC_BACKSLASH) out += '\\\\';
|
|
449
|
+
else if (c === 0x08) out += '\\b';
|
|
450
|
+
else if (c === CC_TAB) out += '\\t';
|
|
451
|
+
else if (c === CC_LF) out += '\\n';
|
|
452
|
+
else if (c === 0x0C) out += '\\f';
|
|
453
|
+
else if (c === CC_CR) out += '\\r';
|
|
454
|
+
else if (c < CC_SPACE) out += '\\u00' + (c < 0x10 ? '0' : '') + c.toString(16);
|
|
455
|
+
else out += name[i];
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function appendName(path, name) {
|
|
461
|
+
return path + "['" + escapeNormalizedName(name) + "']";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// selector-node functions: (value, path, outValues, outPaths, root) => void
|
|
465
|
+
|
|
466
|
+
export function compileSelectorNodeP(sel) {
|
|
467
|
+
switch (sel.kind) {
|
|
468
|
+
case 'name': {
|
|
469
|
+
const name = sel.name;
|
|
470
|
+
const suffix = "['" + escapeNormalizedName(name) + "']";
|
|
471
|
+
return (v, p, outV, outP) => {
|
|
472
|
+
if (typeof v === 'object' && v !== null && !Array.isArray(v) && hasOwn(v, name)) {
|
|
473
|
+
outV.push(v[name]);
|
|
474
|
+
outP.push(p + suffix);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
case 'index': {
|
|
479
|
+
const index = sel.index;
|
|
480
|
+
return (v, p, outV, outP) => {
|
|
481
|
+
if (!Array.isArray(v))
|
|
482
|
+
return;
|
|
483
|
+
const idx = index < 0 ? v.length + index : index;
|
|
484
|
+
if (idx >= 0 && idx < v.length) {
|
|
485
|
+
outV.push(v[idx]);
|
|
486
|
+
outP.push(p + '[' + idx + ']');
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
case 'wildcard':
|
|
491
|
+
return (v, p, outV, outP) => {
|
|
492
|
+
if (Array.isArray(v)) {
|
|
493
|
+
for (let i = 0; i < v.length; i++) {
|
|
494
|
+
outV.push(v[i]);
|
|
495
|
+
outP.push(p + '[' + i + ']');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (typeof v === 'object' && v !== null) {
|
|
499
|
+
for (const key in v) {
|
|
500
|
+
if (hasOwn(v, key)) {
|
|
501
|
+
outV.push(v[key]);
|
|
502
|
+
outP.push(appendName(p, key));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
case 'slice': {
|
|
508
|
+
const start = sel.start;
|
|
509
|
+
const end = sel.end;
|
|
510
|
+
const step = sel.step === null ? 1 : sel.step;
|
|
511
|
+
if (step === 0)
|
|
512
|
+
return () => { };
|
|
513
|
+
return (v, p, outV, outP) => {
|
|
514
|
+
if (!Array.isArray(v))
|
|
515
|
+
return;
|
|
516
|
+
const len = v.length;
|
|
517
|
+
if (len === 0)
|
|
518
|
+
return;
|
|
519
|
+
const s = start === null ? (step > 0 ? 0 : len - 1) : (start < 0 ? len + start : start);
|
|
520
|
+
const e = end === null ? (step > 0 ? len : -1) : (end < 0 ? len + end : end);
|
|
521
|
+
if (step > 0) {
|
|
522
|
+
const lower = s < 0 ? 0 : (s > len ? len : s);
|
|
523
|
+
const upper = e < 0 ? 0 : (e > len ? len : e);
|
|
524
|
+
for (let i = lower; i < upper; i += step) {
|
|
525
|
+
outV.push(v[i]);
|
|
526
|
+
outP.push(p + '[' + i + ']');
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
const upper = s < -1 ? -1 : (s > len - 1 ? len - 1 : s);
|
|
531
|
+
const lower = e < -1 ? -1 : (e > len - 1 ? len - 1 : e);
|
|
532
|
+
for (let i = upper; i > lower; i += step) {
|
|
533
|
+
outV.push(v[i]);
|
|
534
|
+
outP.push(p + '[' + i + ']');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
default: { // 'filter'
|
|
540
|
+
const pred = compileLogicalExpr(sel.expr);
|
|
541
|
+
return (v, p, outV, outP, root) => {
|
|
542
|
+
if (Array.isArray(v)) {
|
|
543
|
+
for (let i = 0; i < v.length; i++) {
|
|
544
|
+
if (pred(v[i], root)) {
|
|
545
|
+
outV.push(v[i]);
|
|
546
|
+
outP.push(p + '[' + i + ']');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else if (typeof v === 'object' && v !== null) {
|
|
551
|
+
for (const key in v) {
|
|
552
|
+
if (hasOwn(v, key) && pred(v[key], root)) {
|
|
553
|
+
outV.push(v[key]);
|
|
554
|
+
outP.push(appendName(p, key));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function descendP(v, p, outV, outP, root, apply) {
|
|
564
|
+
apply(v, p, outV, outP, root);
|
|
565
|
+
if (Array.isArray(v)) {
|
|
566
|
+
for (let i = 0; i < v.length; i++)
|
|
567
|
+
descendP(v[i], p + '[' + i + ']', outV, outP, root, apply);
|
|
568
|
+
}
|
|
569
|
+
else if (typeof v === 'object' && v !== null) {
|
|
570
|
+
for (const key in v) {
|
|
571
|
+
if (hasOwn(v, key))
|
|
572
|
+
descendP(v[key], appendName(p, key), outV, outP, root, apply);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// segment functions: (inValues, inPaths, outValues, outPaths, root) => void
|
|
578
|
+
export function compileSegmentP(seg) {
|
|
579
|
+
const fns = seg.selectors.map(compileSelectorNodeP);
|
|
580
|
+
const apply = fns.length === 1
|
|
581
|
+
? fns[0]
|
|
582
|
+
: (v, p, outV, outP, root) => {
|
|
583
|
+
for (let i = 0; i < fns.length; i++)
|
|
584
|
+
fns[i](v, p, outV, outP, root);
|
|
585
|
+
};
|
|
586
|
+
if (seg.descendant) {
|
|
587
|
+
return (inV, inP, outV, outP, root) => {
|
|
588
|
+
for (let i = 0; i < inV.length; i++)
|
|
589
|
+
descendP(inV[i], inP[i], outV, outP, root, apply);
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return (inV, inP, outV, outP, root) => {
|
|
593
|
+
for (let i = 0; i < inV.length; i++)
|
|
594
|
+
apply(inV[i], inP[i], outV, outP, root);
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Run a chain of compiled nodes-mode segment functions over a start
|
|
600
|
+
* value, threading normalized paths alongside values.
|
|
601
|
+
* @param {Function[]} segs - segment functions from compileSegmentP
|
|
602
|
+
* @param {any} startValue - the value the first segment applies to
|
|
603
|
+
* @param {string} startPath - the base normalized path of startValue
|
|
604
|
+
* (`'$'` for the document root)
|
|
605
|
+
* @param {any} root - the query root (`$` inside embedded filters)
|
|
606
|
+
* @returns {{ vals: any[], paths: string[] }} parallel arrays of the
|
|
607
|
+
* resulting nodelist's values and normalized paths
|
|
608
|
+
*/
|
|
609
|
+
export function runSegmentsP(segs, startValue, startPath, root) {
|
|
610
|
+
let vals = [startValue];
|
|
611
|
+
let paths = [startPath];
|
|
612
|
+
const slen = segs.length;
|
|
613
|
+
for (let i = 0; i < slen; i++) {
|
|
614
|
+
if (vals.length === 0)
|
|
615
|
+
break;
|
|
616
|
+
const outV = [];
|
|
617
|
+
const outP = [];
|
|
618
|
+
segs[i](vals, paths, outV, outP, root);
|
|
619
|
+
vals = outV;
|
|
620
|
+
paths = outP;
|
|
621
|
+
}
|
|
622
|
+
return { vals, paths };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
//#endregion
|
|
626
|
+
|
|
627
|
+
//#endregion
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region XQuery text front-end public API
|
|
2
|
+
// The optional XQuery 3.1 text front-end of the Jaren JSON Query engine:
|
|
3
|
+
// parseXQuery turns text in the supported subset (XQUERY-FRONTEND.md)
|
|
4
|
+
// into a query document, compileXQuery is the parse + compile
|
|
5
|
+
// convenience. The JSON query document is the canonical language
|
|
6
|
+
// (QUERY-FORMAT.md); this module adds a surface syntax, not a second
|
|
7
|
+
// engine.
|
|
8
|
+
|
|
9
|
+
import { compileJsonQuery } from '../query/index.js';
|
|
10
|
+
import { parseXQuery } from './parse.js';
|
|
11
|
+
|
|
12
|
+
export { parseXQuery, XQuerySyntaxError } from './parse.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse XQuery text and compile the resulting query document in one call.
|
|
16
|
+
* Returns the engine's compiled query function (see `compileJsonQuery`):
|
|
17
|
+
* `query(data, externals?)` plus the `first`/`exists`/`externals`/`doc`
|
|
18
|
+
* helpers - `doc` holds the emitted query document.
|
|
19
|
+
* @param {string} text - XQuery text in the supported subset
|
|
20
|
+
* @param {object} [options] - passed through to `compileJsonQuery`
|
|
21
|
+
* @returns {function} the compiled query function
|
|
22
|
+
* @throws {XQuerySyntaxError} when the text is invalid or outside the subset
|
|
23
|
+
* @throws {import('../query/errors.js').JsonQueryCompileError} when the
|
|
24
|
+
* emitted document is rejected by the compiler (e.g. duplicate variable
|
|
25
|
+
* bindings, JQ0007)
|
|
26
|
+
* @example
|
|
27
|
+
* const q = compileXQuery('for $b in $doc?store?book?* where $b?price lt 10 return $b?title');
|
|
28
|
+
* q.externals; // ['doc']
|
|
29
|
+
* q(null, { doc: data }); // ['Sayings of the Century', 'Moby Dick']
|
|
30
|
+
*/
|
|
31
|
+
export function compileXQuery(text, options) {
|
|
32
|
+
return compileJsonQuery(parseXQuery(text), options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|