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.
Files changed (43) hide show
  1. package/.editorconfig +16 -0
  2. package/CHANGES.md +5 -0
  3. package/LICENSE-MIT.txt +21 -0
  4. package/README.md +534 -0
  5. package/dist/AbstractJoiningTransformer.d.ts +42 -0
  6. package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
  7. package/dist/DOMJoiningTransformer.d.ts +113 -0
  8. package/dist/DOMJoiningTransformer.d.ts.map +1 -0
  9. package/dist/JSONJoiningTransformer.d.ts +160 -0
  10. package/dist/JSONJoiningTransformer.d.ts.map +1 -0
  11. package/dist/JSONPathTransformer.d.ts +95 -0
  12. package/dist/JSONPathTransformer.d.ts.map +1 -0
  13. package/dist/JSONPathTransformerContext.d.ts +263 -0
  14. package/dist/JSONPathTransformerContext.d.ts.map +1 -0
  15. package/dist/StringJoiningTransformer.d.ts +168 -0
  16. package/dist/StringJoiningTransformer.d.ts.map +1 -0
  17. package/dist/XPathTransformer.d.ts +51 -0
  18. package/dist/XPathTransformer.d.ts.map +1 -0
  19. package/dist/XPathTransformerContext.d.ts +260 -0
  20. package/dist/XPathTransformerContext.d.ts.map +1 -0
  21. package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
  22. package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
  23. package/dist/index.d.ts +168 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/docs/API.expanded.md +263 -0
  26. package/docs/API.md +69 -0
  27. package/eslint.config.js +30 -0
  28. package/package.json +53 -0
  29. package/pnpm-workspace.yaml +3 -0
  30. package/src/AbstractJoiningTransformer.js +73 -0
  31. package/src/DOMJoiningTransformer.js +237 -0
  32. package/src/JSONJoiningTransformer.js +472 -0
  33. package/src/JSONPathTransformer.js +159 -0
  34. package/src/JSONPathTransformerContext.js +807 -0
  35. package/src/StringJoiningTransformer.js +589 -0
  36. package/src/XPathTransformer.js +94 -0
  37. package/src/XPathTransformerContext.js +496 -0
  38. package/src/XSLTStyleJSONPathResolver.js +39 -0
  39. package/src/index.js +299 -0
  40. package/src/types/xpath2-js.d.ts +2 -0
  41. package/tsconfig-prod.json +19 -0
  42. package/tsconfig.json +14 -0
  43. 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;