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.
- package/CHANGES.md +18 -0
- package/README.md +16 -87
- package/demo/calltemplate-params-demo.js +138 -0
- package/demo/index.html +31 -0
- package/demo/index.js +30 -0
- package/demo/xpath2-placeholder.js +1 -0
- package/dist/AbstractJoiningTransformer.d.ts +83 -9
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/DOMJoiningTransformer.d.ts +85 -25
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts +159 -51
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts +37 -38
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +247 -121
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts +132 -41
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts +35 -20
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +191 -99
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index-browser.d.ts +4 -0
- package/dist/index-browser.d.ts.map +1 -0
- package/dist/index-node.d.ts +4 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index.d.ts +330 -57
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +204 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/API.expanded.md +167 -2
- package/docs/API.md +91 -1
- package/docs/TO-DO.md +144 -0
- package/docs/calltemplate-params.md +251 -0
- package/eslint.config.js +9 -5
- package/package.json +13 -7
- package/pnpm-workspace.yaml +1 -0
- package/src/AbstractJoiningTransformer.js +54 -15
- package/src/DOMJoiningTransformer.js +275 -28
- package/src/JSONJoiningTransformer.js +351 -70
- package/src/JSONPathTransformer.js +48 -30
- package/src/JSONPathTransformerContext.js +308 -104
- package/src/StringJoiningTransformer.js +311 -57
- package/src/XPathTransformer.js +27 -12
- package/src/XPathTransformerContext.js +467 -89
- package/src/index-browser.js +5 -0
- package/src/index-node.js +7 -0
- package/src/index.js +498 -97
- package/typings/xpath2-js.d.ts +40 -1
- 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 {
|
|
20
|
-
* @param {
|
|
21
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
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,
|
|
45
|
+
/** @type {Record<string, Record<string, unknown>>} */
|
|
39
46
|
this.propertySets = {};
|
|
40
|
-
/** @type {Record<string,
|
|
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 {
|
|
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
|
-
|
|
57
|
-
* @returns {
|
|
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 ||
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
101
|
-
|
|
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:
|
|
108
|
-
|
|
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 {
|
|
156
|
+
* @param {unknown} item
|
|
144
157
|
* @returns {XPathTransformerContext}
|
|
145
158
|
*/
|
|
146
159
|
appendOutput (item) {
|
|
147
|
-
|
|
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 {
|
|
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
|
|
193
|
-
const
|
|
194
|
-
|
|
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
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
505
|
+
* @param {unknown} json Any value
|
|
305
506
|
* @returns {void}
|
|
306
507
|
*/
|
|
307
|
-
|
|
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 {
|
|
516
|
+
* @param {(this: XPathTransformerContext) => void} [cb] Callback
|
|
315
517
|
* @returns {XPathTransformerContext}
|
|
316
518
|
*/
|
|
317
519
|
string (str, cb) {
|
|
318
|
-
this
|
|
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 {
|
|
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 {
|
|
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 (
|
|
355
|
-
this._getJoiningTransformer()
|
|
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 {
|
|
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 (
|
|
364
|
-
this._getJoiningTransformer()
|
|
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 {
|
|
371
|
-
*
|
|
372
|
-
* @param {
|
|
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|
|
|
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()
|
|
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 {
|
|
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] =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 {
|
|
692
|
+
* @param {Record<string, unknown>} obj Target object
|
|
418
693
|
* @param {string} name Property set name
|
|
419
|
-
* @returns {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
838
|
+
this.applyTemplates('.', cfg.mode);
|
|
465
839
|
}
|
|
466
840
|
},
|
|
467
841
|
transformElements: {
|
|
468
842
|
/**
|
|
469
|
-
* @
|
|
470
|
-
* @param {
|
|
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
|
-
|
|
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
|
-
/**
|
|
862
|
+
/**
|
|
863
|
+
* @this {XPathTransformerContext}
|
|
864
|
+
* @returns {XPathTransformerContext}
|
|
865
|
+
*/
|
|
488
866
|
template () {
|
|
489
|
-
return
|
|
867
|
+
return this.valueOf({select: '.'});
|
|
490
868
|
}
|
|
491
869
|
}
|
|
492
870
|
};
|