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,16 @@
1
1
  import xpath2 from 'xpath2.js'; // Runtime JS import; ambient types declared
2
2
  // xpathVersion: 1 => browser/native XPathEvaluator API; 2 => xpath2.js
3
3
 
4
+ /**
5
+ * @typedef {object} XPathTransformerContextConfig
6
+ * @property {unknown} [data] - XML/DOM root to transform
7
+ * @property {number} [xpathVersion] - 1 or 2 (default 1)
8
+ * @property {import('./index.js').
9
+ * JoiningTransformer} joiningTransformer Joiner
10
+ * @property {boolean} [errorOnEqualPriority]
11
+ * @property {(path: string) => number} [specificityPriorityResolver]
12
+ */
13
+
4
14
  /**
5
15
  * Execution context for XPath-driven template application.
6
16
  *
@@ -16,36 +26,35 @@ import xpath2 from 'xpath2.js'; // Runtime JS import; ambient types declared
16
26
  */
17
27
  class XPathTransformerContext {
18
28
  /**
19
- * @param {object} config - Configuration object
20
- * @param {Document|Element|any} config.data - XML/DOM root to transform
21
- * @param {number} [config.xpathVersion] - 1 or 2 (default 1)
22
- * @param {object} config.joiningTransformer Joiner
23
- * @param {Function} config.joiningTransformer.append Append output
24
- * @param {Function} config.joiningTransformer.get Get output
25
- * @param {Function} config.joiningTransformer.string Emit string
26
- * @param {Function} config.joiningTransformer.object Emit object
27
- * @param {Function} config.joiningTransformer.array Emit array
28
- * @param {boolean} [config.errorOnEqualPriority]
29
- * @param {Function} [config.specificityPriorityResolver]
30
- * @param {any[]} templates - Template objects
29
+ * @param {XPathTransformerContextConfig} config
30
+ * @param {import('./index.js').
31
+ * XPathTemplateObject<any>[]} templates - Template objects
31
32
  */
32
33
  constructor (config, templates) {
33
34
  this._config = config;
34
35
  this._templates = templates;
35
- this._contextNode = this._origNode = config.data;
36
- /** @type {Record<string, any>} */
36
+ if (!config.data) {
37
+ throw new Error('XPathTransformerContext requires config.data');
38
+ }
39
+ /** @type {Document|Element|Node} */
40
+ this._contextNode = this._origNode = /** @type {Document|Element|Node} */ (
41
+ config.data
42
+ );
43
+ /** @type {Record<string, unknown>} */
37
44
  this.vars = {};
38
- /** @type {Record<string, any>} */
45
+ /** @type {Record<string, Record<string, unknown>>} */
39
46
  this.propertySets = {};
40
- /** @type {Record<string, any>} */
47
+ /** @type {Record<string, {match: string, use: string}>} */
41
48
  this.keys = {};
42
49
  /** @type {boolean|undefined} */
43
50
  this._initialized = undefined;
44
51
  /** @type {string|undefined} */
45
52
  this._currPath = undefined; // XPath string of current context
53
+ /** @type {Record<string, any> | undefined} */
54
+ this._params = undefined;
46
55
  }
47
56
 
48
- /** @returns {any} */
57
+ /** @returns {import('./index.js').JoiningTransformer} */
49
58
  _getJoiningTransformer () {
50
59
  return this._config.joiningTransformer;
51
60
  }
@@ -53,8 +62,8 @@ class XPathTransformerContext {
53
62
  /**
54
63
  * Evaluate an XPath expression against the current context node.
55
64
  * @param {string} expr - XPath expression
56
- * @param {boolean} [asNodes] Return nodes (array) instead of scalar
57
- * @returns {any}
65
+ * @param {boolean} [asNodes] Return nodes (array) instead of scalar
66
+ * @returns {unknown}
58
67
  */
59
68
  _evalXPath (expr, asNodes) {
60
69
  if (!expr) {
@@ -66,11 +75,13 @@ class XPathTransformerContext {
66
75
  const doc = this._contextNode && this._contextNode.ownerDocument
67
76
  ? this._contextNode.ownerDocument
68
77
  : (this._contextNode.nodeType === 9 ? this._contextNode : undefined);
69
- if (!doc || typeof doc.evaluate !== 'function') {
78
+ if (!doc || doc.nodeType !== 9) {
70
79
  throw new Error(
71
80
  'Native XPath unavailable for xpathVersion=1'
72
81
  );
73
82
  }
83
+ /** @type {Document} */
84
+ const docTyped = /** @type {Document} */ (doc);
74
85
  // Evaluate relative to current node. Namespace support optional.
75
86
  const resolver = null; // Placeholder for future namespaceResolver config
76
87
  /* c8 ignore start -- environment-dependent XPathResult availability */
@@ -86,31 +97,33 @@ class XPathTransformerContext {
86
97
  : 0
87
98
  );
88
99
  /* c8 ignore stop */
89
- const resultObj = doc.evaluate(
100
+ const resultObj = docTyped.evaluate(
90
101
  expr, this._contextNode, resolver, type, null
91
102
  );
92
103
  if (asNodes) {
104
+ /** @type {Node[]} */
93
105
  const arr = [];
94
106
  for (let i = 0; i < resultObj.snapshotLength; i++) {
95
- arr.push(resultObj.snapshotItem(i));
107
+ const item = resultObj.snapshotItem(i);
108
+ if (item) {
109
+ arr.push(item);
110
+ }
96
111
  }
97
112
  return arr;
98
113
  }
99
114
  // Handle primitive types from XPathResult
100
- const XR = globalThis.XPathResult || {};
101
- /* c8 ignore start -- JSDOM's XPath implementation does not properly
102
- * set resultType for STRING_TYPE, NUMBER_TYPE, or BOOLEAN_TYPE. These
103
- * branches work in real browsers but cannot be tested in JSDOM. */
115
+ const XR = docTyped.defaultView?.XPathResult ||
116
+ globalThis.XPathResult || {};
104
117
  switch (resultObj.resultType) {
105
118
  case XR.STRING_TYPE: return resultObj.stringValue;
106
119
  case XR.NUMBER_TYPE: return resultObj.numberValue;
107
- case XR.BOOLEAN_TYPE: return resultObj.booleanValue;
108
- /* c8 ignore stop */
109
- /* c8 ignore start -- iterator result branch env-dependent */
120
+ case XR.BOOLEAN_TYPE:
121
+ return resultObj.booleanValue;
110
122
  case XR.UNORDERED_NODE_ITERATOR_TYPE:
111
123
  case XR.ORDERED_NODE_ITERATOR_TYPE: {
112
124
  /* c8 ignore start -- jsdom yields snapshots; iterator traversal
113
125
  * validated logically but not triggered in this environment. */
126
+ /** @type {Node[]} */
114
127
  const nodes = [];
115
128
  let n = resultObj.iterateNext();
116
129
  while (n) {
@@ -118,8 +131,8 @@ class XPathTransformerContext {
118
131
  n = resultObj.iterateNext();
119
132
  }
120
133
  return nodes;
134
+ /* c8 ignore stop */
121
135
  }
122
- /* c8 ignore stop */
123
136
  /* c8 ignore start -- Default fallback for unsupported XPathResult
124
137
  * types; environment-dependent and not hit under jsdom. */
125
138
  default:
@@ -140,15 +153,18 @@ class XPathTransformerContext {
140
153
 
141
154
  /**
142
155
  * Append raw item to output.
143
- * @param {*} item
156
+ * @param {unknown} item
144
157
  * @returns {XPathTransformerContext}
145
158
  */
146
159
  appendOutput (item) {
147
- this._getJoiningTransformer().append(item);
160
+ // Cast item since we trust the caller provides valid append types
161
+ this._getJoiningTransformer().append(
162
+ /** @type {string | Node} */ (item)
163
+ );
148
164
  return this;
149
165
  }
150
166
 
151
- /** @returns {*} */
167
+ /** @returns {unknown} */
152
168
  getOutput () {
153
169
  return this._getJoiningTransformer().get();
154
170
  }
@@ -157,16 +173,16 @@ class XPathTransformerContext {
157
173
  * Get value(s) by XPath relative to current context.
158
174
  * @param {string} select - XPath expression
159
175
  * @param {boolean} [asNodes]
160
- * @returns {*}
176
+ * @returns {Node[]}
161
177
  */
162
178
  get (select, asNodes) {
163
- return this._evalXPath(select, Boolean(asNodes));
179
+ return /** @type {Node[]} */ (this._evalXPath(select, Boolean(asNodes)));
164
180
  }
165
181
 
166
182
  /**
167
183
  * Set current context's parent property (for parity with JSONPath context).
168
184
  * Mostly placeholder for object-mirroring behavior.
169
- * @param {*} v
185
+ * @param {Document|Element|Node} v
170
186
  * @returns {XPathTransformerContext}
171
187
  */
172
188
  set (v) {
@@ -176,7 +192,7 @@ class XPathTransformerContext {
176
192
 
177
193
  /**
178
194
  * Apply templates to nodes matched by an XPath expression.
179
- * @param {string} select - XPath expression (default '.')
195
+ * @param {string} [select] - XPath expression (default '.')
180
196
  * @param {string} [mode]
181
197
  * @returns {XPathTransformerContext}
182
198
  */
@@ -189,17 +205,28 @@ class XPathTransformerContext {
189
205
  } else {
190
206
  select = select || '*';
191
207
  }
192
- const nodes = this._evalXPath(select, true);
193
- const modeMatched = this._templates.filter((t) => (
194
- mode ? t.mode === mode : !t.mode
195
- ));
208
+ const nodesResult = this._evalXPath(select, true);
209
+ const nodes = /** @type {Node[]} */ (nodesResult);
210
+ const modeMatched = this._templates.filter((t) => {
211
+ // Exclude named-only templates (those with name but no path)
212
+ if (t.name && !t.path) {
213
+ return false;
214
+ }
215
+ return mode ? t.mode === mode : !t.mode;
216
+ });
196
217
  // Process each node
197
218
  for (const node of nodes) {
198
219
  // Path resolution simplified (could track full XPath if needed)
199
220
  const pathMatchedTemplates = modeMatched.filter((t) => {
200
221
  // Basic matching: template.path is XPath tested for existence
222
+ // At this point, we know t.path exists because we filtered
223
+ // out named-only templates in modeMatched
224
+ if (!t.path) {
225
+ return false;
226
+ }
201
227
  try {
202
- const res = this._evalXPath(t.path, true);
228
+ const resResult = this._evalXPath(t.path, true);
229
+ const res = /** @type {Node[]} */ (resResult);
203
230
  return res.includes(node);
204
231
  } catch {
205
232
  return false;
@@ -226,12 +253,12 @@ class XPathTransformerContext {
226
253
  pathMatchedTemplates.sort((a, b) => {
227
254
  const aPr = typeof a.priority === 'number'
228
255
  ? a.priority
229
- : (this._config.specificityPriorityResolver
256
+ : (this._config.specificityPriorityResolver && a.path
230
257
  ? this._config.specificityPriorityResolver(a.path)
231
258
  : 0);
232
259
  const bPr = typeof b.priority === 'number'
233
260
  ? b.priority
234
- : (this._config.specificityPriorityResolver
261
+ : (this._config.specificityPriorityResolver && b.path
235
262
  ? this._config.specificityPriorityResolver(b.path)
236
263
  : 0);
237
264
  if (aPr === bPr && this._config.errorOnEqualPriority) {
@@ -239,7 +266,12 @@ class XPathTransformerContext {
239
266
  }
240
267
  return aPr > bPr ? -1 : 1;
241
268
  });
242
- templateObj = pathMatchedTemplates.shift();
269
+ templateObj =
270
+ /**
271
+ * @type {import('./index.js').XPathTemplateObject<any>}
272
+ */ (
273
+ pathMatchedTemplates.shift()
274
+ );
243
275
  }
244
276
  this._contextNode = node;
245
277
  const ret = templateObj.template.call(this, node, {mode});
@@ -251,14 +283,74 @@ class XPathTransformerContext {
251
283
  return this;
252
284
  }
253
285
 
286
+ /**
287
+ * @param {string|
288
+ * {name: string, withParam?: any[]}} name - Template name or
289
+ * options object
290
+ * @param {any[]} [withParams] - Parameters to pass to template
291
+ * @returns {this}
292
+ */
293
+ callTemplate (name, withParams) {
294
+ // Invokes a named template, optionally passing values via withParam.
295
+ if (name && typeof name === 'object') {
296
+ withParams = name.withParam || withParams;
297
+ ({name} = name);
298
+ }
299
+ withParams = withParams || /* c8 ignore next */ [];
300
+
301
+ // Store parameters in a temporary context for valueOf() access
302
+ const prevParams = this._params;
303
+ /** @type {Record<string, any>} */
304
+ const params = {};
305
+ this._params = params;
306
+
307
+ withParams.forEach((withParam, index) => {
308
+ const value = withParam.value !== undefined
309
+ ? withParam.value
310
+ : this.get(withParam.select, false);
311
+
312
+ // Store by name if provided, otherwise by index
313
+ if (withParam.name) {
314
+ params[withParam.name] = value;
315
+ } else {
316
+ params[String(index)] = value;
317
+ }
318
+ });
319
+
320
+ const results = this._getJoiningTransformer();
321
+ const templateObj = this._templates.find((template) => {
322
+ return template.name === name;
323
+ });
324
+ if (!templateObj) {
325
+ throw new Error(
326
+ 'Template, ' + name + ', cannot be called as it was not found.'
327
+ );
328
+ }
329
+
330
+ // @ts-expect-error Todo: Fix
331
+ const result = templateObj.template.call(this, this._contextNode, {});
332
+ if (typeof result !== 'undefined') {
333
+ /** @type {any} */ (results).append(result);
334
+ }
335
+
336
+ // Restore previous parameter context
337
+ this._params = prevParams;
338
+
339
+ return this;
340
+ }
341
+
342
+
254
343
  /**
255
344
  * Iterate over nodes selected by XPath.
256
345
  * @param {string} select - XPath expression
257
- * @param {Function} cb - Callback invoked per node
346
+ * @param {(this: XPathTransformerContext,
347
+ * node: Node
348
+ * )=>void} cb - Callback invoked per node
258
349
  * @returns {XPathTransformerContext}
259
350
  */
260
351
  forEach (select, cb) {
261
- const nodes = this._evalXPath(select, true);
352
+ const nodesResult = this._evalXPath(select, true);
353
+ const nodes = /** @type {Node[]} */ (nodesResult);
262
354
  for (const n of nodes) {
263
355
  cb.call(this, n);
264
356
  }
@@ -273,19 +365,128 @@ class XPathTransformerContext {
273
365
  valueOf (select) {
274
366
  const jt = this._getJoiningTransformer();
275
367
  let val;
276
- if (!select || (
277
- typeof select === 'object' && /** @type {any} */ (select).select === '.'
278
- )) {
368
+
369
+ const selectStr = typeof select === 'object'
370
+ ? /** @type {{select?: string}} */ (select).select
371
+ : select;
372
+
373
+ // Check if this is a parameter reference (starts with $)
374
+ if (selectStr && selectStr.startsWith('$')) {
375
+ const paramName = selectStr.slice(1);
376
+ if (this._params && paramName in this._params) {
377
+ val = this._params[paramName];
378
+ } else {
379
+ // Fall back to normal XPath evaluation
380
+ const resResult = this._evalXPath(selectStr, true);
381
+ const res = /** @type {Node[]} */ (resResult);
382
+ const first = res[0];
383
+ /* c8 ignore start */
384
+ val = first && first.nodeType
385
+ ? first.textContent
386
+ : first;
387
+ /* c8 ignore stop */
388
+ }
389
+ } else if (!selectStr || selectStr === '.') {
279
390
  val = this._contextNode.nodeType === 3
280
391
  ? this._contextNode.nodeValue
281
392
  : this._contextNode.textContent;
282
393
  } else {
283
- const res = this._evalXPath(/** @type {string} */ (select), true);
394
+ const resResult = this._evalXPath(selectStr, true);
395
+ const res = /** @type {Node[]} */ (resResult);
284
396
  // Simplify: use textContent of first match if node, else raw
285
397
  const first = res[0];
286
398
  val = first && first.nodeType ? first.textContent : first;
287
399
  }
288
- jt.append(val);
400
+ // Ensure val is not null before appending
401
+ if (val !== null) {
402
+ jt.append(val);
403
+ }
404
+ return this;
405
+ }
406
+
407
+ /**
408
+ * Deep copy selection or current context when omitted.
409
+ * For DOM nodes uses cloneNode(true); for scalars copies the value.
410
+ * @param {string} [select] XPath expression selecting nodes (optional)
411
+ * @returns {XPathTransformerContext}
412
+ */
413
+ copyOf (select) {
414
+ /** @type {Node[]} */ let nodes = [];
415
+ if (select) {
416
+ try {
417
+ const res = this.get(select, true);
418
+ nodes = Array.isArray(res) ? res : /* c8 ignore next */ [];
419
+ } catch { /* c8 ignore next */
420
+ nodes = /* c8 ignore next */ [];
421
+ }
422
+ } else {
423
+ nodes = [this._contextNode];
424
+ }
425
+ if (nodes.length) {
426
+ for (const n of nodes) {
427
+ if (n && typeof n === 'object' && 'cloneNode' in n) {
428
+ let deep;
429
+ try {
430
+ deep = /** @type {Node} */ (n.cloneNode(true));
431
+ } catch { /* c8 ignore start */
432
+ deep = /** @type {Node} */ (n.cloneNode(false));
433
+ } /* c8 ignore stop */
434
+ this._getJoiningTransformer().append(/** @type {any} */ (deep));
435
+ } else { /* c8 ignore start */
436
+ this._getJoiningTransformer().append(/** @type {any} */ (n));
437
+ } /* c8 ignore stop */
438
+ }
439
+ } else if (select) { // Scalar path
440
+ const scalar = this._evalXPath(select, false);
441
+ // If scalar evaluation unexpectedly returns a Node/Document, use its
442
+ // textContent instead of attempting to append the Node itself (which
443
+ // can cause HierarchyRequestError for Document nodes).
444
+ if (
445
+ scalar &&
446
+ typeof scalar === 'object' &&
447
+ 'nodeType' in /** @type {any} */ (scalar)
448
+ ) {
449
+ const node = /** @type {Node} */ (scalar);
450
+ let txt = /** @type {any} */ (node.textContent);
451
+ if (/* c8 ignore start */
452
+ (txt === null || typeof txt === 'undefined') &&
453
+ /** @type {any} */ (node).nodeType === 9 // Document
454
+ ) {
455
+ // Fallback to documentElement textContent if available
456
+ const docEl = /** @type {any} */ (
457
+ /** @type {any} */ (node)
458
+ ).documentElement;
459
+ txt = /** @type {any} */ (docEl && docEl.textContent) || '';
460
+ } /* c8 ignore stop */
461
+ this._getJoiningTransformer().append(
462
+ /** @type {any} */ (txt || /* c8 ignore next */ '')
463
+ );
464
+ } else {
465
+ this._getJoiningTransformer().append(/** @type {any} */ (scalar));
466
+ }
467
+ }
468
+ return this;
469
+ }
470
+
471
+ /**
472
+ * Shallow copy current context node (cloneNode(false)); scalars copied
473
+ * directly. Provided for parity with JSONPath copy().
474
+ * @param {string[]} [_propertySets] Ignored in XPath variant (parity only)
475
+ * @returns {XPathTransformerContext}
476
+ */
477
+ copy (_propertySets) {
478
+ const target = this._contextNode;
479
+ let clone;
480
+ if (target && typeof target === 'object' && 'nodeType' in target) {
481
+ try {
482
+ clone = /** @type {Node} */ (target.cloneNode(false));
483
+ } catch { /* c8 ignore start */
484
+ clone = target;
485
+ } /* c8 ignore stop */
486
+ } else { /* c8 ignore start */
487
+ clone = target;
488
+ } /* c8 ignore stop */
489
+ this._getJoiningTransformer().append(/** @type {any} */ (clone));
289
490
  return this;
290
491
  }
291
492
 
@@ -301,21 +502,24 @@ class XPathTransformerContext {
301
502
  }
302
503
  /**
303
504
  * Log a message (for debugging).
304
- * @param {*} json Any value
505
+ * @param {unknown} json Any value
305
506
  * @returns {void}
306
507
  */
307
- static message (json) {
508
+ // eslint-disable-next-line class-methods-use-this -- Convenient
509
+ message (json) {
308
510
  /* eslint-disable-next-line no-console -- Debug output */
309
511
  console.log(json);
310
512
  }
311
513
  /**
312
514
  * Append string.
313
515
  * @param {string} str String to append
314
- * @param {Function} [cb] Callback
516
+ * @param {(this: XPathTransformerContext) => void} [cb] Callback
315
517
  * @returns {XPathTransformerContext}
316
518
  */
317
519
  string (str, cb) {
318
- this._getJoiningTransformer().string(str, cb);
520
+ // We don't pass the callback because it has incompatible 'this' type
521
+ // The callback is mainly used for context-building in string transformers
522
+ this._getJoiningTransformer().string(str);
319
523
  return this;
320
524
  }
321
525
  /**
@@ -339,7 +543,7 @@ class XPathTransformerContext {
339
543
  /**
340
544
  * Append property/value pair.
341
545
  * @param {string} prop Property name
342
- * @param {*} val Value
546
+ * @param {any} val Value
343
547
  * @returns {XPathTransformerContext}
344
548
  */
345
549
  propValue (prop, val) {
@@ -348,43 +552,82 @@ class XPathTransformerContext {
348
552
  }
349
553
  /**
350
554
  * Append object.
351
- * @param {...any} args Object args
555
+ * @param {Record<string, unknown>|
556
+ * ((this: XPathTransformerContext) => void)} objOrCb Object or callback
557
+ * @param {((this: XPathTransformerContext) => void)|
558
+ * any[]} [cbOrUsePropertySets] Callback or property sets
559
+ * @param {any[]|
560
+ * Record<string, unknown>} [usePropertySetsOrPropSets]
561
+ * Property sets or props
562
+ * @param {Record<string, unknown>} [propSets] Additional property sets
352
563
  * @returns {XPathTransformerContext}
353
564
  */
354
- object (...args) {
355
- this._getJoiningTransformer().object(...args);
565
+ object (objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets, propSets) {
566
+ const jt = this._getJoiningTransformer();
567
+ // Union of transformers creates intersection types
568
+ // @ts-expect-error
569
+ jt.object(objOrCb, cbOrUsePropertySets, usePropertySetsOrPropSets,
570
+ propSets);
356
571
  return this;
357
572
  }
358
573
  /**
359
574
  * Append array.
360
- * @param {...any} args Array args
575
+ * @param {any[]|
576
+ * ((this: XPathTransformerContext) => void)} [arrOrCb]
577
+ * Array or callback
578
+ * @param {(this: XPathTransformerContext) => void} [cb] Callback
361
579
  * @returns {XPathTransformerContext}
362
580
  */
363
- array (...args) {
364
- this._getJoiningTransformer().array(...args);
581
+ array (arrOrCb, cb) {
582
+ const jt = this._getJoiningTransformer();
583
+ // Union of transformers creates intersection types
584
+ // @ts-expect-error
585
+ jt.array(arrOrCb, cb);
586
+ return this;
587
+ }
588
+
589
+ /**
590
+ * Append text node content.
591
+ * @param {import('./StringJoiningTransformer.js').OutputConfig} cfg Text
592
+ * @returns {XPathTransformerContext}
593
+ */
594
+ output (cfg) {
595
+ this._getJoiningTransformer().output(cfg);
365
596
  return this;
366
597
  }
598
+
367
599
  /**
368
600
  * Append element.
369
601
  * @param {string} name Tag name
370
- * @param {object} [atts] Attributes
371
- * @param {any[]} [children] Children
372
- * @param {Function} [cb] Callback
602
+ * @param {Record<string, string>|any[]|
603
+ * ((this: XPathTransformerContext)=>void)} [atts] Attributes
604
+ * @param {any[]|((this: XPathTransformerContext)=>void)} [children]
605
+ * Children
606
+ * @param {(this: XPathTransformerContext)=>void} [cb] Callback
373
607
  * @returns {XPathTransformerContext}
374
608
  */
375
609
  element (name, atts, children, cb) {
610
+ // @ts-expect-error - Union of transformers creates intersection types
376
611
  this._getJoiningTransformer().element(name, atts, children, cb);
377
612
  return this;
378
613
  }
379
614
  /**
380
615
  * Append attribute.
381
616
  * @param {string} name Attribute name
382
- * @param {string|object} val Value
617
+ * @param {string|Record<string, unknown>} val Value
383
618
  * @param {boolean} [avoid] Avoid duplicates
384
619
  * @returns {XPathTransformerContext}
385
620
  */
386
621
  attribute (name, val, avoid) {
387
- this._getJoiningTransformer().attribute(name, val, avoid);
622
+ const jt = this._getJoiningTransformer();
623
+ // Only StringJoiningTransformer supports the third parameter
624
+ if (typeof avoid !== 'undefined') {
625
+ // Union of transformers creates intersection types
626
+ // @ts-expect-error
627
+ jt.attribute(name, /** @type {string} */ (val), avoid);
628
+ } else {
629
+ jt.attribute(name, /** @type {string} */ (val));
630
+ }
388
631
  return this;
389
632
  }
390
633
  /**
@@ -396,27 +639,59 @@ class XPathTransformerContext {
396
639
  this._getJoiningTransformer().text(txt);
397
640
  return this;
398
641
  }
642
+
643
+ /**
644
+ * Append a comment.
645
+ * @param {string} text - Comment text
646
+ * @returns {XPathTransformerContext}
647
+ */
648
+ comment (text) {
649
+ const jt = this._getJoiningTransformer();
650
+ if (jt.comment) {
651
+ jt.comment(text);
652
+ }
653
+ return this;
654
+ }
655
+
656
+ /**
657
+ * Append a processing instruction.
658
+ * @param {string} target - Processing instruction target
659
+ * @param {string} data - Processing instruction data
660
+ * @returns {XPathTransformerContext}
661
+ */
662
+ processingInstruction (target, data) {
663
+ const jt = this._getJoiningTransformer();
664
+ if (jt.processingInstruction) {
665
+ jt.processingInstruction(target, data);
666
+ }
667
+ return this;
668
+ }
669
+
399
670
  /**
400
671
  * Define a property set (optionally composed from other sets).
401
672
  * @param {string} name Property set name
402
- * @param {object} obj Base properties
673
+ * @param {Record<string, unknown>} obj Base properties
403
674
  * @param {string[]} [use] Property set names to merge
404
675
  * @returns {XPathTransformerContext}
405
676
  */
406
677
  propertySet (name, obj, use) {
407
- this.propertySets[name] = use
408
- ? ({
409
- ...obj,
410
- ...use.reduce((acc, psName) => this._usePropertySets(acc, psName), {})
411
- })
412
- : obj;
678
+ this.propertySets[name] = /** @type {Record<string, unknown>} */ (
679
+ use
680
+ ? ({
681
+ ...obj,
682
+ ...use.reduce(
683
+ (acc, psName) => this._usePropertySets(acc, psName), {}
684
+ )
685
+ })
686
+ : obj
687
+ );
413
688
  return this;
414
689
  }
415
690
  /**
416
691
  * Merge properties from a named property set into obj.
417
- * @param {object} obj Target object
692
+ * @param {Record<string, unknown>} obj Target object
418
693
  * @param {string} name Property set name
419
- * @returns {object}
694
+ * @returns {Record<string, unknown>}
420
695
  */
421
696
  _usePropertySets (obj, name) {
422
697
  return Object.assign(obj, this.propertySets[name]);
@@ -424,16 +699,20 @@ class XPathTransformerContext {
424
699
  /**
425
700
  * Retrieve a key-mapped node matching a value or return context.
426
701
  * @param {string} name Key name
427
- * @param {*} value Value to match
428
- * @returns {*}
702
+ * @param {any} value Value to match
703
+ * @returns {any}
429
704
  */
430
705
  getKey (name, value) {
431
706
  const key = this.keys[name];
432
707
  const matches = this.get(key.match, true);
433
- for (const m of matches) {
708
+ // When asNodes=true, get() returns Node[]
709
+ /** @type {Node[]} */
710
+ const nodesArray = /** @type {Node[]} */ (matches);
711
+ for (const m of nodesArray) {
434
712
  if (m && m.nodeType === 1) { // Element
435
- if (m.getAttribute && m.getAttribute(key.use) === value) {
436
- return m;
713
+ const elem = /** @type {Element} */ (m);
714
+ if (elem.getAttribute(key.use) === value) {
715
+ return elem;
437
716
  }
438
717
  }
439
718
  }
@@ -451,27 +730,123 @@ class XPathTransformerContext {
451
730
  return this;
452
731
  }
453
732
 
733
+ /**
734
+ * Conditionally execute a callback when an XPath evaluates to a truthy
735
+ * scalar or a non-empty node set (akin to xsl:if semantics).
736
+ *
737
+ * Truthiness rules:
738
+ * - Node set: length > 0 passes.
739
+ * - Scalar: Boolean(value) must be true.
740
+ *
741
+ * @param {string} select XPath expression
742
+ * @param {(this: XPathTransformerContext)
743
+ * => void} cb Callback invoked if condition passes
744
+ * @returns {XPathTransformerContext}
745
+ */
746
+ if (select, cb) {
747
+ const passes = this._passesIf(select);
748
+ if (passes && typeof cb === 'function') {
749
+ cb.call(this);
750
+ }
751
+ return this;
752
+ }
753
+
754
+ /**
755
+ * Internal helper: evaluate XPath truthiness like if().
756
+ * @param {string} select
757
+ * @returns {boolean}
758
+ */
759
+ _passesIf (select) {
760
+ let passes = false;
761
+ // Try scalar evaluation first (handles boolean/comparison expressions)
762
+ try {
763
+ const scalar = this.get(select, false);
764
+ let normalized;
765
+ // Unwrap single-item array if it contains a primitive
766
+ /* c8 ignore next 6 -- Defensive code for edge case where _evalXPath
767
+ * returns single-item array with asNodes=false; both native XPath v1
768
+ * and xpath2.js v2 return scalars directly in standard usage. */
769
+ if (
770
+ Array.isArray(scalar) &&
771
+ scalar.length === 1 &&
772
+ ['boolean', 'number', 'string'].includes(typeof scalar[0])
773
+ ) {
774
+ normalized = scalar[0];
775
+ } else if (['boolean', 'number', 'string'].includes(typeof scalar)) {
776
+ normalized = scalar;
777
+ }
778
+ if (typeof normalized !== 'undefined') {
779
+ passes = Boolean(normalized);
780
+ }
781
+ } catch {
782
+ // Scalar eval failed; will try node selection
783
+ }
784
+ // If not yet truthy, attempt node selection (location paths)
785
+ if (!passes && (/[\/@*]/v).test(select)) {
786
+ try {
787
+ const nodes = this.get(select, true);
788
+ // eslint-disable-next-line unicorn/prefer-ternary -- for coverage
789
+ if (Array.isArray(nodes)) {
790
+ passes = nodes.length > 0;
791
+ // Defensive for non-array nodes, but _evalXPath with asNodes=true
792
+ // always returns arrays in both v1 and v2.
793
+ /* c8 ignore start */
794
+ } else {
795
+ passes = Boolean(nodes);
796
+ }
797
+ /* c8 ignore stop */
798
+ } catch {
799
+ passes = false;
800
+ }
801
+ }
802
+ return passes;
803
+ }
804
+
805
+ /**
806
+ * Conditional with optional fallback (like choose/otherwise).
807
+ * Truthiness same as `if()`.
808
+ * @param {string} select XPath expression
809
+ * @param {(this: XPathTransformerContext)
810
+ * => void} whenCb Callback when condition passes
811
+ * @param {(this: XPathTransformerContext)
812
+ * => void} [otherwiseCb] Callback when condition fails
813
+ * @returns {XPathTransformerContext}
814
+ */
815
+ choose (select, whenCb, otherwiseCb) {
816
+ const passes = this._passesIf(select);
817
+ if (passes) {
818
+ if (typeof whenCb === 'function') {
819
+ whenCb.call(this);
820
+ }
821
+ } else if (typeof otherwiseCb === 'function') {
822
+ otherwiseCb.call(this);
823
+ }
824
+ return this;
825
+ }
826
+
454
827
  /* c8 ignore start -- static default rules object has spotty function
455
828
  * attribution under coverage; behavior is exercised via applyTemplates */
456
829
  static DefaultTemplateRules = {
457
830
  transformRoot: {
458
831
  /**
459
- * @param {*} node Root node
832
+ * @this {XPathTransformerContext}
833
+ * @param {unknown} node Root node
460
834
  * @param {{mode:string}} cfg Config
461
835
  * @returns {void}
462
836
  */
463
837
  template (node, cfg) {
464
- /** @type {any} */ (this).applyTemplates('.', cfg.mode);
838
+ this.applyTemplates('.', cfg.mode);
465
839
  }
466
840
  },
467
841
  transformElements: {
468
842
  /**
469
- * @param {*} node Element node
470
- * @param {{mode:string}} cfg Config
843
+ * @this {XPathTransformerContext}
844
+ * @param {unknown} node Element node
845
+ * @param {{mode?:string}} cfg Config
471
846
  * @returns {void}
472
847
  */
473
848
  template (node, cfg) {
474
- /** @type {any} */ (this).applyTemplates('*', cfg.mode);
849
+ this.applyTemplates('*', cfg.mode);
475
850
  }
476
851
  },
477
852
  transformTextNodes: {
@@ -484,9 +859,12 @@ class XPathTransformerContext {
484
859
  }
485
860
  },
486
861
  transformScalars: {
487
- /** @returns {*} */
862
+ /**
863
+ * @this {XPathTransformerContext}
864
+ * @returns {XPathTransformerContext}
865
+ */
488
866
  template () {
489
- return /** @type {any} */ (this).valueOf({select: '.'});
867
+ return this.valueOf({select: '.'});
490
868
  }
491
869
  }
492
870
  };