jtlt 0.2.0 → 0.4.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/CHANGES.md +27 -0
- package/README.md +61 -164
- package/demo/calltemplate-params-demo.js +138 -0
- package/demo/codemirror.esm.js +28242 -0
- package/demo/codemirror.js +94 -0
- package/demo/index.css +7 -0
- package/demo/index.html +28 -0
- package/demo/index.js +210 -0
- package/demo/vendor/jamilih/dist/jml.mjs +2341 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +356 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.Stringifier.js +186 -0
- package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +746 -0
- package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.html +33 -0
- package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.testing.js +25 -0
- package/demo/vendor/jhtml/src/jhtml-browser.js +5 -0
- package/demo/vendor/jhtml/src/jhtml-node.cts +3 -0
- package/demo/vendor/jhtml/src/jhtml-node.js +8 -0
- package/demo/vendor/jhtml/src/jhtml-node.mts +1 -0
- package/demo/vendor/jhtml/src/jhtml.cts +3 -0
- package/demo/vendor/jhtml/src/jhtml.js +602 -0
- package/demo/vendor/jhtml/src/jhtml.mts +1 -0
- package/demo/vendor/jsonpath-plus/dist/index-browser-esm.js +2158 -0
- package/demo/vendor/simple-get-json/dist/index-es.js +151 -0
- package/demo/xpath2-placeholder.js +1 -0
- package/dist/AbstractJoiningTransformer.d.ts +83 -9
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/DOMJoiningTransformer.d.ts +85 -25
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts +159 -51
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts +37 -38
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +334 -124
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts +132 -41
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts +35 -20
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +281 -105
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index-browser.d.ts +4 -0
- package/dist/index-browser.d.ts.map +1 -0
- package/dist/index-node.d.ts +4 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index.d.ts +330 -57
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +204 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/API.expanded.md +172 -5
- package/docs/API.md +92 -2
- package/docs/TO-DO.md +168 -0
- package/docs/calltemplate-params.md +251 -0
- package/eslint.config.js +11 -5
- package/package.json +35 -9
- package/pnpm-workspace.yaml +1 -0
- package/rollup.config.js +13 -0
- package/src/AbstractJoiningTransformer.js +54 -15
- package/src/DOMJoiningTransformer.js +275 -28
- package/src/JSONJoiningTransformer.js +351 -70
- package/src/JSONPathTransformer.js +48 -30
- package/src/JSONPathTransformerContext.js +729 -107
- package/src/StringJoiningTransformer.js +311 -57
- package/src/XPathTransformer.js +27 -12
- package/src/XPathTransformerContext.js +928 -99
- package/src/index-browser.js +5 -0
- package/src/index-node.js +7 -0
- package/src/index.js +502 -98
- package/tsconfig.json +5 -2
- package/typings/xpath2-js.d.ts +40 -1
- package/src/types/xpath2-js.d.ts +0 -2
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import {JSONPath as jsonpath} from 'jsonpath-plus';
|
|
2
2
|
import JSONPathTransformer from './JSONPathTransformer.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {number|string|{
|
|
6
|
+
* value?: number|string,
|
|
7
|
+
* count?: string,
|
|
8
|
+
* format?: string,
|
|
9
|
+
* groupingSeparator?: string,
|
|
10
|
+
* groupingSize?: number,
|
|
11
|
+
* lang?: string,
|
|
12
|
+
* letterValue?: string
|
|
13
|
+
* }} NumberValue
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sort spec types used by applyTemplates() and forEach().
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* select?: string,
|
|
20
|
+
* order?: 'ascending' | 'descending',
|
|
21
|
+
* type?: 'text'|'number',
|
|
22
|
+
* locale?: string,
|
|
23
|
+
* localeOptions?: unknown
|
|
24
|
+
* }} SortObject
|
|
25
|
+
* @typedef {(a: unknown, b: unknown,
|
|
26
|
+
* ctx: JSONPathTransformerContext
|
|
27
|
+
* ) => number} SortComparator
|
|
28
|
+
* @typedef {string | SortObject | SortComparator |
|
|
29
|
+
* Array<string|SortObject>} SortSpec
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template [T = "json"]
|
|
34
|
+
* @typedef {object} JSONPathTransformerContextConfig
|
|
35
|
+
* @property {null|boolean|number|string|object} data - Data to transform
|
|
36
|
+
* @property {object} [parent] - Parent object
|
|
37
|
+
* @property {string} [parentProperty] - Parent property name
|
|
38
|
+
* @property {boolean} [errorOnEqualPriority] - Whether to error on
|
|
39
|
+
* equal priority
|
|
40
|
+
* @property {T extends "json" ? import('./JSONJoiningTransformer.js').
|
|
41
|
+
* default : T extends "string" ? import('./StringJoiningTransformer.js').
|
|
42
|
+
* default : import('./DOMJoiningTransformer.js').
|
|
43
|
+
* default} joiningTransformer - Joining transformer
|
|
44
|
+
* @property {boolean} [preventEval] - Whether to prevent eval in
|
|
45
|
+
* JSONPath
|
|
46
|
+
* @property {(path: string) => number} [specificityPriorityResolver]
|
|
47
|
+
* Priority resolver function
|
|
48
|
+
* @property {import('./index.js').JSONPathTemplateObject<T>[]} templates
|
|
49
|
+
*/
|
|
50
|
+
|
|
4
51
|
/**
|
|
5
52
|
* Execution context for JSONPath-driven template application.
|
|
6
53
|
*
|
|
@@ -8,23 +55,18 @@ import JSONPathTransformer from './JSONPathTransformer.js';
|
|
|
8
55
|
* running templates. Exposes helper methods that mirror the underlying
|
|
9
56
|
* joining transformer (e.g., string(), object(), array()) so templates can
|
|
10
57
|
* emit results without referencing the joiner directly.
|
|
58
|
+
* @template [T = "json"]
|
|
11
59
|
*/
|
|
12
60
|
class JSONPathTransformerContext {
|
|
13
61
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
* @param {
|
|
21
|
-
*
|
|
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
|
|
62
|
+
* Holds the current iteration state (for position calculations).
|
|
63
|
+
* @type {{ index?: number } | undefined}
|
|
64
|
+
*/
|
|
65
|
+
iterationState;
|
|
66
|
+
/**
|
|
67
|
+
* @param {JSONPathTransformerContextConfig<T>} config
|
|
68
|
+
* @param {import('./index.js').JSONPathTemplateObject<T>[]} templates - Array
|
|
69
|
+
* of template objects
|
|
28
70
|
*/
|
|
29
71
|
constructor (config, templates) {
|
|
30
72
|
this._config = config;
|
|
@@ -32,16 +74,18 @@ class JSONPathTransformerContext {
|
|
|
32
74
|
this._contextObj = this._origObj = config.data;
|
|
33
75
|
this._parent = config.parent || this._config;
|
|
34
76
|
this._parentProperty = config.parentProperty || 'data';
|
|
35
|
-
/** @type {Record<string,
|
|
77
|
+
/** @type {Record<string, unknown>} */
|
|
36
78
|
this.vars = {};
|
|
37
|
-
/** @type {Record<string,
|
|
79
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
38
80
|
this.propertySets = {};
|
|
39
|
-
/** @type {Record<string,
|
|
81
|
+
/** @type {Record<string, {match: string, use: string}>} */
|
|
40
82
|
this.keys = {};
|
|
41
83
|
/** @type {boolean | undefined} */
|
|
42
84
|
this._initialized = undefined;
|
|
43
85
|
/** @type {string | undefined} */
|
|
44
86
|
this._currPath = undefined;
|
|
87
|
+
/** @type {Record<string, any> | undefined} */
|
|
88
|
+
this._params = undefined;
|
|
45
89
|
}
|
|
46
90
|
|
|
47
91
|
/**
|
|
@@ -59,28 +103,30 @@ class JSONPathTransformerContext {
|
|
|
59
103
|
|
|
60
104
|
/**
|
|
61
105
|
* Gets the joining transformer from config.
|
|
62
|
-
* @returns {
|
|
106
|
+
* @returns {T extends "json" ? import('./JSONJoiningTransformer.js').
|
|
107
|
+
* default : T extends "string" ? import('./StringJoiningTransformer.js').
|
|
108
|
+
* default : import('./DOMJoiningTransformer.js').
|
|
109
|
+
* default} The joining transformer
|
|
63
110
|
*/
|
|
64
111
|
_getJoiningTransformer () {
|
|
65
112
|
return this._config.joiningTransformer;
|
|
66
113
|
}
|
|
67
114
|
|
|
68
115
|
/**
|
|
69
|
-
* @param {
|
|
70
|
-
* @returns {
|
|
116
|
+
* @param {string | Node} item - Item to append to output
|
|
117
|
+
* @returns {this}
|
|
71
118
|
*/
|
|
72
119
|
appendOutput (item) {
|
|
73
|
-
|
|
120
|
+
this._getJoiningTransformer().append(item);
|
|
74
121
|
return this;
|
|
75
122
|
}
|
|
76
123
|
|
|
77
|
-
/* c8 ignore next 4 -- JSDoc block incorrectly counted as coverable by c8 */
|
|
78
124
|
/**
|
|
79
125
|
* Gets the current output.
|
|
80
|
-
* @returns {
|
|
126
|
+
* @returns {any} The output from the joining transformer
|
|
81
127
|
*/
|
|
82
128
|
getOutput () {
|
|
83
|
-
return
|
|
129
|
+
return this._getJoiningTransformer().get();
|
|
84
130
|
}
|
|
85
131
|
|
|
86
132
|
/**
|
|
@@ -89,7 +135,7 @@ class JSONPathTransformerContext {
|
|
|
89
135
|
* to the result tree instead).
|
|
90
136
|
* @param {string} select - JSONPath selector
|
|
91
137
|
* @param {boolean} wrap - Whether to wrap results
|
|
92
|
-
* @returns {
|
|
138
|
+
* @returns {any} The selected value(s)
|
|
93
139
|
*/
|
|
94
140
|
get (select, wrap) {
|
|
95
141
|
if (select) {
|
|
@@ -103,8 +149,8 @@ class JSONPathTransformerContext {
|
|
|
103
149
|
}
|
|
104
150
|
|
|
105
151
|
/**
|
|
106
|
-
* @param {
|
|
107
|
-
* @returns {
|
|
152
|
+
* @param {any} v - Value to set
|
|
153
|
+
* @returns {this}
|
|
108
154
|
*/
|
|
109
155
|
set (v) {
|
|
110
156
|
(/** @type {Record<string, any>} */ (this._parent))[
|
|
@@ -123,10 +169,12 @@ class JSONPathTransformerContext {
|
|
|
123
169
|
* locale, localeOptions }
|
|
124
170
|
* - array: multiple key objects/strings in priority order.
|
|
125
171
|
*
|
|
126
|
-
* @param {string|
|
|
172
|
+
* @param {string|null|
|
|
173
|
+
* {mode?: string, select?: string}
|
|
174
|
+
* } [select] - JSONPath selector or options object
|
|
127
175
|
* @param {string} [mode] - Mode to apply
|
|
128
|
-
* @param {
|
|
129
|
-
* @returns {
|
|
176
|
+
* @param {SortSpec} [sort] - Sort spec
|
|
177
|
+
* @returns {this}
|
|
130
178
|
*/
|
|
131
179
|
applyTemplates (select, mode, sort) {
|
|
132
180
|
// Matches templates by (path, mode), resolves priority, and invokes each
|
|
@@ -135,10 +183,8 @@ class JSONPathTransformerContext {
|
|
|
135
183
|
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
136
184
|
const that = this;
|
|
137
185
|
if (select && typeof select === 'object') {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
mode = selectObj.mode ?? mode;
|
|
141
|
-
select = selectObj.select ?? select;
|
|
186
|
+
mode = select.mode ?? mode;
|
|
187
|
+
select = select.select ?? select;
|
|
142
188
|
}
|
|
143
189
|
if (!this._initialized) {
|
|
144
190
|
select = select || '$';
|
|
@@ -154,7 +200,11 @@ class JSONPathTransformerContext {
|
|
|
154
200
|
const jsonPathExpr = propertyNamesMode ? select.slice(0, -1) : select;
|
|
155
201
|
// Todo: Use results here?
|
|
156
202
|
/* const results = */ this._getJoiningTransformer();
|
|
157
|
-
const modeMatchedTemplates = this._templates.filter(
|
|
203
|
+
const modeMatchedTemplates = this._templates.filter((templateObj) => {
|
|
204
|
+
// Exclude named-only templates (those with name but no path)
|
|
205
|
+
if (templateObj.name && !templateObj.path) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
158
208
|
return ((mode && mode === templateObj.mode) ||
|
|
159
209
|
(!mode && !templateObj.mode));
|
|
160
210
|
});
|
|
@@ -162,7 +212,7 @@ class JSONPathTransformerContext {
|
|
|
162
212
|
// Collect matches first (to allow sorting), then process
|
|
163
213
|
/**
|
|
164
214
|
* @type {{
|
|
165
|
-
* value:any, parent:any, parentProperty?:string, path:string
|
|
215
|
+
* value: any, parent: any, parentProperty?: string, path: string
|
|
166
216
|
* }[]}
|
|
167
217
|
*/
|
|
168
218
|
const matches = /** @type {any} */ (jsonpath)({
|
|
@@ -231,7 +281,7 @@ class JSONPathTransformerContext {
|
|
|
231
281
|
}
|
|
232
282
|
/**
|
|
233
283
|
* @param {any} sortSpec
|
|
234
|
-
* @returns {((a:{value:any}, b:{value:any})=>number)|null}
|
|
284
|
+
* @returns {((a: {value: any}, b: {value: any}) => number) | null}
|
|
235
285
|
*/
|
|
236
286
|
function buildComparator (sortSpec) {
|
|
237
287
|
if (!sortSpec) {
|
|
@@ -283,6 +333,13 @@ class JSONPathTransformerContext {
|
|
|
283
333
|
that._currPath += path.replace(/^\$/v, '');
|
|
284
334
|
const pathMatchedTemplates = modeMatchedTemplates.filter(
|
|
285
335
|
function (templateObj) {
|
|
336
|
+
// At this point, we know templateObj.path exists because we filtered
|
|
337
|
+
// out named-only templates in modeMatchedTemplates
|
|
338
|
+
/* c8 ignore start -- defensive check, already filtered at line 188 */
|
|
339
|
+
if (!templateObj.path) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
/* c8 ignore stop */
|
|
286
343
|
const queryResult = /** @type {any[]} */ (
|
|
287
344
|
(/** @type {any} */ (jsonpath))({
|
|
288
345
|
path: JSONPathTransformer.makeJSONPathAbsolute(
|
|
@@ -323,12 +380,12 @@ class JSONPathTransformerContext {
|
|
|
323
380
|
* trigger error, making the `: 0` branch nearly unreachable. */
|
|
324
381
|
const aPriority = typeof a.priority === 'number'
|
|
325
382
|
? a.priority
|
|
326
|
-
: (that._config.specificityPriorityResolver
|
|
383
|
+
: (that._config.specificityPriorityResolver && a.path
|
|
327
384
|
? that._config.specificityPriorityResolver(a.path)
|
|
328
385
|
: 0);
|
|
329
386
|
const bPriority = typeof b.priority === 'number'
|
|
330
387
|
? b.priority
|
|
331
|
-
: (that._config.specificityPriorityResolver
|
|
388
|
+
: (that._config.specificityPriorityResolver && b.path
|
|
332
389
|
? that._config.specificityPriorityResolver(b.path)
|
|
333
390
|
: 0);
|
|
334
391
|
/* c8 ignore stop */
|
|
@@ -340,18 +397,27 @@ class JSONPathTransformerContext {
|
|
|
340
397
|
return (aPriority > bPriority) ? -1 : 1;
|
|
341
398
|
});
|
|
342
399
|
|
|
343
|
-
templateObj =
|
|
400
|
+
templateObj =
|
|
401
|
+
/** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
|
|
402
|
+
pathMatchedTemplates.shift()
|
|
403
|
+
);
|
|
344
404
|
}
|
|
345
405
|
|
|
346
406
|
that._contextObj = value;
|
|
347
407
|
that._parent = parent;
|
|
348
408
|
that._parentProperty = (parentProperty ?? that._parentProperty);
|
|
349
409
|
|
|
350
|
-
const ret =
|
|
351
|
-
|
|
352
|
-
|
|
410
|
+
const ret =
|
|
411
|
+
/** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
|
|
412
|
+
templateObj
|
|
413
|
+
).template.call(
|
|
414
|
+
that, value, {mode, parent, parentProperty}
|
|
415
|
+
);
|
|
353
416
|
if (typeof ret !== 'undefined') {
|
|
354
|
-
|
|
417
|
+
// After the undefined check, ret is ResultType<T>
|
|
418
|
+
that._getJoiningTransformer().append(
|
|
419
|
+
/** @type {string|Node|*} */ (ret)
|
|
420
|
+
);
|
|
355
421
|
}
|
|
356
422
|
|
|
357
423
|
that._contextObj = value;
|
|
@@ -368,26 +434,41 @@ class JSONPathTransformerContext {
|
|
|
368
434
|
}
|
|
369
435
|
|
|
370
436
|
/**
|
|
371
|
-
* @param {string|
|
|
437
|
+
* @param {string|
|
|
438
|
+
* {name: string, withParam?: any[]}} name - Template name or
|
|
439
|
+
* options object
|
|
372
440
|
* @param {any[]} [withParams] - Parameters to pass to template
|
|
373
|
-
* @returns {
|
|
441
|
+
* @returns {this}
|
|
374
442
|
*/
|
|
375
443
|
callTemplate (name, withParams) {
|
|
376
444
|
// Invokes a named template, optionally passing values via withParam.
|
|
377
|
-
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
378
|
-
const that = this;
|
|
379
445
|
if (name && typeof name === 'object') {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
withParams = nameObj.withParam || withParams;
|
|
383
|
-
name = nameObj.name ?? name;
|
|
446
|
+
withParams = name.withParam || withParams;
|
|
447
|
+
({name} = name);
|
|
384
448
|
}
|
|
385
449
|
withParams = withParams || [];
|
|
386
|
-
|
|
387
|
-
|
|
450
|
+
|
|
451
|
+
// Store parameters in a temporary context for valueOf() access
|
|
452
|
+
const prevParams = this._params;
|
|
453
|
+
/** @type {Record<string, any>} */
|
|
454
|
+
const params = {};
|
|
455
|
+
this._params = params;
|
|
456
|
+
|
|
457
|
+
withParams.forEach((withParam, index) => {
|
|
458
|
+
const value = withParam.value !== undefined
|
|
459
|
+
? withParam.value
|
|
460
|
+
: this.get(withParam.select, false);
|
|
461
|
+
|
|
462
|
+
// Store by name if provided, otherwise by index
|
|
463
|
+
if (withParam.name) {
|
|
464
|
+
params[withParam.name] = value;
|
|
465
|
+
} else {
|
|
466
|
+
params[String(index)] = value;
|
|
467
|
+
}
|
|
388
468
|
});
|
|
469
|
+
|
|
389
470
|
const results = this._getJoiningTransformer();
|
|
390
|
-
const templateObj = this._templates.find(
|
|
471
|
+
const templateObj = this._templates.find((template) => {
|
|
391
472
|
return template.name === name;
|
|
392
473
|
});
|
|
393
474
|
if (!templateObj) {
|
|
@@ -396,8 +477,14 @@ class JSONPathTransformerContext {
|
|
|
396
477
|
);
|
|
397
478
|
}
|
|
398
479
|
|
|
399
|
-
const result = templateObj.template.
|
|
400
|
-
|
|
480
|
+
const result = templateObj.template.call(this, this._contextObj, {});
|
|
481
|
+
if (typeof result !== 'undefined') {
|
|
482
|
+
/** @type {any} */ (results).append(result);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Restore previous parameter context
|
|
486
|
+
this._params = prevParams;
|
|
487
|
+
|
|
401
488
|
return this;
|
|
402
489
|
}
|
|
403
490
|
|
|
@@ -406,14 +493,16 @@ class JSONPathTransformerContext {
|
|
|
406
493
|
*
|
|
407
494
|
* Sort parameter forms are the same as applyTemplates().
|
|
408
495
|
* @param {string} select - JSONPath selector
|
|
409
|
-
* @param {
|
|
410
|
-
*
|
|
411
|
-
*
|
|
496
|
+
* @param {(this: JSONPathTransformerContext<T>,
|
|
497
|
+
* value: any
|
|
498
|
+
* ) => void} cb - Callback function
|
|
499
|
+
* @param {SortSpec} [sort] - Sort spec
|
|
500
|
+
* @returns {this}
|
|
412
501
|
*/
|
|
413
502
|
forEach (select, cb, sort) {
|
|
414
503
|
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
415
504
|
const that = this;
|
|
416
|
-
/** @type {{value:any}[]} */
|
|
505
|
+
/** @type {{value: any}[]} */
|
|
417
506
|
const matches = /** @type {any} */ (jsonpath)({
|
|
418
507
|
path: select,
|
|
419
508
|
json: this._contextObj,
|
|
@@ -478,7 +567,7 @@ class JSONPathTransformerContext {
|
|
|
478
567
|
}
|
|
479
568
|
/**
|
|
480
569
|
* @param {any} sortSpec
|
|
481
|
-
* @returns {((a:{value:any}, b:{value:any})=>number)|null}
|
|
570
|
+
* @returns {((a: {value: any}, b: {value: any}) => number) | null}
|
|
482
571
|
*/
|
|
483
572
|
function feBuildComparator (sortSpec) {
|
|
484
573
|
if (!sortSpec) {
|
|
@@ -524,24 +613,181 @@ class JSONPathTransformerContext {
|
|
|
524
613
|
|
|
525
614
|
/**
|
|
526
615
|
* @param {string|object} [select] - JSONPath selector
|
|
527
|
-
* @returns {
|
|
616
|
+
* @returns {this}
|
|
528
617
|
*/
|
|
529
618
|
valueOf (select) {
|
|
530
619
|
// Appends the value of the given JSONPath (or the current context when
|
|
531
620
|
// `{select: '.'}` is passed) to the output via the joining transformer.
|
|
532
621
|
const results = this._getJoiningTransformer();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
622
|
+
let result;
|
|
623
|
+
|
|
624
|
+
if (select && typeof select === 'object' &&
|
|
625
|
+
/** @type {{select?: string}} */ (select).select === '.') {
|
|
626
|
+
result = this._contextObj;
|
|
627
|
+
} else {
|
|
628
|
+
const selectStr = typeof select === 'object'
|
|
629
|
+
? /** @type {{select?: string}} */ (select).select
|
|
630
|
+
: select;
|
|
631
|
+
|
|
632
|
+
// Check if this is a parameter reference (starts with $)
|
|
633
|
+
if (selectStr && selectStr.startsWith('$')) {
|
|
634
|
+
const paramName = selectStr.slice(1);
|
|
635
|
+
result = (this._params && paramName in this._params)
|
|
636
|
+
? this._params[paramName]
|
|
637
|
+
: this.get(/** @type {string} */ (selectStr), false);
|
|
638
|
+
} else {
|
|
639
|
+
result = this.get(/** @type {string} */ (select), false);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
537
643
|
/** @type {any} */ (results).append(result);
|
|
538
644
|
return this;
|
|
539
645
|
}
|
|
540
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Analyze a string with a regular expression, equivalent to
|
|
649
|
+
* xsl:analyze-string. Processes matching and non-matching substrings
|
|
650
|
+
* with separate callbacks.
|
|
651
|
+
* @param {string} str - The string to analyze
|
|
652
|
+
* @param {string|RegExp} regex - Regular expression to match against
|
|
653
|
+
* @param {{
|
|
654
|
+
* matchingSubstring?: (
|
|
655
|
+
* this: JSONPathTransformerContext<T>,
|
|
656
|
+
* substring: string,
|
|
657
|
+
* groups: string[],
|
|
658
|
+
* regexGroup: (n: number) => string
|
|
659
|
+
* ) => void,
|
|
660
|
+
* nonMatchingSubstring?: (
|
|
661
|
+
* this: JSONPathTransformerContext<T>,
|
|
662
|
+
* substring: string
|
|
663
|
+
* ) => void,
|
|
664
|
+
* flags?: string
|
|
665
|
+
* }} options - Options object
|
|
666
|
+
* @returns {this}
|
|
667
|
+
*/
|
|
668
|
+
analyzeString (str, regex, options = {}) {
|
|
669
|
+
// Ensure we have a string
|
|
670
|
+
const inputString = String(str || '');
|
|
671
|
+
|
|
672
|
+
// If empty string, do nothing
|
|
673
|
+
if (inputString.length === 0) {
|
|
674
|
+
return this;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const {
|
|
678
|
+
matchingSubstring,
|
|
679
|
+
nonMatchingSubstring,
|
|
680
|
+
flags = ''
|
|
681
|
+
} = options;
|
|
682
|
+
|
|
683
|
+
// Convert regex to RegExp if it's a string
|
|
684
|
+
let regexObj;
|
|
685
|
+
if (typeof regex === 'string') {
|
|
686
|
+
// Ensure 'g' flag is present for global matching
|
|
687
|
+
const actualFlags = flags.includes('g') ? flags : flags + 'g';
|
|
688
|
+
regexObj = new RegExp(regex, actualFlags);
|
|
689
|
+
} else {
|
|
690
|
+
regexObj = regex;
|
|
691
|
+
// Ensure global flag is set
|
|
692
|
+
if (!regexObj.global) {
|
|
693
|
+
regexObj = new RegExp(
|
|
694
|
+
regexObj.source,
|
|
695
|
+
regexObj.flags + 'g'
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Check for zero-length matches (error condition in XSLT)
|
|
701
|
+
if (regexObj.test('')) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
'Regular expression matches zero-length string'
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Store captured groups for access during callback
|
|
708
|
+
/** @type {string[] | undefined} */
|
|
709
|
+
let currentCapturedGroups;
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Get captured group by index.
|
|
713
|
+
* @param {number} groupNumber - Group index
|
|
714
|
+
* @returns {string} - Captured group or empty string
|
|
715
|
+
*/
|
|
716
|
+
const getRegexGroup = (groupNumber) => {
|
|
717
|
+
if (!currentCapturedGroups ||
|
|
718
|
+
groupNumber < 0 ||
|
|
719
|
+
groupNumber >= currentCapturedGroups.length) {
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
return currentCapturedGroups[groupNumber] || '';
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// Save previous context to restore later
|
|
726
|
+
const prevContext = this._contextObj;
|
|
727
|
+
|
|
728
|
+
let lastIndex = 0;
|
|
729
|
+
let match;
|
|
730
|
+
|
|
731
|
+
// Bind callbacks to this context
|
|
732
|
+
const boundMatchingSubstring = matchingSubstring
|
|
733
|
+
? matchingSubstring.bind(this)
|
|
734
|
+
: undefined;
|
|
735
|
+
const boundNonMatchingSubstring = nonMatchingSubstring
|
|
736
|
+
? nonMatchingSubstring.bind(this)
|
|
737
|
+
: undefined;
|
|
738
|
+
|
|
739
|
+
// Find all matches
|
|
740
|
+
while ((match = regexObj.exec(inputString)) !== null) {
|
|
741
|
+
// Process non-matching substring before this match
|
|
742
|
+
if (match.index > lastIndex) {
|
|
743
|
+
const nonMatchingStr = inputString.slice(lastIndex, match.index);
|
|
744
|
+
if (boundNonMatchingSubstring) {
|
|
745
|
+
this._contextObj = nonMatchingStr;
|
|
746
|
+
boundNonMatchingSubstring(nonMatchingStr);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Process matching substring
|
|
751
|
+
if (boundMatchingSubstring) {
|
|
752
|
+
const matchingStr = match[0];
|
|
753
|
+
// Store captured groups: [full match, group1, group2, ...]
|
|
754
|
+
currentCapturedGroups = [...match];
|
|
755
|
+
this._contextObj = matchingStr;
|
|
756
|
+
boundMatchingSubstring(
|
|
757
|
+
matchingStr, currentCapturedGroups, getRegexGroup
|
|
758
|
+
);
|
|
759
|
+
currentCapturedGroups = undefined;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const {lastIndex: newLastIndex} = regexObj;
|
|
763
|
+
lastIndex = newLastIndex;
|
|
764
|
+
|
|
765
|
+
// Prevent infinite loop on zero-length matches (shouldn't happen
|
|
766
|
+
// due to earlier check, but defensive)
|
|
767
|
+
if (match.index === regexObj.lastIndex) {
|
|
768
|
+
regexObj.lastIndex++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Process final non-matching substring
|
|
773
|
+
if (lastIndex < inputString.length) {
|
|
774
|
+
const nonMatchingStr = inputString.slice(lastIndex);
|
|
775
|
+
if (boundNonMatchingSubstring) {
|
|
776
|
+
this._contextObj = nonMatchingStr;
|
|
777
|
+
boundNonMatchingSubstring(nonMatchingStr);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Restore previous context
|
|
782
|
+
this._contextObj = prevContext;
|
|
783
|
+
|
|
784
|
+
return this;
|
|
785
|
+
}
|
|
786
|
+
|
|
541
787
|
/**
|
|
542
788
|
* Deep copy selection or current context when omitted.
|
|
543
789
|
* @param {string} [select] - JSONPath selector
|
|
544
|
-
* @returns {
|
|
790
|
+
* @returns {this}
|
|
545
791
|
*/
|
|
546
792
|
copyOf (select) { // Deep
|
|
547
793
|
// Deeply clones the value at `select` (or current context if omitted)
|
|
@@ -567,11 +813,9 @@ class JSONPathTransformerContext {
|
|
|
567
813
|
* vary across environments; behavior covered by tests. */
|
|
568
814
|
// structuredClone failed (e.g., Symbols); if any functions present
|
|
569
815
|
// on own enumerable string-keyed properties, preserve via shallow.
|
|
570
|
-
/** @type {boolean} */ let hasFunc = false;
|
|
571
816
|
for (const k of Object.keys(val)) {
|
|
572
817
|
const v = /** @type {any} */ (val)[k];
|
|
573
818
|
if (typeof v === 'function') {
|
|
574
|
-
hasFunc = true;
|
|
575
819
|
break;
|
|
576
820
|
}
|
|
577
821
|
}
|
|
@@ -584,14 +828,14 @@ class JSONPathTransformerContext {
|
|
|
584
828
|
// Primitives/functions copied by value/reference semantics naturally.
|
|
585
829
|
clone = val;
|
|
586
830
|
}
|
|
587
|
-
|
|
831
|
+
this._getJoiningTransformer().append(clone);
|
|
588
832
|
return this;
|
|
589
833
|
}
|
|
590
834
|
|
|
591
835
|
/**
|
|
592
836
|
* Shallow copy current context; optionally merge property set names.
|
|
593
837
|
* @param {string[]} [propertySets] - Property sets to merge
|
|
594
|
-
* @returns {
|
|
838
|
+
* @returns {this}
|
|
595
839
|
*/
|
|
596
840
|
copy (propertySets) { // Shallow
|
|
597
841
|
// Creates a shallow clone of current context object/array (or primitive)
|
|
@@ -611,14 +855,14 @@ class JSONPathTransformerContext {
|
|
|
611
855
|
} else { /* c8 ignore start -- primitive branch attribution variance */
|
|
612
856
|
clone = src; // Primitive/function - nothing to shallow clone
|
|
613
857
|
} /* c8 ignore stop */
|
|
614
|
-
|
|
858
|
+
this._getJoiningTransformer().append(clone);
|
|
615
859
|
return this;
|
|
616
860
|
}
|
|
617
861
|
|
|
618
862
|
/**
|
|
619
863
|
* @param {string} name - Variable name
|
|
620
864
|
* @param {string} select - JSONPath selector
|
|
621
|
-
* @returns {
|
|
865
|
+
* @returns {this}
|
|
622
866
|
*/
|
|
623
867
|
variable (name, select) {
|
|
624
868
|
this.vars[name] = this.get(select, false);
|
|
@@ -626,7 +870,7 @@ class JSONPathTransformerContext {
|
|
|
626
870
|
}
|
|
627
871
|
|
|
628
872
|
/**
|
|
629
|
-
* @param {
|
|
873
|
+
* @param {unknown} json - JSON data to log
|
|
630
874
|
* @returns {void}
|
|
631
875
|
*/
|
|
632
876
|
// eslint-disable-next-line class-methods-use-this -- Convenient
|
|
@@ -637,35 +881,297 @@ class JSONPathTransformerContext {
|
|
|
637
881
|
|
|
638
882
|
/**
|
|
639
883
|
* @param {string} str - String value
|
|
640
|
-
* @param {
|
|
641
|
-
*
|
|
884
|
+
* @param {import('./JSONJoiningTransformer.js').
|
|
885
|
+
* SimpleCallback<T>} [cb] - Optional callback to build nested
|
|
886
|
+
* string content
|
|
887
|
+
* @returns {this}
|
|
642
888
|
*/
|
|
643
|
-
// Todo: Add other methods from the joining transformers
|
|
644
889
|
string (str, cb) {
|
|
645
890
|
/** @type {any} */ (this._getJoiningTransformer()).string(str, cb);
|
|
646
891
|
return this;
|
|
647
892
|
}
|
|
648
893
|
|
|
649
894
|
/**
|
|
650
|
-
* Append a number to JSON output
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* @returns {
|
|
895
|
+
* Append a number to JSON output with xsl:number-like formatting.
|
|
896
|
+
* @param {NumberValue} num - Number value, "position()" string, or
|
|
897
|
+
* options object
|
|
898
|
+
* @returns {this}
|
|
654
899
|
*/
|
|
655
900
|
number (num) {
|
|
656
|
-
|
|
901
|
+
// Handle xsl:number-like functionality
|
|
902
|
+
if (typeof num === 'object' && num !== null) {
|
|
903
|
+
const opts = num;
|
|
904
|
+
let {value} = opts;
|
|
905
|
+
|
|
906
|
+
// Handle position() and hierarchical numbering
|
|
907
|
+
if (value === 'position()' || value === undefined) {
|
|
908
|
+
const {count} = opts;
|
|
909
|
+
// @ts-expect-error: dynamic property access
|
|
910
|
+
const level = opts.level || 'single';
|
|
911
|
+
|
|
912
|
+
switch (level) {
|
|
913
|
+
case 'single': {
|
|
914
|
+
value = this.calculatePosition(count);
|
|
915
|
+
// If count is set, simulate count by returning total items
|
|
916
|
+
// in current context
|
|
917
|
+
if (count) {
|
|
918
|
+
const arr = Array.isArray(this.get(count, true))
|
|
919
|
+
? this.get(count, true)
|
|
920
|
+
/* c8 ignore next -- defensive: get(wrap) always returns array */
|
|
921
|
+
: [];
|
|
922
|
+
value = arr.length;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
case 'multiple': {
|
|
928
|
+
// Hierarchical numbering: get position for each ancestor up to root
|
|
929
|
+
const positions = [];
|
|
930
|
+
let state = /** @type {any} */ (this._config).iterationState;
|
|
931
|
+
while (state) {
|
|
932
|
+
// If count is set, use count for each ancestor if possible
|
|
933
|
+
if (count) {
|
|
934
|
+
const arr = Array.isArray(this.get(count, true))
|
|
935
|
+
? this.get(count, true)
|
|
936
|
+
/* c8 ignore next -- defensive: get(wrap) returns array */
|
|
937
|
+
: [];
|
|
938
|
+
positions.unshift(arr.length);
|
|
939
|
+
} else {
|
|
940
|
+
positions.unshift(
|
|
941
|
+
state.index !== undefined
|
|
942
|
+
? state.index + 1
|
|
943
|
+
/* c8 ignore next -- defensive: state always has index */
|
|
944
|
+
: 1
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
state = state.parentState;
|
|
948
|
+
}
|
|
949
|
+
value = positions.join('.');
|
|
950
|
+
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
case 'any': {
|
|
954
|
+
// Count all matching items up to current
|
|
955
|
+
value = this.calculatePosition(count);
|
|
956
|
+
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
// No default
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Determine format string and locale
|
|
964
|
+
let format = opts.format || '1';
|
|
965
|
+
const locale = opts.lang || 'en';
|
|
966
|
+
const {letterValue} = opts;
|
|
967
|
+
|
|
968
|
+
// If letterValue is 'alphabetic', force alphabetic format
|
|
969
|
+
if (letterValue === 'alphabetic') {
|
|
970
|
+
format = (opts.format && (/^[aA]$/v).test(opts.format)) ? opts.format : 'a';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Ensure value is a number or string for formatting
|
|
974
|
+
let numValue = value;
|
|
975
|
+
// If value is undefined, fallback to opts.value
|
|
976
|
+
if (typeof numValue === 'undefined') {
|
|
977
|
+
numValue = opts.value;
|
|
978
|
+
}
|
|
979
|
+
if (typeof numValue === 'string') {
|
|
980
|
+
numValue = Number(numValue);
|
|
981
|
+
}
|
|
982
|
+
if (typeof numValue !== 'number' || Number.isNaN(numValue)) {
|
|
983
|
+
numValue = 1;
|
|
984
|
+
}
|
|
985
|
+
const formatted = this._formatNumber(
|
|
986
|
+
numValue,
|
|
987
|
+
format,
|
|
988
|
+
opts.groupingSeparator,
|
|
989
|
+
opts.groupingSize,
|
|
990
|
+
locale
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
// Output as string if formatted, otherwise as number
|
|
994
|
+
if (format && format !== '1') {
|
|
995
|
+
this._getJoiningTransformer().plainText(formatted);
|
|
996
|
+
} else {
|
|
997
|
+
this._getJoiningTransformer().number(Number(formatted));
|
|
998
|
+
}
|
|
999
|
+
} else if (num === 'position()') {
|
|
1000
|
+
// Simple position() call
|
|
1001
|
+
const pos = this.calculatePosition();
|
|
1002
|
+
this._getJoiningTransformer().number(pos);
|
|
1003
|
+
} else {
|
|
1004
|
+
// Simple number
|
|
1005
|
+
this._getJoiningTransformer().number(
|
|
1006
|
+
typeof num === 'string' ? Number(num) : num
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
657
1009
|
return this;
|
|
658
1010
|
}
|
|
659
1011
|
|
|
1012
|
+
/**
|
|
1013
|
+
* Calculate position in current iteration context.
|
|
1014
|
+
* @param {string} [count] - JSONPath expression to match
|
|
1015
|
+
* @returns {number}
|
|
1016
|
+
*/
|
|
1017
|
+
calculatePosition (count) {
|
|
1018
|
+
// If count is provided, return the length of the matched array from
|
|
1019
|
+
// the root data
|
|
1020
|
+
if (count) {
|
|
1021
|
+
const result = jsonpath({
|
|
1022
|
+
path: count, json: this._origObj, resultType: 'value', wrap: true
|
|
1023
|
+
});
|
|
1024
|
+
if (Array.isArray(result)) {
|
|
1025
|
+
if (result.length === 0) {
|
|
1026
|
+
return 0;
|
|
1027
|
+
}
|
|
1028
|
+
// If the first item is an array, return its length
|
|
1029
|
+
if (Array.isArray(result[0])) {
|
|
1030
|
+
return result[0].length;
|
|
1031
|
+
}
|
|
1032
|
+
// Otherwise, return the number of matches
|
|
1033
|
+
return result.length;
|
|
1034
|
+
}
|
|
1035
|
+
/* c8 ignore next 3 -- defensive:
|
|
1036
|
+
jsonpath-plus with wrap:true always returns arrays */
|
|
1037
|
+
return 0;
|
|
1038
|
+
}
|
|
1039
|
+
// Get current index from iteration state
|
|
1040
|
+
const state = this.iterationState;
|
|
1041
|
+
if (state && typeof state.index === 'number') {
|
|
1042
|
+
return state.index + 1; // 1-indexed
|
|
1043
|
+
}
|
|
1044
|
+
return 1;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Format a number according to format string.
|
|
1049
|
+
* @param {number} num - Number to format
|
|
1050
|
+
* @param {string} format - Format string (1, a, A, i, I, 01, etc.)
|
|
1051
|
+
* @param {string} [groupingSeparator] - Separator for grouping
|
|
1052
|
+
* @param {number} [groupingSize] - Size of groups
|
|
1053
|
+
* @param {string} [locale]
|
|
1054
|
+
* @returns {string}
|
|
1055
|
+
*/
|
|
1056
|
+
_formatNumber (num, format, groupingSeparator, groupingSize, locale = 'en') {
|
|
1057
|
+
if (Number.isNaN(num)) {
|
|
1058
|
+
return String(num);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
let result;
|
|
1062
|
+
const formatChar = format.charAt(0);
|
|
1063
|
+
|
|
1064
|
+
switch (formatChar) {
|
|
1065
|
+
case 'i': {
|
|
1066
|
+
result = this._toRoman(num).toLowerCase();
|
|
1067
|
+
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
case 'I': {
|
|
1071
|
+
result = this._toRoman(num);
|
|
1072
|
+
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
case 'a': {
|
|
1076
|
+
result = this._toAlphabetic(num, false);
|
|
1077
|
+
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
case 'A': {
|
|
1081
|
+
result = this._toAlphabetic(num, true);
|
|
1082
|
+
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
case '0': {
|
|
1086
|
+
const width = format.length;
|
|
1087
|
+
result = String(num).padStart(width, '0');
|
|
1088
|
+
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
default: {
|
|
1092
|
+
// Use Intl.NumberFormat for decimal formatting if grouping/locale
|
|
1093
|
+
// options are provided
|
|
1094
|
+
let options = {};
|
|
1095
|
+
if (groupingSeparator || groupingSize) {
|
|
1096
|
+
options = {
|
|
1097
|
+
useGrouping: true
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
try {
|
|
1102
|
+
result = new Intl.NumberFormat(locale, options).format(num);
|
|
1103
|
+
if (groupingSeparator) {
|
|
1104
|
+
result = result.replaceAll(',', groupingSeparator);
|
|
1105
|
+
}
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
result = String(num);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Convert number to Roman numerals.
|
|
1116
|
+
* @param {number} num - Number to convert (1-3999)
|
|
1117
|
+
* @returns {string}
|
|
1118
|
+
* @private
|
|
1119
|
+
*/
|
|
1120
|
+
// eslint-disable-next-line class-methods-use-this -- Avoid for now
|
|
1121
|
+
_toRoman (num) {
|
|
1122
|
+
if (num < 1 || num > 3999) {
|
|
1123
|
+
return String(num);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
|
|
1127
|
+
const syms = [
|
|
1128
|
+
'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'
|
|
1129
|
+
];
|
|
1130
|
+
|
|
1131
|
+
let result = '';
|
|
1132
|
+
for (const [i, val] of vals.entries()) {
|
|
1133
|
+
while (num >= val) {
|
|
1134
|
+
result += syms[i];
|
|
1135
|
+
num -= val;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return result;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Convert number to alphabetic sequence.
|
|
1143
|
+
* @param {number} num - Number to convert
|
|
1144
|
+
* @param {boolean} uppercase - Use uppercase letters
|
|
1145
|
+
* @returns {string}
|
|
1146
|
+
* @private
|
|
1147
|
+
*/
|
|
1148
|
+
// eslint-disable-next-line class-methods-use-this -- Avoid for now
|
|
1149
|
+
_toAlphabetic (num, uppercase) {
|
|
1150
|
+
if (num < 1) {
|
|
1151
|
+
return String(num);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
let result = '';
|
|
1155
|
+
const base = uppercase ? 65 : 97; // 'A' or 'a'
|
|
1156
|
+
|
|
1157
|
+
while (num > 0) {
|
|
1158
|
+
num--; // Make 0-indexed
|
|
1159
|
+
result = String.fromCodePoint(base + (num % 26)) + result;
|
|
1160
|
+
num = Math.floor(num / 26);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return result;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
660
1166
|
/**
|
|
661
1167
|
* Append plain text directly to the output without escaping or JSON
|
|
662
1168
|
* stringification. Mirrors the joining transformer API so templates can
|
|
663
1169
|
* call `this.plainText()`.
|
|
664
1170
|
* @param {string} str - Plain text to append
|
|
665
|
-
* @returns {
|
|
1171
|
+
* @returns {this}
|
|
666
1172
|
*/
|
|
667
1173
|
plainText (str) {
|
|
668
|
-
|
|
1174
|
+
this._getJoiningTransformer().plainText(str);
|
|
669
1175
|
return this;
|
|
670
1176
|
}
|
|
671
1177
|
|
|
@@ -673,11 +1179,11 @@ class JSONPathTransformerContext {
|
|
|
673
1179
|
* Set a property value on the current object (JSON joiner). Mirrors the
|
|
674
1180
|
* joining transformer API so templates can call `this.propValue()`.
|
|
675
1181
|
* @param {string} prop - Property name
|
|
676
|
-
* @param {
|
|
677
|
-
* @returns {
|
|
1182
|
+
* @param {any} val - Property value
|
|
1183
|
+
* @returns {this}
|
|
678
1184
|
*/
|
|
679
1185
|
propValue (prop, val) {
|
|
680
|
-
|
|
1186
|
+
this._getJoiningTransformer().propValue(prop, val);
|
|
681
1187
|
return this;
|
|
682
1188
|
}
|
|
683
1189
|
|
|
@@ -686,7 +1192,7 @@ class JSONPathTransformerContext {
|
|
|
686
1192
|
* support both signatures: (obj, cb, usePropertySets, propSets) with seed
|
|
687
1193
|
* object or (cb, usePropertySets, propSets) without.
|
|
688
1194
|
* @param {...any} args - Arguments to pass to joiner
|
|
689
|
-
* @returns {
|
|
1195
|
+
* @returns {this}
|
|
690
1196
|
*/
|
|
691
1197
|
object (...args) {
|
|
692
1198
|
/** @type {any} */ (this._getJoiningTransformer()).object(...args);
|
|
@@ -697,21 +1203,32 @@ class JSONPathTransformerContext {
|
|
|
697
1203
|
* Build an array. Mirrors the joining transformer API. All joiners now
|
|
698
1204
|
* support both signatures: (arr, cb) with seed array or (cb) without.
|
|
699
1205
|
* @param {...any} args - Arguments to pass to joiner
|
|
700
|
-
* @returns {
|
|
1206
|
+
* @returns {this}
|
|
701
1207
|
*/
|
|
702
1208
|
array (...args) {
|
|
703
1209
|
/** @type {any} */ (this._getJoiningTransformer()).array(...args);
|
|
704
1210
|
return this;
|
|
705
1211
|
}
|
|
706
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* Set document-level configuration.
|
|
1215
|
+
* @param {import('./StringJoiningTransformer.js').OutputConfig} cfg Text
|
|
1216
|
+
* @returns {this}
|
|
1217
|
+
*/
|
|
1218
|
+
output (cfg) {
|
|
1219
|
+
this._getJoiningTransformer().output(cfg);
|
|
1220
|
+
return this;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
707
1223
|
/**
|
|
708
1224
|
* Create an element. Mirrors the joining transformer API so templates can
|
|
709
1225
|
* call `this.element()`.
|
|
710
1226
|
* @param {string} name - Element name
|
|
711
|
-
* @param {
|
|
1227
|
+
* @param {Record<string, string>} [atts] - Attributes object
|
|
712
1228
|
* @param {any[]} [children] - Child nodes
|
|
713
|
-
* @param {
|
|
714
|
-
*
|
|
1229
|
+
* @param {import('./JSONJoiningTransformer.js').
|
|
1230
|
+
* SimpleCallback<T>} [cb] - Callback function
|
|
1231
|
+
* @returns {this}
|
|
715
1232
|
*/
|
|
716
1233
|
element (name, atts, children, cb) {
|
|
717
1234
|
/** @type {any} */ (this._getJoiningTransformer()).element(
|
|
@@ -724,13 +1241,12 @@ class JSONPathTransformerContext {
|
|
|
724
1241
|
* Add an attribute to the most recently opened element. Mirrors the joining
|
|
725
1242
|
* transformer API so templates can call `this.attribute()`.
|
|
726
1243
|
* @param {string} name - Attribute name
|
|
727
|
-
* @param {string|
|
|
728
|
-
* @
|
|
729
|
-
* @returns {JSONPathTransformerContext}
|
|
1244
|
+
* @param {string|Record<string, unknown>} val - Attribute value
|
|
1245
|
+
* @returns {this}
|
|
730
1246
|
*/
|
|
731
|
-
attribute (name, val
|
|
1247
|
+
attribute (name, val) {
|
|
732
1248
|
/** @type {any} */ (this._getJoiningTransformer()).attribute(
|
|
733
|
-
name, val
|
|
1249
|
+
name, val
|
|
734
1250
|
);
|
|
735
1251
|
return this;
|
|
736
1252
|
}
|
|
@@ -739,18 +1255,46 @@ class JSONPathTransformerContext {
|
|
|
739
1255
|
* Append text content. Mirrors the joining transformer API so templates can
|
|
740
1256
|
* call `this.text()`.
|
|
741
1257
|
* @param {string} txt - Text content
|
|
742
|
-
* @returns {
|
|
1258
|
+
* @returns {this}
|
|
743
1259
|
*/
|
|
744
1260
|
text (txt) {
|
|
745
|
-
|
|
1261
|
+
this._getJoiningTransformer().text(txt);
|
|
1262
|
+
return this;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Add a comment to the most recently opened element. Mirrors the joining
|
|
1267
|
+
* transformer API so templates can call `this.comment()`.
|
|
1268
|
+
* @param {string} text - Comment text
|
|
1269
|
+
* @returns {this}
|
|
1270
|
+
*/
|
|
1271
|
+
comment (text) {
|
|
1272
|
+
this._getJoiningTransformer().comment(
|
|
1273
|
+
text
|
|
1274
|
+
);
|
|
1275
|
+
return this;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Add a processing instruction to the most recently opened element.
|
|
1280
|
+
* Mirrors the joining transformer API so templates can call
|
|
1281
|
+
* `this.processingInstruction()`.
|
|
1282
|
+
* @param {string} target - Processing instruction target
|
|
1283
|
+
* @param {string} data - Processing instruction data
|
|
1284
|
+
* @returns {this}
|
|
1285
|
+
*/
|
|
1286
|
+
processingInstruction (target, data) {
|
|
1287
|
+
this._getJoiningTransformer().processingInstruction(
|
|
1288
|
+
target, data
|
|
1289
|
+
);
|
|
746
1290
|
return this;
|
|
747
1291
|
}
|
|
748
1292
|
|
|
749
1293
|
/**
|
|
750
1294
|
* @param {string} name - Property set name
|
|
751
|
-
* @param {
|
|
1295
|
+
* @param {Record<string, unknown>} propertySetObj - Property set object
|
|
752
1296
|
* @param {any[]} [usePropertySets] - Property sets to use
|
|
753
|
-
* @returns {
|
|
1297
|
+
* @returns {this}
|
|
754
1298
|
*/
|
|
755
1299
|
propertySet (name, propertySetObj, usePropertySets) {
|
|
756
1300
|
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
@@ -767,9 +1311,9 @@ class JSONPathTransformerContext {
|
|
|
767
1311
|
}
|
|
768
1312
|
|
|
769
1313
|
/**
|
|
770
|
-
* @param {
|
|
1314
|
+
* @param {Record<string, unknown>} obj - Object to assign properties to
|
|
771
1315
|
* @param {string} name - Property set name
|
|
772
|
-
* @returns {
|
|
1316
|
+
* @returns {Record<string, unknown>}
|
|
773
1317
|
*/
|
|
774
1318
|
_usePropertySets (obj, name) {
|
|
775
1319
|
return Object.assign(obj, this.propertySets[name]);
|
|
@@ -777,8 +1321,8 @@ class JSONPathTransformerContext {
|
|
|
777
1321
|
|
|
778
1322
|
/**
|
|
779
1323
|
* @param {string} name - Key name
|
|
780
|
-
* @param {
|
|
781
|
-
* @returns {
|
|
1324
|
+
* @param {any} value - Value to match
|
|
1325
|
+
* @returns {any}
|
|
782
1326
|
*/
|
|
783
1327
|
getKey (name, value) {
|
|
784
1328
|
const key = this.keys[name];
|
|
@@ -796,12 +1340,90 @@ class JSONPathTransformerContext {
|
|
|
796
1340
|
* @param {string} name - Key name
|
|
797
1341
|
* @param {string} match - Match expression
|
|
798
1342
|
* @param {string} use - Use expression
|
|
799
|
-
* @returns {
|
|
1343
|
+
* @returns {this}
|
|
800
1344
|
*/
|
|
801
1345
|
key (name, match, use) {
|
|
802
1346
|
this.keys[name] = {match, use};
|
|
803
1347
|
return this;
|
|
804
1348
|
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Conditionally execute a callback when a JSONPath selector evaluates
|
|
1352
|
+
* to a truthy scalar or a non-empty result set (node set analogue).
|
|
1353
|
+
* Mirrors XSLT's xsl:if semantics where a non-empty node set is truthy.
|
|
1354
|
+
*
|
|
1355
|
+
* Truthiness rules:
|
|
1356
|
+
* - If the selection (with wrap) yields an array with length > 0, the
|
|
1357
|
+
* condition passes.
|
|
1358
|
+
* - Otherwise the (non-wrapped) scalar value is coerced with Boolean();
|
|
1359
|
+
* e.g., 0, '', null, undefined => false; others => true.
|
|
1360
|
+
*
|
|
1361
|
+
* @param {string} select - JSONPath selector expression
|
|
1362
|
+
* @param {(this: JSONPathTransformerContext<T>)
|
|
1363
|
+
* => void} cb - Callback to invoke if condition is met
|
|
1364
|
+
* @returns {this}
|
|
1365
|
+
*/
|
|
1366
|
+
if (select, cb) {
|
|
1367
|
+
const passes = this._passesIf(select);
|
|
1368
|
+
if (passes && typeof cb === 'function') {
|
|
1369
|
+
cb.call(this);
|
|
1370
|
+
}
|
|
1371
|
+
return this;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Internal helper: determine if `select` passes truthiness test.
|
|
1376
|
+
* Non-empty wrapped results => true; single item: objects truthy,
|
|
1377
|
+
* primitives coerced via Boolean().
|
|
1378
|
+
* @param {string} select
|
|
1379
|
+
* @returns {boolean}
|
|
1380
|
+
*/
|
|
1381
|
+
_passesIf (select) {
|
|
1382
|
+
// Evaluate with wrapping to detect non-empty match sets
|
|
1383
|
+
/** @type {any} */ const wrapped = this.get(select, true);
|
|
1384
|
+
if (Array.isArray(wrapped)) {
|
|
1385
|
+
if (wrapped.length === 0) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
if (wrapped.length > 1) {
|
|
1389
|
+
// Multiple matches (node set analogue) => truthy
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
// Single item; apply scalar truthiness
|
|
1393
|
+
const single = wrapped[0];
|
|
1394
|
+
// Objects (arrays) always truthy; primitives use Boolean()
|
|
1395
|
+
if (single && typeof single === 'object') {
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
return Boolean(single);
|
|
1399
|
+
}
|
|
1400
|
+
/* c8 ignore next 3 -- unreachable defensive non-array branch:
|
|
1401
|
+
* jsonpath-plus with wrap:true always returns arrays. */
|
|
1402
|
+
// Fallback if library behavior changed in future
|
|
1403
|
+
return Boolean(wrapped);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Like `if()`, but also supports an optional fallback callback executed
|
|
1408
|
+
* when the test does not pass (similar to xsl:choose/xsl:otherwise).
|
|
1409
|
+
* @param {string} select JSONPath selector
|
|
1410
|
+
* @param {(this: JSONPathTransformerContext<T>)
|
|
1411
|
+
* => void} whenCb Callback when condition passes
|
|
1412
|
+
* @param {(this: JSONPathTransformerContext<T>)
|
|
1413
|
+
* => void} [otherwiseCb] Callback when condition fails
|
|
1414
|
+
* @returns {this}
|
|
1415
|
+
*/
|
|
1416
|
+
choose (select, whenCb, otherwiseCb) {
|
|
1417
|
+
const passes = this._passesIf(select);
|
|
1418
|
+
if (passes) {
|
|
1419
|
+
if (typeof whenCb === 'function') {
|
|
1420
|
+
whenCb.call(this);
|
|
1421
|
+
}
|
|
1422
|
+
} else if (typeof otherwiseCb === 'function') {
|
|
1423
|
+
otherwiseCb.call(this);
|
|
1424
|
+
}
|
|
1425
|
+
return this;
|
|
1426
|
+
}
|
|
805
1427
|
}
|
|
806
1428
|
|
|
807
1429
|
export default JSONPathTransformerContext;
|