jtlt 0.1.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/.editorconfig +16 -0
- package/CHANGES.md +5 -0
- package/LICENSE-MIT.txt +21 -0
- package/README.md +534 -0
- package/dist/AbstractJoiningTransformer.d.ts +42 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
- package/dist/DOMJoiningTransformer.d.ts +113 -0
- package/dist/DOMJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONJoiningTransformer.d.ts +160 -0
- package/dist/JSONJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformer.d.ts +95 -0
- package/dist/JSONPathTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformerContext.d.ts +263 -0
- package/dist/JSONPathTransformerContext.d.ts.map +1 -0
- package/dist/StringJoiningTransformer.d.ts +168 -0
- package/dist/StringJoiningTransformer.d.ts.map +1 -0
- package/dist/XPathTransformer.d.ts +51 -0
- package/dist/XPathTransformer.d.ts.map +1 -0
- package/dist/XPathTransformerContext.d.ts +260 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.d.ts.map +1 -0
- package/docs/API.expanded.md +263 -0
- package/docs/API.md +69 -0
- package/eslint.config.js +30 -0
- package/package.json +53 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/AbstractJoiningTransformer.js +73 -0
- package/src/DOMJoiningTransformer.js +237 -0
- package/src/JSONJoiningTransformer.js +472 -0
- package/src/JSONPathTransformer.js +159 -0
- package/src/JSONPathTransformerContext.js +807 -0
- package/src/StringJoiningTransformer.js +589 -0
- package/src/XPathTransformer.js +94 -0
- package/src/XPathTransformerContext.js +496 -0
- package/src/XSLTStyleJSONPathResolver.js +39 -0
- package/src/index.js +299 -0
- package/src/types/xpath2-js.d.ts +2 -0
- package/tsconfig-prod.json +19 -0
- package/tsconfig.json +14 -0
- package/typings/xpath2-js.d.ts +2 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import {JSONPath as jsonpath} from 'jsonpath-plus';
|
|
2
|
+
import JSONPathTransformer from './JSONPathTransformer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execution context for JSONPath-driven template application.
|
|
6
|
+
*
|
|
7
|
+
* Holds the current node, parent, path, variables, and property sets while
|
|
8
|
+
* running templates. Exposes helper methods that mirror the underlying
|
|
9
|
+
* joining transformer (e.g., string(), object(), array()) so templates can
|
|
10
|
+
* emit results without referencing the joiner directly.
|
|
11
|
+
*/
|
|
12
|
+
class JSONPathTransformerContext {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} config - Configuration object
|
|
15
|
+
* @param {object} config.data - Data to transform
|
|
16
|
+
* @param {object} [config.parent] - Parent object
|
|
17
|
+
* @param {string} [config.parentProperty] - Parent property name
|
|
18
|
+
* @param {boolean} [config.errorOnEqualPriority] - Whether to error on
|
|
19
|
+
* equal priority
|
|
20
|
+
* @param {{append: Function, get: Function, string: Function,
|
|
21
|
+
* object: Function, array: Function}} config.joiningTransformer -
|
|
22
|
+
* Joining transformer
|
|
23
|
+
* @param {boolean} [config.preventEval] - Whether to prevent eval in
|
|
24
|
+
* JSONPath
|
|
25
|
+
* @param {Function} [config.specificityPriorityResolver] - Function to
|
|
26
|
+
* resolve priority
|
|
27
|
+
* @param {any[]} templates - Array of template objects
|
|
28
|
+
*/
|
|
29
|
+
constructor (config, templates) {
|
|
30
|
+
this._config = config;
|
|
31
|
+
this._templates = templates;
|
|
32
|
+
this._contextObj = this._origObj = config.data;
|
|
33
|
+
this._parent = config.parent || this._config;
|
|
34
|
+
this._parentProperty = config.parentProperty || 'data';
|
|
35
|
+
/** @type {Record<string, any>} */
|
|
36
|
+
this.vars = {};
|
|
37
|
+
/** @type {Record<string, any>} */
|
|
38
|
+
this.propertySets = {};
|
|
39
|
+
/** @type {Record<string, any>} */
|
|
40
|
+
this.keys = {};
|
|
41
|
+
/** @type {boolean | undefined} */
|
|
42
|
+
this._initialized = undefined;
|
|
43
|
+
/** @type {string | undefined} */
|
|
44
|
+
this._currPath = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Triggers an error if equal priority templates are found.
|
|
49
|
+
* @returns {void}
|
|
50
|
+
*/
|
|
51
|
+
_triggerEqualPriorityError () {
|
|
52
|
+
if (this._config.errorOnEqualPriority) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'You have configured JSONPathTransformer to throw errors on ' +
|
|
55
|
+
'finding templates of equal priority and these have been found.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gets the joining transformer from config.
|
|
62
|
+
* @returns {any} The joining transformer
|
|
63
|
+
*/
|
|
64
|
+
_getJoiningTransformer () {
|
|
65
|
+
return this._config.joiningTransformer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {*} item - Item to append to output
|
|
70
|
+
* @returns {JSONPathTransformerContext}
|
|
71
|
+
*/
|
|
72
|
+
appendOutput (item) {
|
|
73
|
+
/** @type {any} */ (this._getJoiningTransformer()).append(item);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* c8 ignore next 4 -- JSDoc block incorrectly counted as coverable by c8 */
|
|
78
|
+
/**
|
|
79
|
+
* Gets the current output.
|
|
80
|
+
* @returns {*} The output from the joining transformer
|
|
81
|
+
*/
|
|
82
|
+
getOutput () {
|
|
83
|
+
return /** @type {any} */ (this._getJoiningTransformer()).get();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get() and set() are provided as a convenience method for templates, but
|
|
88
|
+
* it should typically not be used (use valueOf or the copy methods to add
|
|
89
|
+
* to the result tree instead).
|
|
90
|
+
* @param {string} select - JSONPath selector
|
|
91
|
+
* @param {boolean} wrap - Whether to wrap results
|
|
92
|
+
* @returns {*} The selected value(s)
|
|
93
|
+
*/
|
|
94
|
+
get (select, wrap) {
|
|
95
|
+
if (select) {
|
|
96
|
+
return /** @type {any} */ (jsonpath)({
|
|
97
|
+
path: select, json: this._contextObj,
|
|
98
|
+
preventEval: this._config.preventEval,
|
|
99
|
+
wrap: wrap || false, returnType: 'value'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return this._contextObj;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {*} v - Value to set
|
|
107
|
+
* @returns {JSONPathTransformerContext}
|
|
108
|
+
*/
|
|
109
|
+
set (v) {
|
|
110
|
+
(/** @type {Record<string, any>} */ (this._parent))[
|
|
111
|
+
this._parentProperty
|
|
112
|
+
] = v;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Apply matching templates to nodes selected by JSONPath, optionally sorted.
|
|
118
|
+
*
|
|
119
|
+
* Sort parameter forms:
|
|
120
|
+
* - string: JSONPath relative to each match (e.g., '$.name' or '@')
|
|
121
|
+
* - function: comparator (aValue, bValue, ctx) => number
|
|
122
|
+
* - object: { select, order='ascending'|'descending', type='text'|'number',
|
|
123
|
+
* locale, localeOptions }
|
|
124
|
+
* - array: multiple key objects/strings in priority order.
|
|
125
|
+
*
|
|
126
|
+
* @param {string|object} select - JSONPath selector or options object
|
|
127
|
+
* @param {string} [mode] - Mode to apply
|
|
128
|
+
* @param {string|Function|object|Array<string|object>} [sort] - Sort spec
|
|
129
|
+
* @returns {JSONPathTransformerContext}
|
|
130
|
+
*/
|
|
131
|
+
applyTemplates (select, mode, sort) {
|
|
132
|
+
// Matches templates by (path, mode), resolves priority, and invokes each
|
|
133
|
+
// template in document order (or sorted order) for all nodes selected by
|
|
134
|
+
// `select`.
|
|
135
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
136
|
+
const that = this;
|
|
137
|
+
if (select && typeof select === 'object') {
|
|
138
|
+
/** @type {{mode?: string, select?: string}} */
|
|
139
|
+
const selectObj = /** @type {any} */ (select);
|
|
140
|
+
mode = selectObj.mode ?? mode;
|
|
141
|
+
select = selectObj.select ?? select;
|
|
142
|
+
}
|
|
143
|
+
if (!this._initialized) {
|
|
144
|
+
select = select || '$';
|
|
145
|
+
this._currPath = '$';
|
|
146
|
+
this._initialized = true;
|
|
147
|
+
} else {
|
|
148
|
+
select = select || '*';
|
|
149
|
+
}
|
|
150
|
+
// Preserve original selector to support special suffixes (e.g., "$~")
|
|
151
|
+
const originalSelect = /** @type {string} */ (select);
|
|
152
|
+
select = JSONPathTransformer.makeJSONPathAbsolute(originalSelect);
|
|
153
|
+
const propertyNamesMode = select.endsWith('~');
|
|
154
|
+
const jsonPathExpr = propertyNamesMode ? select.slice(0, -1) : select;
|
|
155
|
+
// Todo: Use results here?
|
|
156
|
+
/* const results = */ this._getJoiningTransformer();
|
|
157
|
+
const modeMatchedTemplates = this._templates.filter(function (templateObj) {
|
|
158
|
+
return ((mode && mode === templateObj.mode) ||
|
|
159
|
+
(!mode && !templateObj.mode));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Collect matches first (to allow sorting), then process
|
|
163
|
+
/**
|
|
164
|
+
* @type {{
|
|
165
|
+
* value:any, parent:any, parentProperty?:string, path:string
|
|
166
|
+
* }[]}
|
|
167
|
+
*/
|
|
168
|
+
const matches = /** @type {any} */ (jsonpath)({
|
|
169
|
+
path: jsonPathExpr,
|
|
170
|
+
resultType: 'all',
|
|
171
|
+
wrap: true,
|
|
172
|
+
json: this._contextObj,
|
|
173
|
+
preventEval: this._config.preventEval
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Sorting utilities
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} expr
|
|
179
|
+
* @param {any} ctxVal
|
|
180
|
+
* @returns {any}
|
|
181
|
+
*/
|
|
182
|
+
function evalInContext (expr, ctxVal) {
|
|
183
|
+
if (!expr) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (expr === '.' || expr === '@') {
|
|
187
|
+
return ctxVal;
|
|
188
|
+
}
|
|
189
|
+
return /** @type {any} */ (jsonpath)({
|
|
190
|
+
path: expr,
|
|
191
|
+
json: ctxVal,
|
|
192
|
+
preventEval: that._config.preventEval,
|
|
193
|
+
wrap: false, returnType: 'value'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* @param {any} aVal
|
|
198
|
+
* @param {any} bVal
|
|
199
|
+
* @param {{
|
|
200
|
+
* order?: 'ascending'|'descending', type?: 'text'|'number',
|
|
201
|
+
* locale?: string, localeOptions?: any
|
|
202
|
+
* }|undefined} spec
|
|
203
|
+
* @returns {number}
|
|
204
|
+
*/
|
|
205
|
+
function compareBySpec (aVal, bVal, spec) {
|
|
206
|
+
const order = (spec && spec.order === 'descending') ? -1 : 1;
|
|
207
|
+
const type = (spec && spec.type) || 'text';
|
|
208
|
+
if (type === 'number') {
|
|
209
|
+
const an = Number(aVal);
|
|
210
|
+
const bn = Number(bVal);
|
|
211
|
+
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (Number.isNaN(an)) {
|
|
215
|
+
return Number(order);
|
|
216
|
+
}
|
|
217
|
+
if (Number.isNaN(bn)) {
|
|
218
|
+
return -1 * order;
|
|
219
|
+
}
|
|
220
|
+
return (an - bn) * order;
|
|
221
|
+
}
|
|
222
|
+
// text
|
|
223
|
+
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
224
|
+
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
225
|
+
if (spec && spec.locale) {
|
|
226
|
+
return aStr.localeCompare(
|
|
227
|
+
bStr, spec.locale, spec.localeOptions
|
|
228
|
+
) * order;
|
|
229
|
+
}
|
|
230
|
+
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* @param {any} sortSpec
|
|
234
|
+
* @returns {((a:{value:any}, b:{value:any})=>number)|null}
|
|
235
|
+
*/
|
|
236
|
+
function buildComparator (sortSpec) {
|
|
237
|
+
if (!sortSpec) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (typeof sortSpec === 'function') {
|
|
241
|
+
return function (a, b) {
|
|
242
|
+
return sortSpec(a.value, b.value, that);
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const specs = Array.isArray(sortSpec) ? sortSpec : [sortSpec];
|
|
246
|
+
return function (a, b) {
|
|
247
|
+
for (const s of specs) {
|
|
248
|
+
if (typeof s === 'string') {
|
|
249
|
+
const av = evalInContext(s, a.value);
|
|
250
|
+
const bv = evalInContext(s, b.value);
|
|
251
|
+
const c = compareBySpec(av, bv, {type: 'text', order: 'ascending'});
|
|
252
|
+
if (c !== 0) {
|
|
253
|
+
return c;
|
|
254
|
+
}
|
|
255
|
+
} else if (s && typeof s === 'object') {
|
|
256
|
+
const av = evalInContext(s.select, a.value);
|
|
257
|
+
const bv = evalInContext(s.select, b.value);
|
|
258
|
+
const c = compareBySpec(av, bv, s);
|
|
259
|
+
if (c !== 0) {
|
|
260
|
+
return c;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const comparator = buildComparator(sort);
|
|
269
|
+
if (comparator) {
|
|
270
|
+
matches.sort(comparator);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Preserve outer context across processing
|
|
274
|
+
const prevContext = that._contextObj;
|
|
275
|
+
const prevParent = that._parent;
|
|
276
|
+
const prevParentProp = that._parentProperty;
|
|
277
|
+
const prevCurrPath = that._currPath;
|
|
278
|
+
|
|
279
|
+
// Process in (sorted) order
|
|
280
|
+
for (const o of matches) {
|
|
281
|
+
const {value, parent, parentProperty, path} = o;
|
|
282
|
+
const _oldPath = that._currPath;
|
|
283
|
+
that._currPath += path.replace(/^\$/v, '');
|
|
284
|
+
const pathMatchedTemplates = modeMatchedTemplates.filter(
|
|
285
|
+
function (templateObj) {
|
|
286
|
+
const queryResult = /** @type {any[]} */ (
|
|
287
|
+
(/** @type {any} */ (jsonpath))({
|
|
288
|
+
path: JSONPathTransformer.makeJSONPathAbsolute(
|
|
289
|
+
templateObj.path
|
|
290
|
+
),
|
|
291
|
+
json: that._origObj,
|
|
292
|
+
resultType: 'path',
|
|
293
|
+
preventEval: that._config.preventEval,
|
|
294
|
+
wrap: true
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
return (
|
|
298
|
+
/** @type {any[]} */ (queryResult)
|
|
299
|
+
).includes(that._currPath);
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
let templateObj;
|
|
304
|
+
if (!pathMatchedTemplates.length) {
|
|
305
|
+
const dtr = JSONPathTransformer.DefaultTemplateRules;
|
|
306
|
+
if (propertyNamesMode) {
|
|
307
|
+
templateObj = dtr.transformPropertyNames;
|
|
308
|
+
} else if (Array.isArray(value)) {
|
|
309
|
+
templateObj = dtr.transformArrays;
|
|
310
|
+
} else if (value && typeof value === 'object') {
|
|
311
|
+
templateObj = dtr.transformObjects;
|
|
312
|
+
} else if (value && typeof value === 'function') {
|
|
313
|
+
templateObj = dtr.transformFunctions;
|
|
314
|
+
} else {
|
|
315
|
+
templateObj = dtr.transformScalars;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
pathMatchedTemplates.sort(function (a, b) {
|
|
319
|
+
/* c8 ignore start -- Fallback to priority 0 when no numeric priority
|
|
320
|
+
* and no specificityPriorityResolver is extremely rare in practice.
|
|
321
|
+
* Requires: template without priority property AND no resolver AND
|
|
322
|
+
* multiple templates matching same path. Equal priorities then
|
|
323
|
+
* trigger error, making the `: 0` branch nearly unreachable. */
|
|
324
|
+
const aPriority = typeof a.priority === 'number'
|
|
325
|
+
? a.priority
|
|
326
|
+
: (that._config.specificityPriorityResolver
|
|
327
|
+
? that._config.specificityPriorityResolver(a.path)
|
|
328
|
+
: 0);
|
|
329
|
+
const bPriority = typeof b.priority === 'number'
|
|
330
|
+
? b.priority
|
|
331
|
+
: (that._config.specificityPriorityResolver
|
|
332
|
+
? that._config.specificityPriorityResolver(b.path)
|
|
333
|
+
: 0);
|
|
334
|
+
/* c8 ignore stop */
|
|
335
|
+
|
|
336
|
+
if (aPriority === bPriority) {
|
|
337
|
+
that._triggerEqualPriorityError();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (aPriority > bPriority) ? -1 : 1;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
templateObj = pathMatchedTemplates.shift();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
that._contextObj = value;
|
|
347
|
+
that._parent = parent;
|
|
348
|
+
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
349
|
+
|
|
350
|
+
const ret = templateObj.template.call(
|
|
351
|
+
that, value, {mode, parent, parentProperty}
|
|
352
|
+
);
|
|
353
|
+
if (typeof ret !== 'undefined') {
|
|
354
|
+
that._getJoiningTransformer().append(ret);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
that._contextObj = value;
|
|
358
|
+
that._parent = parent;
|
|
359
|
+
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
360
|
+
that._currPath = _oldPath;
|
|
361
|
+
}
|
|
362
|
+
// Restore outer context
|
|
363
|
+
that._contextObj = prevContext;
|
|
364
|
+
that._parent = prevParent;
|
|
365
|
+
that._parentProperty = prevParentProp;
|
|
366
|
+
that._currPath = prevCurrPath;
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {string|object} name - Template name or options object
|
|
372
|
+
* @param {any[]} [withParams] - Parameters to pass to template
|
|
373
|
+
* @returns {JSONPathTransformerContext}
|
|
374
|
+
*/
|
|
375
|
+
callTemplate (name, withParams) {
|
|
376
|
+
// Invokes a named template, optionally passing values via withParam.
|
|
377
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
378
|
+
const that = this;
|
|
379
|
+
if (name && typeof name === 'object') {
|
|
380
|
+
/** @type {{name?: string, withParam?: any[]}} */
|
|
381
|
+
const nameObj = /** @type {any} */ (name);
|
|
382
|
+
withParams = nameObj.withParam || withParams;
|
|
383
|
+
name = nameObj.name ?? name;
|
|
384
|
+
}
|
|
385
|
+
withParams = withParams || [];
|
|
386
|
+
const paramValues = withParams.map(function (withParam) {
|
|
387
|
+
return withParam.value || that.get(withParam.select, false);
|
|
388
|
+
});
|
|
389
|
+
const results = this._getJoiningTransformer();
|
|
390
|
+
const templateObj = this._templates.find(function (template) {
|
|
391
|
+
return template.name === name;
|
|
392
|
+
});
|
|
393
|
+
if (!templateObj) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
'Template, ' + name + ', cannot be called as it was not found.'
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = templateObj.template.apply(this, paramValues);
|
|
400
|
+
/** @type {any} */ (results).append(result);
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Iterate over values selected by JSONPath, optionally sorted.
|
|
406
|
+
*
|
|
407
|
+
* Sort parameter forms are the same as applyTemplates().
|
|
408
|
+
* @param {string} select - JSONPath selector
|
|
409
|
+
* @param {Function} cb - Callback function
|
|
410
|
+
* @param {string|Function|object|Array<string|object>} [sort] - Sort spec
|
|
411
|
+
* @returns {JSONPathTransformerContext}
|
|
412
|
+
*/
|
|
413
|
+
forEach (select, cb, sort) {
|
|
414
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
415
|
+
const that = this;
|
|
416
|
+
/** @type {{value:any}[]} */
|
|
417
|
+
const matches = /** @type {any} */ (jsonpath)({
|
|
418
|
+
path: select,
|
|
419
|
+
json: this._contextObj,
|
|
420
|
+
preventEval: this._config.preventEval,
|
|
421
|
+
wrap: true,
|
|
422
|
+
resultType: 'all'
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {string} expr
|
|
427
|
+
* @param {any} ctxVal
|
|
428
|
+
* @returns {any}
|
|
429
|
+
*/
|
|
430
|
+
function feEvalInContext (expr, ctxVal) {
|
|
431
|
+
if (!expr) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
if (expr === '.' || expr === '@') {
|
|
435
|
+
return ctxVal;
|
|
436
|
+
}
|
|
437
|
+
return /** @type {any} */ (jsonpath)({
|
|
438
|
+
path: expr,
|
|
439
|
+
json: ctxVal,
|
|
440
|
+
preventEval: that._config.preventEval,
|
|
441
|
+
wrap: false, returnType: 'value'
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* @param {any} aVal
|
|
446
|
+
* @param {any} bVal
|
|
447
|
+
* @param {{
|
|
448
|
+
* order?: 'ascending'|'descending', type?: 'text'|'number',
|
|
449
|
+
* locale?: string, localeOptions?: any
|
|
450
|
+
* }|undefined} spec
|
|
451
|
+
* @returns {number}
|
|
452
|
+
*/
|
|
453
|
+
function feCompareBySpec (aVal, bVal, spec) {
|
|
454
|
+
const order = (spec && spec.order === 'descending') ? -1 : 1;
|
|
455
|
+
const type = (spec && spec.type) || 'text';
|
|
456
|
+
if (type === 'number') {
|
|
457
|
+
const an = Number(aVal);
|
|
458
|
+
const bn = Number(bVal);
|
|
459
|
+
if (Number.isNaN(an) && Number.isNaN(bn)) {
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
if (Number.isNaN(an)) {
|
|
463
|
+
return Number(order);
|
|
464
|
+
}
|
|
465
|
+
if (Number.isNaN(bn)) {
|
|
466
|
+
return -1 * order;
|
|
467
|
+
}
|
|
468
|
+
return (an - bn) * order;
|
|
469
|
+
}
|
|
470
|
+
const aStr = aVal === null || aVal === undefined ? '' : String(aVal);
|
|
471
|
+
const bStr = bVal === null || bVal === undefined ? '' : String(bVal);
|
|
472
|
+
if (spec && spec.locale) {
|
|
473
|
+
return aStr.localeCompare(
|
|
474
|
+
bStr, spec.locale, spec.localeOptions
|
|
475
|
+
) * order;
|
|
476
|
+
}
|
|
477
|
+
return (aStr < bStr ? -1 : (aStr > bStr ? 1 : 0)) * order;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* @param {any} sortSpec
|
|
481
|
+
* @returns {((a:{value:any}, b:{value:any})=>number)|null}
|
|
482
|
+
*/
|
|
483
|
+
function feBuildComparator (sortSpec) {
|
|
484
|
+
if (!sortSpec) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
if (typeof sortSpec === 'function') {
|
|
488
|
+
return function (a, b) {
|
|
489
|
+
return sortSpec(a.value, b.value, that);
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const specs = Array.isArray(sortSpec) ? sortSpec : [sortSpec];
|
|
493
|
+
return function (a, b) {
|
|
494
|
+
for (const s of specs) {
|
|
495
|
+
if (typeof s === 'string') {
|
|
496
|
+
const av = feEvalInContext(s, a.value);
|
|
497
|
+
const bv = feEvalInContext(s, b.value);
|
|
498
|
+
const c = feCompareBySpec(
|
|
499
|
+
av, bv, {type: 'text', order: 'ascending'}
|
|
500
|
+
);
|
|
501
|
+
if (c !== 0) {
|
|
502
|
+
return c;
|
|
503
|
+
}
|
|
504
|
+
} else if (s && typeof s === 'object') {
|
|
505
|
+
const av = feEvalInContext(s.select, a.value);
|
|
506
|
+
const bv = feEvalInContext(s.select, b.value);
|
|
507
|
+
const c = feCompareBySpec(av, bv, s);
|
|
508
|
+
if (c !== 0) {
|
|
509
|
+
return c;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return 0;
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const comparator = feBuildComparator(sort);
|
|
518
|
+
const list = comparator ? [...matches].toSorted(comparator) : matches;
|
|
519
|
+
for (const m of list) {
|
|
520
|
+
cb.call(that, m.value);
|
|
521
|
+
}
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* @param {string|object} [select] - JSONPath selector
|
|
527
|
+
* @returns {JSONPathTransformerContext}
|
|
528
|
+
*/
|
|
529
|
+
valueOf (select) {
|
|
530
|
+
// Appends the value of the given JSONPath (or the current context when
|
|
531
|
+
// `{select: '.'}` is passed) to the output via the joining transformer.
|
|
532
|
+
const results = this._getJoiningTransformer();
|
|
533
|
+
const result = select && typeof select === 'object' &&
|
|
534
|
+
/** @type {{select?: string}} */ (select).select === '.'
|
|
535
|
+
? this._contextObj
|
|
536
|
+
: this.get(/** @type {string} */ (select), false);
|
|
537
|
+
/** @type {any} */ (results).append(result);
|
|
538
|
+
return this;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Deep copy selection or current context when omitted.
|
|
543
|
+
* @param {string} [select] - JSONPath selector
|
|
544
|
+
* @returns {JSONPathTransformerContext}
|
|
545
|
+
*/
|
|
546
|
+
copyOf (select) { // Deep
|
|
547
|
+
// Deeply clones the value at `select` (or current context if omitted)
|
|
548
|
+
// and appends the clone to output. Cycles supported if structuredClone
|
|
549
|
+
// available; otherwise falls back to JSON serialization (dropping
|
|
550
|
+
// functions/undefined).
|
|
551
|
+
const val = select ? this.get(select, false) : this._contextObj;
|
|
552
|
+
// If JSONPath returned array of matches (wrap true not used here), we
|
|
553
|
+
// just copy the raw value which should be scalar/object/array/function.
|
|
554
|
+
// For functions or non-serializable values, structuredClone may throw.
|
|
555
|
+
/** @type {any} */ let clone;
|
|
556
|
+
if (val && typeof val === 'object') {
|
|
557
|
+
/* c8 ignore try -- structuredClone existence depends on runtime */
|
|
558
|
+
try {
|
|
559
|
+
// Prefer native structuredClone when available.
|
|
560
|
+
clone = typeof structuredClone === 'function'
|
|
561
|
+
? structuredClone(val)
|
|
562
|
+
// Fall back to a (potentially shallow) spread clone when deep
|
|
563
|
+
// cloning utility unavailable; better than lossy JSON stringify.
|
|
564
|
+
: (Array.isArray(val) ? [...val] : {...val});
|
|
565
|
+
} catch {
|
|
566
|
+
/* c8 ignore start -- structuredClone error fallback attribution can
|
|
567
|
+
* vary across environments; behavior covered by tests. */
|
|
568
|
+
// structuredClone failed (e.g., Symbols); if any functions present
|
|
569
|
+
// on own enumerable string-keyed properties, preserve via shallow.
|
|
570
|
+
/** @type {boolean} */ let hasFunc = false;
|
|
571
|
+
for (const k of Object.keys(val)) {
|
|
572
|
+
const v = /** @type {any} */ (val)[k];
|
|
573
|
+
if (typeof v === 'function') {
|
|
574
|
+
hasFunc = true;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// For non-functions, attempting structuredClone again would rethrow;
|
|
579
|
+
// use shallow clone to retain non-serializable props like Symbols.
|
|
580
|
+
clone = Array.isArray(val) ? [...val] : {...val};
|
|
581
|
+
/* c8 ignore stop */
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
// Primitives/functions copied by value/reference semantics naturally.
|
|
585
|
+
clone = val;
|
|
586
|
+
}
|
|
587
|
+
/** @type {any} */ (this._getJoiningTransformer()).append(clone);
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Shallow copy current context; optionally merge property set names.
|
|
593
|
+
* @param {string[]} [propertySets] - Property sets to merge
|
|
594
|
+
* @returns {JSONPathTransformerContext}
|
|
595
|
+
*/
|
|
596
|
+
copy (propertySets) { // Shallow
|
|
597
|
+
// Creates a shallow clone of current context object/array (or primitive)
|
|
598
|
+
// and appends it. If `propertySets` is an array of names, merges those
|
|
599
|
+
// named property sets (if found) into the top-level shallow copy.
|
|
600
|
+
const src = this._contextObj;
|
|
601
|
+
/** @type {any} */ let clone;
|
|
602
|
+
if (src && typeof src === 'object') {
|
|
603
|
+
clone = Array.isArray(src) ? [...src] : {...src};
|
|
604
|
+
if (Array.isArray(propertySets)) {
|
|
605
|
+
for (const ps of propertySets) {
|
|
606
|
+
if (this.propertySets[ps]) {
|
|
607
|
+
Object.assign(clone, this.propertySets[ps]);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} else { /* c8 ignore start -- primitive branch attribution variance */
|
|
612
|
+
clone = src; // Primitive/function - nothing to shallow clone
|
|
613
|
+
} /* c8 ignore stop */
|
|
614
|
+
/** @type {any} */ (this._getJoiningTransformer()).append(clone);
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* @param {string} name - Variable name
|
|
620
|
+
* @param {string} select - JSONPath selector
|
|
621
|
+
* @returns {JSONPathTransformerContext}
|
|
622
|
+
*/
|
|
623
|
+
variable (name, select) {
|
|
624
|
+
this.vars[name] = this.get(select, false);
|
|
625
|
+
return this;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @param {*} json - JSON data to log
|
|
630
|
+
* @returns {void}
|
|
631
|
+
*/
|
|
632
|
+
// eslint-disable-next-line class-methods-use-this -- Convenient
|
|
633
|
+
message (json) {
|
|
634
|
+
// eslint-disable-next-line no-console -- Ok
|
|
635
|
+
console.log(json);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* @param {string} str - String value
|
|
640
|
+
* @param {Function} cb - Callback function
|
|
641
|
+
* @returns {JSONPathTransformerContext}
|
|
642
|
+
*/
|
|
643
|
+
// Todo: Add other methods from the joining transformers
|
|
644
|
+
string (str, cb) {
|
|
645
|
+
/** @type {any} */ (this._getJoiningTransformer()).string(str, cb);
|
|
646
|
+
return this;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Append a number to JSON output. Mirrors the joining transformer API so
|
|
651
|
+
* templates can call `this.number()`.
|
|
652
|
+
* @param {number} num - Number value to append
|
|
653
|
+
* @returns {JSONPathTransformerContext}
|
|
654
|
+
*/
|
|
655
|
+
number (num) {
|
|
656
|
+
/** @type {any} */ (this._getJoiningTransformer()).number(num);
|
|
657
|
+
return this;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Append plain text directly to the output without escaping or JSON
|
|
662
|
+
* stringification. Mirrors the joining transformer API so templates can
|
|
663
|
+
* call `this.plainText()`.
|
|
664
|
+
* @param {string} str - Plain text to append
|
|
665
|
+
* @returns {JSONPathTransformerContext}
|
|
666
|
+
*/
|
|
667
|
+
plainText (str) {
|
|
668
|
+
/** @type {any} */ (this._getJoiningTransformer()).plainText(str);
|
|
669
|
+
return this;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Set a property value on the current object (JSON joiner). Mirrors the
|
|
674
|
+
* joining transformer API so templates can call `this.propValue()`.
|
|
675
|
+
* @param {string} prop - Property name
|
|
676
|
+
* @param {*} val - Property value
|
|
677
|
+
* @returns {JSONPathTransformerContext}
|
|
678
|
+
*/
|
|
679
|
+
propValue (prop, val) {
|
|
680
|
+
/** @type {any} */ (this._getJoiningTransformer()).propValue(prop, val);
|
|
681
|
+
return this;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Build an object. Mirrors the joining transformer API. All joiners now
|
|
686
|
+
* support both signatures: (obj, cb, usePropertySets, propSets) with seed
|
|
687
|
+
* object or (cb, usePropertySets, propSets) without.
|
|
688
|
+
* @param {...any} args - Arguments to pass to joiner
|
|
689
|
+
* @returns {JSONPathTransformerContext}
|
|
690
|
+
*/
|
|
691
|
+
object (...args) {
|
|
692
|
+
/** @type {any} */ (this._getJoiningTransformer()).object(...args);
|
|
693
|
+
return this;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Build an array. Mirrors the joining transformer API. All joiners now
|
|
698
|
+
* support both signatures: (arr, cb) with seed array or (cb) without.
|
|
699
|
+
* @param {...any} args - Arguments to pass to joiner
|
|
700
|
+
* @returns {JSONPathTransformerContext}
|
|
701
|
+
*/
|
|
702
|
+
array (...args) {
|
|
703
|
+
/** @type {any} */ (this._getJoiningTransformer()).array(...args);
|
|
704
|
+
return this;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Create an element. Mirrors the joining transformer API so templates can
|
|
709
|
+
* call `this.element()`.
|
|
710
|
+
* @param {string} name - Element name
|
|
711
|
+
* @param {object} [atts] - Attributes object
|
|
712
|
+
* @param {any[]} [children] - Child nodes
|
|
713
|
+
* @param {Function} [cb] - Callback function
|
|
714
|
+
* @returns {JSONPathTransformerContext}
|
|
715
|
+
*/
|
|
716
|
+
element (name, atts, children, cb) {
|
|
717
|
+
/** @type {any} */ (this._getJoiningTransformer()).element(
|
|
718
|
+
name, atts, children, cb
|
|
719
|
+
);
|
|
720
|
+
return this;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Add an attribute to the most recently opened element. Mirrors the joining
|
|
725
|
+
* transformer API so templates can call `this.attribute()`.
|
|
726
|
+
* @param {string} name - Attribute name
|
|
727
|
+
* @param {string|object} val - Attribute value
|
|
728
|
+
* @param {boolean} [avoidAttEscape] - Whether to avoid escaping
|
|
729
|
+
* @returns {JSONPathTransformerContext}
|
|
730
|
+
*/
|
|
731
|
+
attribute (name, val, avoidAttEscape) {
|
|
732
|
+
/** @type {any} */ (this._getJoiningTransformer()).attribute(
|
|
733
|
+
name, val, avoidAttEscape
|
|
734
|
+
);
|
|
735
|
+
return this;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Append text content. Mirrors the joining transformer API so templates can
|
|
740
|
+
* call `this.text()`.
|
|
741
|
+
* @param {string} txt - Text content
|
|
742
|
+
* @returns {JSONPathTransformerContext}
|
|
743
|
+
*/
|
|
744
|
+
text (txt) {
|
|
745
|
+
/** @type {any} */ (this._getJoiningTransformer()).text(txt);
|
|
746
|
+
return this;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* @param {string} name - Property set name
|
|
751
|
+
* @param {object} propertySetObj - Property set object
|
|
752
|
+
* @param {any[]} [usePropertySets] - Property sets to use
|
|
753
|
+
* @returns {JSONPathTransformerContext}
|
|
754
|
+
*/
|
|
755
|
+
propertySet (name, propertySetObj, usePropertySets) {
|
|
756
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
757
|
+
const that = this;
|
|
758
|
+
this.propertySets[name] = usePropertySets
|
|
759
|
+
? ({
|
|
760
|
+
...propertySetObj,
|
|
761
|
+
...usePropertySets.reduce((obj, psName) => {
|
|
762
|
+
return that._usePropertySets(obj, psName);
|
|
763
|
+
}, {})
|
|
764
|
+
})
|
|
765
|
+
: propertySetObj;
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* @param {object} obj - Object to assign properties to
|
|
771
|
+
* @param {string} name - Property set name
|
|
772
|
+
* @returns {object}
|
|
773
|
+
*/
|
|
774
|
+
_usePropertySets (obj, name) {
|
|
775
|
+
return Object.assign(obj, this.propertySets[name]);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* @param {string} name - Key name
|
|
780
|
+
* @param {*} value - Value to match
|
|
781
|
+
* @returns {*}
|
|
782
|
+
*/
|
|
783
|
+
getKey (name, value) {
|
|
784
|
+
const key = this.keys[name];
|
|
785
|
+
const matches = this.get(key.match, true);
|
|
786
|
+
for (const match of matches) { // For objects or arrays
|
|
787
|
+
if (match && typeof match === 'object' &&
|
|
788
|
+
match[key.use] === value) {
|
|
789
|
+
return match;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return this;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* @param {string} name - Key name
|
|
797
|
+
* @param {string} match - Match expression
|
|
798
|
+
* @param {string} use - Use expression
|
|
799
|
+
* @returns {JSONPathTransformerContext}
|
|
800
|
+
*/
|
|
801
|
+
key (name, match, use) {
|
|
802
|
+
this.keys[name] = {match, use};
|
|
803
|
+
return this;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export default JSONPathTransformerContext;
|