jtlt 0.2.0 → 0.3.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 (50) hide show
  1. package/CHANGES.md +18 -0
  2. package/README.md +16 -87
  3. package/demo/calltemplate-params-demo.js +138 -0
  4. package/demo/index.html +31 -0
  5. package/demo/index.js +30 -0
  6. package/demo/xpath2-placeholder.js +1 -0
  7. package/dist/AbstractJoiningTransformer.d.ts +83 -9
  8. package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
  9. package/dist/DOMJoiningTransformer.d.ts +85 -25
  10. package/dist/DOMJoiningTransformer.d.ts.map +1 -1
  11. package/dist/JSONJoiningTransformer.d.ts +159 -51
  12. package/dist/JSONJoiningTransformer.d.ts.map +1 -1
  13. package/dist/JSONPathTransformer.d.ts +37 -38
  14. package/dist/JSONPathTransformer.d.ts.map +1 -1
  15. package/dist/JSONPathTransformerContext.d.ts +247 -121
  16. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  17. package/dist/StringJoiningTransformer.d.ts +132 -41
  18. package/dist/StringJoiningTransformer.d.ts.map +1 -1
  19. package/dist/XPathTransformer.d.ts +35 -20
  20. package/dist/XPathTransformer.d.ts.map +1 -1
  21. package/dist/XPathTransformerContext.d.ts +191 -99
  22. package/dist/XPathTransformerContext.d.ts.map +1 -1
  23. package/dist/index-browser.d.ts +4 -0
  24. package/dist/index-browser.d.ts.map +1 -0
  25. package/dist/index-node.d.ts +4 -0
  26. package/dist/index-node.d.ts.map +1 -0
  27. package/dist/index.d.ts +330 -57
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/types.d.ts +204 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/docs/API.expanded.md +167 -2
  32. package/docs/API.md +91 -1
  33. package/docs/TO-DO.md +144 -0
  34. package/docs/calltemplate-params.md +251 -0
  35. package/eslint.config.js +9 -5
  36. package/package.json +13 -7
  37. package/pnpm-workspace.yaml +1 -0
  38. package/src/AbstractJoiningTransformer.js +54 -15
  39. package/src/DOMJoiningTransformer.js +275 -28
  40. package/src/JSONJoiningTransformer.js +351 -70
  41. package/src/JSONPathTransformer.js +48 -30
  42. package/src/JSONPathTransformerContext.js +308 -104
  43. package/src/StringJoiningTransformer.js +311 -57
  44. package/src/XPathTransformer.js +27 -12
  45. package/src/XPathTransformerContext.js +467 -89
  46. package/src/index-browser.js +5 -0
  47. package/src/index-node.js +7 -0
  48. package/src/index.js +498 -97
  49. package/typings/xpath2-js.d.ts +40 -1
  50. package/src/types/xpath2-js.d.ts +0 -2
@@ -1,6 +1,41 @@
1
1
  import {JSONPath as jsonpath} from 'jsonpath-plus';
2
2
  import JSONPathTransformer from './JSONPathTransformer.js';
3
3
 
4
+ /**
5
+ * Sort spec types used by applyTemplates() and forEach().
6
+ * @typedef {{
7
+ * select?: string,
8
+ * order?: 'ascending' | 'descending',
9
+ * type?: 'text'|'number',
10
+ * locale?: string,
11
+ * localeOptions?: unknown
12
+ * }} SortObject
13
+ * @typedef {(a: unknown, b: unknown,
14
+ * ctx: JSONPathTransformerContext
15
+ * ) => number} SortComparator
16
+ * @typedef {string | SortObject | SortComparator |
17
+ * Array<string|SortObject>} SortSpec
18
+ */
19
+
20
+ /**
21
+ * @template [T = "json"]
22
+ * @typedef {object} JSONPathTransformerContextConfig
23
+ * @property {null|boolean|number|string|object} data - Data to transform
24
+ * @property {object} [parent] - Parent object
25
+ * @property {string} [parentProperty] - Parent property name
26
+ * @property {boolean} [errorOnEqualPriority] - Whether to error on
27
+ * equal priority
28
+ * @property {T extends "json" ? import('./JSONJoiningTransformer.js').
29
+ * default : T extends "string" ? import('./StringJoiningTransformer.js').
30
+ * default : import('./DOMJoiningTransformer.js').
31
+ * default} joiningTransformer - Joining transformer
32
+ * @property {boolean} [preventEval] - Whether to prevent eval in
33
+ * JSONPath
34
+ * @property {(path: string) => number} [specificityPriorityResolver]
35
+ * Priority resolver function
36
+ * @property {import('./index.js').JSONPathTemplateObject<T>[]} templates
37
+ */
38
+
4
39
  /**
5
40
  * Execution context for JSONPath-driven template application.
6
41
  *
@@ -8,23 +43,13 @@ import JSONPathTransformer from './JSONPathTransformer.js';
8
43
  * running templates. Exposes helper methods that mirror the underlying
9
44
  * joining transformer (e.g., string(), object(), array()) so templates can
10
45
  * emit results without referencing the joiner directly.
46
+ * @template [T = "json"]
11
47
  */
12
48
  class JSONPathTransformerContext {
13
49
  /**
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
50
+ * @param {JSONPathTransformerContextConfig<T>} config
51
+ * @param {import('./index.js').JSONPathTemplateObject<T>[]} templates - Array
52
+ * of template objects
28
53
  */
29
54
  constructor (config, templates) {
30
55
  this._config = config;
@@ -32,16 +57,18 @@ class JSONPathTransformerContext {
32
57
  this._contextObj = this._origObj = config.data;
33
58
  this._parent = config.parent || this._config;
34
59
  this._parentProperty = config.parentProperty || 'data';
35
- /** @type {Record<string, any>} */
60
+ /** @type {Record<string, unknown>} */
36
61
  this.vars = {};
37
- /** @type {Record<string, any>} */
62
+ /** @type {Record<string, Record<string, unknown>>} */
38
63
  this.propertySets = {};
39
- /** @type {Record<string, any>} */
64
+ /** @type {Record<string, {match: string, use: string}>} */
40
65
  this.keys = {};
41
66
  /** @type {boolean | undefined} */
42
67
  this._initialized = undefined;
43
68
  /** @type {string | undefined} */
44
69
  this._currPath = undefined;
70
+ /** @type {Record<string, any> | undefined} */
71
+ this._params = undefined;
45
72
  }
46
73
 
47
74
  /**
@@ -59,28 +86,30 @@ class JSONPathTransformerContext {
59
86
 
60
87
  /**
61
88
  * Gets the joining transformer from config.
62
- * @returns {any} The joining transformer
89
+ * @returns {T extends "json" ? import('./JSONJoiningTransformer.js').
90
+ * default : T extends "string" ? import('./StringJoiningTransformer.js').
91
+ * default : import('./DOMJoiningTransformer.js').
92
+ * default} The joining transformer
63
93
  */
64
94
  _getJoiningTransformer () {
65
95
  return this._config.joiningTransformer;
66
96
  }
67
97
 
68
98
  /**
69
- * @param {*} item - Item to append to output
70
- * @returns {JSONPathTransformerContext}
99
+ * @param {string | Node} item - Item to append to output
100
+ * @returns {this}
71
101
  */
72
102
  appendOutput (item) {
73
- /** @type {any} */ (this._getJoiningTransformer()).append(item);
103
+ this._getJoiningTransformer().append(item);
74
104
  return this;
75
105
  }
76
106
 
77
- /* c8 ignore next 4 -- JSDoc block incorrectly counted as coverable by c8 */
78
107
  /**
79
108
  * Gets the current output.
80
- * @returns {*} The output from the joining transformer
109
+ * @returns {any} The output from the joining transformer
81
110
  */
82
111
  getOutput () {
83
- return /** @type {any} */ (this._getJoiningTransformer()).get();
112
+ return this._getJoiningTransformer().get();
84
113
  }
85
114
 
86
115
  /**
@@ -89,7 +118,7 @@ class JSONPathTransformerContext {
89
118
  * to the result tree instead).
90
119
  * @param {string} select - JSONPath selector
91
120
  * @param {boolean} wrap - Whether to wrap results
92
- * @returns {*} The selected value(s)
121
+ * @returns {any} The selected value(s)
93
122
  */
94
123
  get (select, wrap) {
95
124
  if (select) {
@@ -103,8 +132,8 @@ class JSONPathTransformerContext {
103
132
  }
104
133
 
105
134
  /**
106
- * @param {*} v - Value to set
107
- * @returns {JSONPathTransformerContext}
135
+ * @param {any} v - Value to set
136
+ * @returns {this}
108
137
  */
109
138
  set (v) {
110
139
  (/** @type {Record<string, any>} */ (this._parent))[
@@ -123,10 +152,12 @@ class JSONPathTransformerContext {
123
152
  * locale, localeOptions }
124
153
  * - array: multiple key objects/strings in priority order.
125
154
  *
126
- * @param {string|object} select - JSONPath selector or options object
155
+ * @param {string|null|
156
+ * {mode?: string, select?: string}
157
+ * } [select] - JSONPath selector or options object
127
158
  * @param {string} [mode] - Mode to apply
128
- * @param {string|Function|object|Array<string|object>} [sort] - Sort spec
129
- * @returns {JSONPathTransformerContext}
159
+ * @param {SortSpec} [sort] - Sort spec
160
+ * @returns {this}
130
161
  */
131
162
  applyTemplates (select, mode, sort) {
132
163
  // Matches templates by (path, mode), resolves priority, and invokes each
@@ -135,10 +166,8 @@ class JSONPathTransformerContext {
135
166
  // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
136
167
  const that = this;
137
168
  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;
169
+ mode = select.mode ?? mode;
170
+ select = select.select ?? select;
142
171
  }
143
172
  if (!this._initialized) {
144
173
  select = select || '$';
@@ -154,7 +183,11 @@ class JSONPathTransformerContext {
154
183
  const jsonPathExpr = propertyNamesMode ? select.slice(0, -1) : select;
155
184
  // Todo: Use results here?
156
185
  /* const results = */ this._getJoiningTransformer();
157
- const modeMatchedTemplates = this._templates.filter(function (templateObj) {
186
+ const modeMatchedTemplates = this._templates.filter((templateObj) => {
187
+ // Exclude named-only templates (those with name but no path)
188
+ if (templateObj.name && !templateObj.path) {
189
+ return false;
190
+ }
158
191
  return ((mode && mode === templateObj.mode) ||
159
192
  (!mode && !templateObj.mode));
160
193
  });
@@ -162,7 +195,7 @@ class JSONPathTransformerContext {
162
195
  // Collect matches first (to allow sorting), then process
163
196
  /**
164
197
  * @type {{
165
- * value:any, parent:any, parentProperty?:string, path:string
198
+ * value: any, parent: any, parentProperty?: string, path: string
166
199
  * }[]}
167
200
  */
168
201
  const matches = /** @type {any} */ (jsonpath)({
@@ -231,7 +264,7 @@ class JSONPathTransformerContext {
231
264
  }
232
265
  /**
233
266
  * @param {any} sortSpec
234
- * @returns {((a:{value:any}, b:{value:any})=>number)|null}
267
+ * @returns {((a: {value: any}, b: {value: any}) => number) | null}
235
268
  */
236
269
  function buildComparator (sortSpec) {
237
270
  if (!sortSpec) {
@@ -283,6 +316,13 @@ class JSONPathTransformerContext {
283
316
  that._currPath += path.replace(/^\$/v, '');
284
317
  const pathMatchedTemplates = modeMatchedTemplates.filter(
285
318
  function (templateObj) {
319
+ // At this point, we know templateObj.path exists because we filtered
320
+ // out named-only templates in modeMatchedTemplates
321
+ /* c8 ignore start -- defensive check, already filtered at line 188 */
322
+ if (!templateObj.path) {
323
+ return false;
324
+ }
325
+ /* c8 ignore stop */
286
326
  const queryResult = /** @type {any[]} */ (
287
327
  (/** @type {any} */ (jsonpath))({
288
328
  path: JSONPathTransformer.makeJSONPathAbsolute(
@@ -323,12 +363,12 @@ class JSONPathTransformerContext {
323
363
  * trigger error, making the `: 0` branch nearly unreachable. */
324
364
  const aPriority = typeof a.priority === 'number'
325
365
  ? a.priority
326
- : (that._config.specificityPriorityResolver
366
+ : (that._config.specificityPriorityResolver && a.path
327
367
  ? that._config.specificityPriorityResolver(a.path)
328
368
  : 0);
329
369
  const bPriority = typeof b.priority === 'number'
330
370
  ? b.priority
331
- : (that._config.specificityPriorityResolver
371
+ : (that._config.specificityPriorityResolver && b.path
332
372
  ? that._config.specificityPriorityResolver(b.path)
333
373
  : 0);
334
374
  /* c8 ignore stop */
@@ -340,18 +380,27 @@ class JSONPathTransformerContext {
340
380
  return (aPriority > bPriority) ? -1 : 1;
341
381
  });
342
382
 
343
- templateObj = pathMatchedTemplates.shift();
383
+ templateObj =
384
+ /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
385
+ pathMatchedTemplates.shift()
386
+ );
344
387
  }
345
388
 
346
389
  that._contextObj = value;
347
390
  that._parent = parent;
348
391
  that._parentProperty = (parentProperty ?? that._parentProperty);
349
392
 
350
- const ret = templateObj.template.call(
351
- that, value, {mode, parent, parentProperty}
352
- );
393
+ const ret =
394
+ /** @type {import('./index.js').JSONPathTemplateObject<T>} */ (
395
+ templateObj
396
+ ).template.call(
397
+ that, value, {mode, parent, parentProperty}
398
+ );
353
399
  if (typeof ret !== 'undefined') {
354
- that._getJoiningTransformer().append(ret);
400
+ // After the undefined check, ret is ResultType<T>
401
+ that._getJoiningTransformer().append(
402
+ /** @type {string|Node|*} */ (ret)
403
+ );
355
404
  }
356
405
 
357
406
  that._contextObj = value;
@@ -368,26 +417,41 @@ class JSONPathTransformerContext {
368
417
  }
369
418
 
370
419
  /**
371
- * @param {string|object} name - Template name or options object
420
+ * @param {string|
421
+ * {name: string, withParam?: any[]}} name - Template name or
422
+ * options object
372
423
  * @param {any[]} [withParams] - Parameters to pass to template
373
- * @returns {JSONPathTransformerContext}
424
+ * @returns {this}
374
425
  */
375
426
  callTemplate (name, withParams) {
376
427
  // Invokes a named template, optionally passing values via withParam.
377
- // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
378
- const that = this;
379
428
  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;
429
+ withParams = name.withParam || withParams;
430
+ ({name} = name);
384
431
  }
385
432
  withParams = withParams || [];
386
- const paramValues = withParams.map(function (withParam) {
387
- return withParam.value || that.get(withParam.select, false);
433
+
434
+ // Store parameters in a temporary context for valueOf() access
435
+ const prevParams = this._params;
436
+ /** @type {Record<string, any>} */
437
+ const params = {};
438
+ this._params = params;
439
+
440
+ withParams.forEach((withParam, index) => {
441
+ const value = withParam.value !== undefined
442
+ ? withParam.value
443
+ : this.get(withParam.select, false);
444
+
445
+ // Store by name if provided, otherwise by index
446
+ if (withParam.name) {
447
+ params[withParam.name] = value;
448
+ } else {
449
+ params[String(index)] = value;
450
+ }
388
451
  });
452
+
389
453
  const results = this._getJoiningTransformer();
390
- const templateObj = this._templates.find(function (template) {
454
+ const templateObj = this._templates.find((template) => {
391
455
  return template.name === name;
392
456
  });
393
457
  if (!templateObj) {
@@ -396,8 +460,14 @@ class JSONPathTransformerContext {
396
460
  );
397
461
  }
398
462
 
399
- const result = templateObj.template.apply(this, paramValues);
400
- /** @type {any} */ (results).append(result);
463
+ const result = templateObj.template.call(this, this._contextObj, {});
464
+ if (typeof result !== 'undefined') {
465
+ /** @type {any} */ (results).append(result);
466
+ }
467
+
468
+ // Restore previous parameter context
469
+ this._params = prevParams;
470
+
401
471
  return this;
402
472
  }
403
473
 
@@ -406,14 +476,16 @@ class JSONPathTransformerContext {
406
476
  *
407
477
  * Sort parameter forms are the same as applyTemplates().
408
478
  * @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}
479
+ * @param {(this: JSONPathTransformerContext<T>,
480
+ * value: any
481
+ * ) => void} cb - Callback function
482
+ * @param {SortSpec} [sort] - Sort spec
483
+ * @returns {this}
412
484
  */
413
485
  forEach (select, cb, sort) {
414
486
  // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
415
487
  const that = this;
416
- /** @type {{value:any}[]} */
488
+ /** @type {{value: any}[]} */
417
489
  const matches = /** @type {any} */ (jsonpath)({
418
490
  path: select,
419
491
  json: this._contextObj,
@@ -478,7 +550,7 @@ class JSONPathTransformerContext {
478
550
  }
479
551
  /**
480
552
  * @param {any} sortSpec
481
- * @returns {((a:{value:any}, b:{value:any})=>number)|null}
553
+ * @returns {((a: {value: any}, b: {value: any}) => number) | null}
482
554
  */
483
555
  function feBuildComparator (sortSpec) {
484
556
  if (!sortSpec) {
@@ -524,16 +596,33 @@ class JSONPathTransformerContext {
524
596
 
525
597
  /**
526
598
  * @param {string|object} [select] - JSONPath selector
527
- * @returns {JSONPathTransformerContext}
599
+ * @returns {this}
528
600
  */
529
601
  valueOf (select) {
530
602
  // Appends the value of the given JSONPath (or the current context when
531
603
  // `{select: '.'}` is passed) to the output via the joining transformer.
532
604
  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);
605
+ let result;
606
+
607
+ if (select && typeof select === 'object' &&
608
+ /** @type {{select?: string}} */ (select).select === '.') {
609
+ result = this._contextObj;
610
+ } else {
611
+ const selectStr = typeof select === 'object'
612
+ ? /** @type {{select?: string}} */ (select).select
613
+ : select;
614
+
615
+ // Check if this is a parameter reference (starts with $)
616
+ if (selectStr && selectStr.startsWith('$')) {
617
+ const paramName = selectStr.slice(1);
618
+ result = (this._params && paramName in this._params)
619
+ ? this._params[paramName]
620
+ : this.get(/** @type {string} */ (selectStr), false);
621
+ } else {
622
+ result = this.get(/** @type {string} */ (select), false);
623
+ }
624
+ }
625
+
537
626
  /** @type {any} */ (results).append(result);
538
627
  return this;
539
628
  }
@@ -541,7 +630,7 @@ class JSONPathTransformerContext {
541
630
  /**
542
631
  * Deep copy selection or current context when omitted.
543
632
  * @param {string} [select] - JSONPath selector
544
- * @returns {JSONPathTransformerContext}
633
+ * @returns {this}
545
634
  */
546
635
  copyOf (select) { // Deep
547
636
  // Deeply clones the value at `select` (or current context if omitted)
@@ -567,11 +656,9 @@ class JSONPathTransformerContext {
567
656
  * vary across environments; behavior covered by tests. */
568
657
  // structuredClone failed (e.g., Symbols); if any functions present
569
658
  // on own enumerable string-keyed properties, preserve via shallow.
570
- /** @type {boolean} */ let hasFunc = false;
571
659
  for (const k of Object.keys(val)) {
572
660
  const v = /** @type {any} */ (val)[k];
573
661
  if (typeof v === 'function') {
574
- hasFunc = true;
575
662
  break;
576
663
  }
577
664
  }
@@ -584,14 +671,14 @@ class JSONPathTransformerContext {
584
671
  // Primitives/functions copied by value/reference semantics naturally.
585
672
  clone = val;
586
673
  }
587
- /** @type {any} */ (this._getJoiningTransformer()).append(clone);
674
+ this._getJoiningTransformer().append(clone);
588
675
  return this;
589
676
  }
590
677
 
591
678
  /**
592
679
  * Shallow copy current context; optionally merge property set names.
593
680
  * @param {string[]} [propertySets] - Property sets to merge
594
- * @returns {JSONPathTransformerContext}
681
+ * @returns {this}
595
682
  */
596
683
  copy (propertySets) { // Shallow
597
684
  // Creates a shallow clone of current context object/array (or primitive)
@@ -611,14 +698,14 @@ class JSONPathTransformerContext {
611
698
  } else { /* c8 ignore start -- primitive branch attribution variance */
612
699
  clone = src; // Primitive/function - nothing to shallow clone
613
700
  } /* c8 ignore stop */
614
- /** @type {any} */ (this._getJoiningTransformer()).append(clone);
701
+ this._getJoiningTransformer().append(clone);
615
702
  return this;
616
703
  }
617
704
 
618
705
  /**
619
706
  * @param {string} name - Variable name
620
707
  * @param {string} select - JSONPath selector
621
- * @returns {JSONPathTransformerContext}
708
+ * @returns {this}
622
709
  */
623
710
  variable (name, select) {
624
711
  this.vars[name] = this.get(select, false);
@@ -626,7 +713,7 @@ class JSONPathTransformerContext {
626
713
  }
627
714
 
628
715
  /**
629
- * @param {*} json - JSON data to log
716
+ * @param {unknown} json - JSON data to log
630
717
  * @returns {void}
631
718
  */
632
719
  // eslint-disable-next-line class-methods-use-this -- Convenient
@@ -637,10 +724,11 @@ class JSONPathTransformerContext {
637
724
 
638
725
  /**
639
726
  * @param {string} str - String value
640
- * @param {Function} cb - Callback function
641
- * @returns {JSONPathTransformerContext}
727
+ * @param {import('./JSONJoiningTransformer.js').
728
+ * SimpleCallback<T>} [cb] - Optional callback to build nested
729
+ * string content
730
+ * @returns {this}
642
731
  */
643
- // Todo: Add other methods from the joining transformers
644
732
  string (str, cb) {
645
733
  /** @type {any} */ (this._getJoiningTransformer()).string(str, cb);
646
734
  return this;
@@ -650,10 +738,10 @@ class JSONPathTransformerContext {
650
738
  * Append a number to JSON output. Mirrors the joining transformer API so
651
739
  * templates can call `this.number()`.
652
740
  * @param {number} num - Number value to append
653
- * @returns {JSONPathTransformerContext}
741
+ * @returns {this}
654
742
  */
655
743
  number (num) {
656
- /** @type {any} */ (this._getJoiningTransformer()).number(num);
744
+ this._getJoiningTransformer().number(num);
657
745
  return this;
658
746
  }
659
747
 
@@ -662,10 +750,10 @@ class JSONPathTransformerContext {
662
750
  * stringification. Mirrors the joining transformer API so templates can
663
751
  * call `this.plainText()`.
664
752
  * @param {string} str - Plain text to append
665
- * @returns {JSONPathTransformerContext}
753
+ * @returns {this}
666
754
  */
667
755
  plainText (str) {
668
- /** @type {any} */ (this._getJoiningTransformer()).plainText(str);
756
+ this._getJoiningTransformer().plainText(str);
669
757
  return this;
670
758
  }
671
759
 
@@ -673,11 +761,11 @@ class JSONPathTransformerContext {
673
761
  * Set a property value on the current object (JSON joiner). Mirrors the
674
762
  * joining transformer API so templates can call `this.propValue()`.
675
763
  * @param {string} prop - Property name
676
- * @param {*} val - Property value
677
- * @returns {JSONPathTransformerContext}
764
+ * @param {any} val - Property value
765
+ * @returns {this}
678
766
  */
679
767
  propValue (prop, val) {
680
- /** @type {any} */ (this._getJoiningTransformer()).propValue(prop, val);
768
+ this._getJoiningTransformer().propValue(prop, val);
681
769
  return this;
682
770
  }
683
771
 
@@ -686,7 +774,7 @@ class JSONPathTransformerContext {
686
774
  * support both signatures: (obj, cb, usePropertySets, propSets) with seed
687
775
  * object or (cb, usePropertySets, propSets) without.
688
776
  * @param {...any} args - Arguments to pass to joiner
689
- * @returns {JSONPathTransformerContext}
777
+ * @returns {this}
690
778
  */
691
779
  object (...args) {
692
780
  /** @type {any} */ (this._getJoiningTransformer()).object(...args);
@@ -697,21 +785,32 @@ class JSONPathTransformerContext {
697
785
  * Build an array. Mirrors the joining transformer API. All joiners now
698
786
  * support both signatures: (arr, cb) with seed array or (cb) without.
699
787
  * @param {...any} args - Arguments to pass to joiner
700
- * @returns {JSONPathTransformerContext}
788
+ * @returns {this}
701
789
  */
702
790
  array (...args) {
703
791
  /** @type {any} */ (this._getJoiningTransformer()).array(...args);
704
792
  return this;
705
793
  }
706
794
 
795
+ /**
796
+ * Set document-level configuration.
797
+ * @param {import('./StringJoiningTransformer.js').OutputConfig} cfg Text
798
+ * @returns {this}
799
+ */
800
+ output (cfg) {
801
+ this._getJoiningTransformer().output(cfg);
802
+ return this;
803
+ }
804
+
707
805
  /**
708
806
  * Create an element. Mirrors the joining transformer API so templates can
709
807
  * call `this.element()`.
710
808
  * @param {string} name - Element name
711
- * @param {object} [atts] - Attributes object
809
+ * @param {Record<string, string>} [atts] - Attributes object
712
810
  * @param {any[]} [children] - Child nodes
713
- * @param {Function} [cb] - Callback function
714
- * @returns {JSONPathTransformerContext}
811
+ * @param {import('./JSONJoiningTransformer.js').
812
+ * SimpleCallback<T>} [cb] - Callback function
813
+ * @returns {this}
715
814
  */
716
815
  element (name, atts, children, cb) {
717
816
  /** @type {any} */ (this._getJoiningTransformer()).element(
@@ -724,13 +823,12 @@ class JSONPathTransformerContext {
724
823
  * Add an attribute to the most recently opened element. Mirrors the joining
725
824
  * transformer API so templates can call `this.attribute()`.
726
825
  * @param {string} name - Attribute name
727
- * @param {string|object} val - Attribute value
728
- * @param {boolean} [avoidAttEscape] - Whether to avoid escaping
729
- * @returns {JSONPathTransformerContext}
826
+ * @param {string|Record<string, unknown>} val - Attribute value
827
+ * @returns {this}
730
828
  */
731
- attribute (name, val, avoidAttEscape) {
829
+ attribute (name, val) {
732
830
  /** @type {any} */ (this._getJoiningTransformer()).attribute(
733
- name, val, avoidAttEscape
831
+ name, val
734
832
  );
735
833
  return this;
736
834
  }
@@ -739,18 +837,46 @@ class JSONPathTransformerContext {
739
837
  * Append text content. Mirrors the joining transformer API so templates can
740
838
  * call `this.text()`.
741
839
  * @param {string} txt - Text content
742
- * @returns {JSONPathTransformerContext}
840
+ * @returns {this}
743
841
  */
744
842
  text (txt) {
745
- /** @type {any} */ (this._getJoiningTransformer()).text(txt);
843
+ this._getJoiningTransformer().text(txt);
844
+ return this;
845
+ }
846
+
847
+ /**
848
+ * Add a comment to the most recently opened element. Mirrors the joining
849
+ * transformer API so templates can call `this.comment()`.
850
+ * @param {string} text - Comment text
851
+ * @returns {this}
852
+ */
853
+ comment (text) {
854
+ this._getJoiningTransformer().comment(
855
+ text
856
+ );
857
+ return this;
858
+ }
859
+
860
+ /**
861
+ * Add a processing instruction to the most recently opened element.
862
+ * Mirrors the joining transformer API so templates can call
863
+ * `this.processingInstruction()`.
864
+ * @param {string} target - Processing instruction target
865
+ * @param {string} data - Processing instruction data
866
+ * @returns {this}
867
+ */
868
+ processingInstruction (target, data) {
869
+ this._getJoiningTransformer().processingInstruction(
870
+ target, data
871
+ );
746
872
  return this;
747
873
  }
748
874
 
749
875
  /**
750
876
  * @param {string} name - Property set name
751
- * @param {object} propertySetObj - Property set object
877
+ * @param {Record<string, unknown>} propertySetObj - Property set object
752
878
  * @param {any[]} [usePropertySets] - Property sets to use
753
- * @returns {JSONPathTransformerContext}
879
+ * @returns {this}
754
880
  */
755
881
  propertySet (name, propertySetObj, usePropertySets) {
756
882
  // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
@@ -767,9 +893,9 @@ class JSONPathTransformerContext {
767
893
  }
768
894
 
769
895
  /**
770
- * @param {object} obj - Object to assign properties to
896
+ * @param {Record<string, unknown>} obj - Object to assign properties to
771
897
  * @param {string} name - Property set name
772
- * @returns {object}
898
+ * @returns {Record<string, unknown>}
773
899
  */
774
900
  _usePropertySets (obj, name) {
775
901
  return Object.assign(obj, this.propertySets[name]);
@@ -777,8 +903,8 @@ class JSONPathTransformerContext {
777
903
 
778
904
  /**
779
905
  * @param {string} name - Key name
780
- * @param {*} value - Value to match
781
- * @returns {*}
906
+ * @param {any} value - Value to match
907
+ * @returns {any}
782
908
  */
783
909
  getKey (name, value) {
784
910
  const key = this.keys[name];
@@ -796,12 +922,90 @@ class JSONPathTransformerContext {
796
922
  * @param {string} name - Key name
797
923
  * @param {string} match - Match expression
798
924
  * @param {string} use - Use expression
799
- * @returns {JSONPathTransformerContext}
925
+ * @returns {this}
800
926
  */
801
927
  key (name, match, use) {
802
928
  this.keys[name] = {match, use};
803
929
  return this;
804
930
  }
931
+
932
+ /**
933
+ * Conditionally execute a callback when a JSONPath selector evaluates
934
+ * to a truthy scalar or a non-empty result set (node set analogue).
935
+ * Mirrors XSLT's xsl:if semantics where a non-empty node set is truthy.
936
+ *
937
+ * Truthiness rules:
938
+ * - If the selection (with wrap) yields an array with length > 0, the
939
+ * condition passes.
940
+ * - Otherwise the (non-wrapped) scalar value is coerced with Boolean();
941
+ * e.g., 0, '', null, undefined => false; others => true.
942
+ *
943
+ * @param {string} select - JSONPath selector expression
944
+ * @param {(this: JSONPathTransformerContext<T>)
945
+ * => void} cb - Callback to invoke if condition is met
946
+ * @returns {this}
947
+ */
948
+ if (select, cb) {
949
+ const passes = this._passesIf(select);
950
+ if (passes && typeof cb === 'function') {
951
+ cb.call(this);
952
+ }
953
+ return this;
954
+ }
955
+
956
+ /**
957
+ * Internal helper: determine if `select` passes truthiness test.
958
+ * Non-empty wrapped results => true; single item: objects truthy,
959
+ * primitives coerced via Boolean().
960
+ * @param {string} select
961
+ * @returns {boolean}
962
+ */
963
+ _passesIf (select) {
964
+ // Evaluate with wrapping to detect non-empty match sets
965
+ /** @type {any} */ const wrapped = this.get(select, true);
966
+ if (Array.isArray(wrapped)) {
967
+ if (wrapped.length === 0) {
968
+ return false;
969
+ }
970
+ if (wrapped.length > 1) {
971
+ // Multiple matches (node set analogue) => truthy
972
+ return true;
973
+ }
974
+ // Single item; apply scalar truthiness
975
+ const single = wrapped[0];
976
+ // Objects (arrays) always truthy; primitives use Boolean()
977
+ if (single && typeof single === 'object') {
978
+ return true;
979
+ }
980
+ return Boolean(single);
981
+ }
982
+ /* c8 ignore next 3 -- unreachable defensive non-array branch:
983
+ * jsonpath-plus with wrap:true always returns arrays. */
984
+ // Fallback if library behavior changed in future
985
+ return Boolean(wrapped);
986
+ }
987
+
988
+ /**
989
+ * Like `if()`, but also supports an optional fallback callback executed
990
+ * when the test does not pass (similar to xsl:choose/xsl:otherwise).
991
+ * @param {string} select JSONPath selector
992
+ * @param {(this: JSONPathTransformerContext<T>)
993
+ * => void} whenCb Callback when condition passes
994
+ * @param {(this: JSONPathTransformerContext<T>)
995
+ * => void} [otherwiseCb] Callback when condition fails
996
+ * @returns {this}
997
+ */
998
+ choose (select, whenCb, otherwiseCb) {
999
+ const passes = this._passesIf(select);
1000
+ if (passes) {
1001
+ if (typeof whenCb === 'function') {
1002
+ whenCb.call(this);
1003
+ }
1004
+ } else if (typeof otherwiseCb === 'function') {
1005
+ otherwiseCb.call(this);
1006
+ }
1007
+ return this;
1008
+ }
805
1009
  }
806
1010
 
807
1011
  export default JSONPathTransformerContext;