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.
Files changed (70) hide show
  1. package/CHANGES.md +27 -0
  2. package/README.md +61 -164
  3. package/demo/calltemplate-params-demo.js +138 -0
  4. package/demo/codemirror.esm.js +28242 -0
  5. package/demo/codemirror.js +94 -0
  6. package/demo/index.css +7 -0
  7. package/demo/index.html +28 -0
  8. package/demo/index.js +210 -0
  9. package/demo/vendor/jamilih/dist/jml.mjs +2341 -0
  10. package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +356 -0
  11. package/demo/vendor/jhtml/src/SAJJ/SAJJ.Stringifier.js +186 -0
  12. package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +746 -0
  13. package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.html +33 -0
  14. package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.testing.js +25 -0
  15. package/demo/vendor/jhtml/src/jhtml-browser.js +5 -0
  16. package/demo/vendor/jhtml/src/jhtml-node.cts +3 -0
  17. package/demo/vendor/jhtml/src/jhtml-node.js +8 -0
  18. package/demo/vendor/jhtml/src/jhtml-node.mts +1 -0
  19. package/demo/vendor/jhtml/src/jhtml.cts +3 -0
  20. package/demo/vendor/jhtml/src/jhtml.js +602 -0
  21. package/demo/vendor/jhtml/src/jhtml.mts +1 -0
  22. package/demo/vendor/jsonpath-plus/dist/index-browser-esm.js +2158 -0
  23. package/demo/vendor/simple-get-json/dist/index-es.js +151 -0
  24. package/demo/xpath2-placeholder.js +1 -0
  25. package/dist/AbstractJoiningTransformer.d.ts +83 -9
  26. package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
  27. package/dist/DOMJoiningTransformer.d.ts +85 -25
  28. package/dist/DOMJoiningTransformer.d.ts.map +1 -1
  29. package/dist/JSONJoiningTransformer.d.ts +159 -51
  30. package/dist/JSONJoiningTransformer.d.ts.map +1 -1
  31. package/dist/JSONPathTransformer.d.ts +37 -38
  32. package/dist/JSONPathTransformer.d.ts.map +1 -1
  33. package/dist/JSONPathTransformerContext.d.ts +334 -124
  34. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  35. package/dist/StringJoiningTransformer.d.ts +132 -41
  36. package/dist/StringJoiningTransformer.d.ts.map +1 -1
  37. package/dist/XPathTransformer.d.ts +35 -20
  38. package/dist/XPathTransformer.d.ts.map +1 -1
  39. package/dist/XPathTransformerContext.d.ts +281 -105
  40. package/dist/XPathTransformerContext.d.ts.map +1 -1
  41. package/dist/index-browser.d.ts +4 -0
  42. package/dist/index-browser.d.ts.map +1 -0
  43. package/dist/index-node.d.ts +4 -0
  44. package/dist/index-node.d.ts.map +1 -0
  45. package/dist/index.d.ts +330 -57
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/types.d.ts +204 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/docs/API.expanded.md +172 -5
  50. package/docs/API.md +92 -2
  51. package/docs/TO-DO.md +168 -0
  52. package/docs/calltemplate-params.md +251 -0
  53. package/eslint.config.js +11 -5
  54. package/package.json +35 -9
  55. package/pnpm-workspace.yaml +1 -0
  56. package/rollup.config.js +13 -0
  57. package/src/AbstractJoiningTransformer.js +54 -15
  58. package/src/DOMJoiningTransformer.js +275 -28
  59. package/src/JSONJoiningTransformer.js +351 -70
  60. package/src/JSONPathTransformer.js +48 -30
  61. package/src/JSONPathTransformerContext.js +729 -107
  62. package/src/StringJoiningTransformer.js +311 -57
  63. package/src/XPathTransformer.js +27 -12
  64. package/src/XPathTransformerContext.js +928 -99
  65. package/src/index-browser.js +5 -0
  66. package/src/index-node.js +7 -0
  67. package/src/index.js +502 -98
  68. package/tsconfig.json +5 -2
  69. package/typings/xpath2-js.d.ts +40 -1
  70. 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
- * @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
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, any>} */
77
+ /** @type {Record<string, unknown>} */
36
78
  this.vars = {};
37
- /** @type {Record<string, any>} */
79
+ /** @type {Record<string, Record<string, unknown>>} */
38
80
  this.propertySets = {};
39
- /** @type {Record<string, any>} */
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 {any} The joining transformer
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 {*} item - Item to append to output
70
- * @returns {JSONPathTransformerContext}
116
+ * @param {string | Node} item - Item to append to output
117
+ * @returns {this}
71
118
  */
72
119
  appendOutput (item) {
73
- /** @type {any} */ (this._getJoiningTransformer()).append(item);
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 {*} The output from the joining transformer
126
+ * @returns {any} The output from the joining transformer
81
127
  */
82
128
  getOutput () {
83
- return /** @type {any} */ (this._getJoiningTransformer()).get();
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 {*} The selected value(s)
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 {*} v - Value to set
107
- * @returns {JSONPathTransformerContext}
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|object} select - JSONPath selector or options object
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 {string|Function|object|Array<string|object>} [sort] - Sort spec
129
- * @returns {JSONPathTransformerContext}
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
- /** @type {{mode?: string, select?: string}} */
139
- const selectObj = /** @type {any} */ (select);
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(function (templateObj) {
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 = pathMatchedTemplates.shift();
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 = templateObj.template.call(
351
- that, value, {mode, parent, parentProperty}
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
- that._getJoiningTransformer().append(ret);
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|object} name - Template name or options object
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 {JSONPathTransformerContext}
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
- /** @type {{name?: string, withParam?: any[]}} */
381
- const nameObj = /** @type {any} */ (name);
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
- const paramValues = withParams.map(function (withParam) {
387
- return withParam.value || that.get(withParam.select, false);
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(function (template) {
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.apply(this, paramValues);
400
- /** @type {any} */ (results).append(result);
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 {Function} cb - Callback function
410
- * @param {string|Function|object|Array<string|object>} [sort] - Sort spec
411
- * @returns {JSONPathTransformerContext}
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 {JSONPathTransformerContext}
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
- const result = select && typeof select === 'object' &&
534
- /** @type {{select?: string}} */ (select).select === '.'
535
- ? this._contextObj
536
- : this.get(/** @type {string} */ (select), false);
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 {JSONPathTransformerContext}
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
- /** @type {any} */ (this._getJoiningTransformer()).append(clone);
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 {JSONPathTransformerContext}
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
- /** @type {any} */ (this._getJoiningTransformer()).append(clone);
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 {JSONPathTransformerContext}
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 {*} json - JSON data to log
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 {Function} cb - Callback function
641
- * @returns {JSONPathTransformerContext}
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. Mirrors the joining transformer API so
651
- * templates can call `this.number()`.
652
- * @param {number} num - Number value to append
653
- * @returns {JSONPathTransformerContext}
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
- /** @type {any} */ (this._getJoiningTransformer()).number(num);
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 {JSONPathTransformerContext}
1171
+ * @returns {this}
666
1172
  */
667
1173
  plainText (str) {
668
- /** @type {any} */ (this._getJoiningTransformer()).plainText(str);
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 {*} val - Property value
677
- * @returns {JSONPathTransformerContext}
1182
+ * @param {any} val - Property value
1183
+ * @returns {this}
678
1184
  */
679
1185
  propValue (prop, val) {
680
- /** @type {any} */ (this._getJoiningTransformer()).propValue(prop, val);
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 {JSONPathTransformerContext}
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 {JSONPathTransformerContext}
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 {object} [atts] - Attributes object
1227
+ * @param {Record<string, string>} [atts] - Attributes object
712
1228
  * @param {any[]} [children] - Child nodes
713
- * @param {Function} [cb] - Callback function
714
- * @returns {JSONPathTransformerContext}
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|object} val - Attribute value
728
- * @param {boolean} [avoidAttEscape] - Whether to avoid escaping
729
- * @returns {JSONPathTransformerContext}
1244
+ * @param {string|Record<string, unknown>} val - Attribute value
1245
+ * @returns {this}
730
1246
  */
731
- attribute (name, val, avoidAttEscape) {
1247
+ attribute (name, val) {
732
1248
  /** @type {any} */ (this._getJoiningTransformer()).attribute(
733
- name, val, avoidAttEscape
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 {JSONPathTransformerContext}
1258
+ * @returns {this}
743
1259
  */
744
1260
  text (txt) {
745
- /** @type {any} */ (this._getJoiningTransformer()).text(txt);
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 {object} propertySetObj - Property set object
1295
+ * @param {Record<string, unknown>} propertySetObj - Property set object
752
1296
  * @param {any[]} [usePropertySets] - Property sets to use
753
- * @returns {JSONPathTransformerContext}
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 {object} obj - Object to assign properties to
1314
+ * @param {Record<string, unknown>} obj - Object to assign properties to
771
1315
  * @param {string} name - Property set name
772
- * @returns {object}
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 {*} value - Value to match
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 {JSONPathTransformerContext}
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;