jtlt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +16 -0
- package/CHANGES.md +5 -0
- package/LICENSE-MIT.txt +21 -0
- package/README.md +534 -0
- package/dist/AbstractJoiningTransformer.d.ts +42 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
- package/dist/DOMJoiningTransformer.d.ts +113 -0
- package/dist/DOMJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONJoiningTransformer.d.ts +160 -0
- package/dist/JSONJoiningTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformer.d.ts +95 -0
- package/dist/JSONPathTransformer.d.ts.map +1 -0
- package/dist/JSONPathTransformerContext.d.ts +263 -0
- package/dist/JSONPathTransformerContext.d.ts.map +1 -0
- package/dist/StringJoiningTransformer.d.ts +168 -0
- package/dist/StringJoiningTransformer.d.ts.map +1 -0
- package/dist/XPathTransformer.d.ts +51 -0
- package/dist/XPathTransformer.d.ts.map +1 -0
- package/dist/XPathTransformerContext.d.ts +260 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
- package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.d.ts.map +1 -0
- package/docs/API.expanded.md +263 -0
- package/docs/API.md +69 -0
- package/eslint.config.js +30 -0
- package/package.json +53 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/AbstractJoiningTransformer.js +73 -0
- package/src/DOMJoiningTransformer.js +237 -0
- package/src/JSONJoiningTransformer.js +472 -0
- package/src/JSONPathTransformer.js +159 -0
- package/src/JSONPathTransformerContext.js +807 -0
- package/src/StringJoiningTransformer.js +589 -0
- package/src/XPathTransformer.js +94 -0
- package/src/XPathTransformerContext.js +496 -0
- package/src/XSLTStyleJSONPathResolver.js +39 -0
- package/src/index.js +299 -0
- package/src/types/xpath2-js.d.ts +2 -0
- package/tsconfig-prod.json +19 -0
- package/tsconfig.json +14 -0
- package/typings/xpath2-js.d.ts +2 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import xpath2 from 'xpath2.js'; // Runtime JS import; ambient types declared
|
|
2
|
+
// xpathVersion: 1 => browser/native XPathEvaluator API; 2 => xpath2.js
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execution context for XPath-driven template application.
|
|
6
|
+
*
|
|
7
|
+
* Similar to JSONPathTransformerContext but uses XPath expressions on a
|
|
8
|
+
* DOM/XML-like tree. Supports XPath 1.0 (default) or 2.0 when
|
|
9
|
+
* `xpathVersion: 2`.
|
|
10
|
+
*
|
|
11
|
+
* Expected config:
|
|
12
|
+
* - data: A Document, Element, or XML-like root node.
|
|
13
|
+
* - joiningTransformer: joiner with append(), string(), object(), array(), etc.
|
|
14
|
+
* - xpathVersion: 1|2 (default 1)
|
|
15
|
+
* - errorOnEqualPriority, specificityPriorityResolver (same semantics).
|
|
16
|
+
*/
|
|
17
|
+
class XPathTransformerContext {
|
|
18
|
+
/**
|
|
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
|
|
31
|
+
*/
|
|
32
|
+
constructor (config, templates) {
|
|
33
|
+
this._config = config;
|
|
34
|
+
this._templates = templates;
|
|
35
|
+
this._contextNode = this._origNode = config.data;
|
|
36
|
+
/** @type {Record<string, any>} */
|
|
37
|
+
this.vars = {};
|
|
38
|
+
/** @type {Record<string, any>} */
|
|
39
|
+
this.propertySets = {};
|
|
40
|
+
/** @type {Record<string, any>} */
|
|
41
|
+
this.keys = {};
|
|
42
|
+
/** @type {boolean|undefined} */
|
|
43
|
+
this._initialized = undefined;
|
|
44
|
+
/** @type {string|undefined} */
|
|
45
|
+
this._currPath = undefined; // XPath string of current context
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @returns {any} */
|
|
49
|
+
_getJoiningTransformer () {
|
|
50
|
+
return this._config.joiningTransformer;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate an XPath expression against the current context node.
|
|
55
|
+
* @param {string} expr - XPath expression
|
|
56
|
+
* @param {boolean} [asNodes] Return nodes (array) instead of scalar
|
|
57
|
+
* @returns {any}
|
|
58
|
+
*/
|
|
59
|
+
_evalXPath (expr, asNodes) {
|
|
60
|
+
if (!expr) {
|
|
61
|
+
return this._contextNode;
|
|
62
|
+
}
|
|
63
|
+
const version = this._config.xpathVersion === 2 ? 2 : 1;
|
|
64
|
+
if (version === 1) {
|
|
65
|
+
// Use native XPath (browser-like); rely on DOM doc if available.
|
|
66
|
+
const doc = this._contextNode && this._contextNode.ownerDocument
|
|
67
|
+
? this._contextNode.ownerDocument
|
|
68
|
+
: (this._contextNode.nodeType === 9 ? this._contextNode : undefined);
|
|
69
|
+
if (!doc || typeof doc.evaluate !== 'function') {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'Native XPath unavailable for xpathVersion=1'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
// Evaluate relative to current node. Namespace support optional.
|
|
75
|
+
const resolver = null; // Placeholder for future namespaceResolver config
|
|
76
|
+
/* c8 ignore start -- environment-dependent XPathResult availability */
|
|
77
|
+
const type = asNodes
|
|
78
|
+
? (
|
|
79
|
+
globalThis.XPathResult
|
|
80
|
+
? globalThis.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
|
|
81
|
+
: 7
|
|
82
|
+
)
|
|
83
|
+
: (
|
|
84
|
+
globalThis.XPathResult
|
|
85
|
+
? globalThis.XPathResult.ANY_TYPE
|
|
86
|
+
: 0
|
|
87
|
+
);
|
|
88
|
+
/* c8 ignore stop */
|
|
89
|
+
const resultObj = doc.evaluate(
|
|
90
|
+
expr, this._contextNode, resolver, type, null
|
|
91
|
+
);
|
|
92
|
+
if (asNodes) {
|
|
93
|
+
const arr = [];
|
|
94
|
+
for (let i = 0; i < resultObj.snapshotLength; i++) {
|
|
95
|
+
arr.push(resultObj.snapshotItem(i));
|
|
96
|
+
}
|
|
97
|
+
return arr;
|
|
98
|
+
}
|
|
99
|
+
// 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. */
|
|
104
|
+
switch (resultObj.resultType) {
|
|
105
|
+
case XR.STRING_TYPE: return resultObj.stringValue;
|
|
106
|
+
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 */
|
|
110
|
+
case XR.UNORDERED_NODE_ITERATOR_TYPE:
|
|
111
|
+
case XR.ORDERED_NODE_ITERATOR_TYPE: {
|
|
112
|
+
/* c8 ignore start -- jsdom yields snapshots; iterator traversal
|
|
113
|
+
* validated logically but not triggered in this environment. */
|
|
114
|
+
const nodes = [];
|
|
115
|
+
let n = resultObj.iterateNext();
|
|
116
|
+
while (n) {
|
|
117
|
+
nodes.push(n);
|
|
118
|
+
n = resultObj.iterateNext();
|
|
119
|
+
}
|
|
120
|
+
return nodes;
|
|
121
|
+
}
|
|
122
|
+
/* c8 ignore stop */
|
|
123
|
+
/* c8 ignore start -- Default fallback for unsupported XPathResult
|
|
124
|
+
* types; environment-dependent and not hit under jsdom. */
|
|
125
|
+
default:
|
|
126
|
+
// Fallback: return original context for unsupported types
|
|
127
|
+
return this._contextNode;
|
|
128
|
+
/* c8 ignore stop */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Version 2: xpath2.js
|
|
132
|
+
const result = xpath2.evaluate(expr, this._contextNode);
|
|
133
|
+
if (asNodes) {
|
|
134
|
+
/* c8 ignore next -- array wrap/identity branch counted in other tests */
|
|
135
|
+
return Array.isArray(result) ? result : [result];
|
|
136
|
+
}
|
|
137
|
+
/* c8 ignore next -- scalar return trivial; wrap behavior tested */
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Append raw item to output.
|
|
143
|
+
* @param {*} item
|
|
144
|
+
* @returns {XPathTransformerContext}
|
|
145
|
+
*/
|
|
146
|
+
appendOutput (item) {
|
|
147
|
+
this._getJoiningTransformer().append(item);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @returns {*} */
|
|
152
|
+
getOutput () {
|
|
153
|
+
return this._getJoiningTransformer().get();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get value(s) by XPath relative to current context.
|
|
158
|
+
* @param {string} select - XPath expression
|
|
159
|
+
* @param {boolean} [asNodes]
|
|
160
|
+
* @returns {*}
|
|
161
|
+
*/
|
|
162
|
+
get (select, asNodes) {
|
|
163
|
+
return this._evalXPath(select, Boolean(asNodes));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Set current context's parent property (for parity with JSONPath context).
|
|
168
|
+
* Mostly placeholder for object-mirroring behavior.
|
|
169
|
+
* @param {*} v
|
|
170
|
+
* @returns {XPathTransformerContext}
|
|
171
|
+
*/
|
|
172
|
+
set (v) {
|
|
173
|
+
this._contextNode = v;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Apply templates to nodes matched by an XPath expression.
|
|
179
|
+
* @param {string} select - XPath expression (default '.')
|
|
180
|
+
* @param {string} [mode]
|
|
181
|
+
* @returns {XPathTransformerContext}
|
|
182
|
+
*/
|
|
183
|
+
applyTemplates (select, mode) {
|
|
184
|
+
// Initialization similar to JSONPath context
|
|
185
|
+
if (!this._initialized) {
|
|
186
|
+
select = select || '.';
|
|
187
|
+
this._currPath = '.'; // Root context indicator
|
|
188
|
+
this._initialized = true;
|
|
189
|
+
} else {
|
|
190
|
+
select = select || '*';
|
|
191
|
+
}
|
|
192
|
+
const nodes = this._evalXPath(select, true);
|
|
193
|
+
const modeMatched = this._templates.filter((t) => (
|
|
194
|
+
mode ? t.mode === mode : !t.mode
|
|
195
|
+
));
|
|
196
|
+
// Process each node
|
|
197
|
+
for (const node of nodes) {
|
|
198
|
+
// Path resolution simplified (could track full XPath if needed)
|
|
199
|
+
const pathMatchedTemplates = modeMatched.filter((t) => {
|
|
200
|
+
// Basic matching: template.path is XPath tested for existence
|
|
201
|
+
try {
|
|
202
|
+
const res = this._evalXPath(t.path, true);
|
|
203
|
+
return res.includes(node);
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
let templateObj;
|
|
209
|
+
if (!pathMatchedTemplates.length) { // default template rule branches
|
|
210
|
+
// Default template rules (simplified compared to JSON version)
|
|
211
|
+
const DTR = XPathTransformerContext.DefaultTemplateRules;
|
|
212
|
+
// Treat Document (9) like Element (1) so the default root rule
|
|
213
|
+
// descends into children when no template matches the Document.
|
|
214
|
+
/* c8 ignore start -- nodeType default-rule union env-stable */
|
|
215
|
+
if (node && (node.nodeType === 1 || node.nodeType === 9)) {
|
|
216
|
+
// Element or Document
|
|
217
|
+
templateObj = DTR.transformElements;
|
|
218
|
+
} else if (node && node.nodeType === 3) { // Text
|
|
219
|
+
templateObj = DTR.transformTextNodes;
|
|
220
|
+
} else {
|
|
221
|
+
templateObj = DTR.transformScalars;
|
|
222
|
+
}
|
|
223
|
+
/* c8 ignore stop */
|
|
224
|
+
} else {
|
|
225
|
+
// Sort by priority (numeric or specificity resolver)
|
|
226
|
+
pathMatchedTemplates.sort((a, b) => {
|
|
227
|
+
const aPr = typeof a.priority === 'number'
|
|
228
|
+
? a.priority
|
|
229
|
+
: (this._config.specificityPriorityResolver
|
|
230
|
+
? this._config.specificityPriorityResolver(a.path)
|
|
231
|
+
: 0);
|
|
232
|
+
const bPr = typeof b.priority === 'number'
|
|
233
|
+
? b.priority
|
|
234
|
+
: (this._config.specificityPriorityResolver
|
|
235
|
+
? this._config.specificityPriorityResolver(b.path)
|
|
236
|
+
: 0);
|
|
237
|
+
if (aPr === bPr && this._config.errorOnEqualPriority) {
|
|
238
|
+
throw new Error('Equal priority templates found.');
|
|
239
|
+
}
|
|
240
|
+
return aPr > bPr ? -1 : 1;
|
|
241
|
+
});
|
|
242
|
+
templateObj = pathMatchedTemplates.shift();
|
|
243
|
+
}
|
|
244
|
+
this._contextNode = node;
|
|
245
|
+
const ret = templateObj.template.call(this, node, {mode});
|
|
246
|
+
if (typeof ret !== 'undefined') {
|
|
247
|
+
this._getJoiningTransformer().append(ret);
|
|
248
|
+
}
|
|
249
|
+
this._contextNode = node; // Restore (placeholder for more complex state)
|
|
250
|
+
}
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Iterate over nodes selected by XPath.
|
|
256
|
+
* @param {string} select - XPath expression
|
|
257
|
+
* @param {Function} cb - Callback invoked per node
|
|
258
|
+
* @returns {XPathTransformerContext}
|
|
259
|
+
*/
|
|
260
|
+
forEach (select, cb) {
|
|
261
|
+
const nodes = this._evalXPath(select, true);
|
|
262
|
+
for (const n of nodes) {
|
|
263
|
+
cb.call(this, n);
|
|
264
|
+
}
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Append the value from an XPath expression or the context node text.
|
|
270
|
+
* @param {string|object} [select]
|
|
271
|
+
* @returns {XPathTransformerContext}
|
|
272
|
+
*/
|
|
273
|
+
valueOf (select) {
|
|
274
|
+
const jt = this._getJoiningTransformer();
|
|
275
|
+
let val;
|
|
276
|
+
if (!select || (
|
|
277
|
+
typeof select === 'object' && /** @type {any} */ (select).select === '.'
|
|
278
|
+
)) {
|
|
279
|
+
val = this._contextNode.nodeType === 3
|
|
280
|
+
? this._contextNode.nodeValue
|
|
281
|
+
: this._contextNode.textContent;
|
|
282
|
+
} else {
|
|
283
|
+
const res = this._evalXPath(/** @type {string} */ (select), true);
|
|
284
|
+
// Simplify: use textContent of first match if node, else raw
|
|
285
|
+
const first = res[0];
|
|
286
|
+
val = first && first.nodeType ? first.textContent : first;
|
|
287
|
+
}
|
|
288
|
+
jt.append(val);
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Define a variable by XPath selection (stores node array if nodes).
|
|
294
|
+
* @param {string} name Variable name
|
|
295
|
+
* @param {string} select XPath expression
|
|
296
|
+
* @returns {XPathTransformerContext}
|
|
297
|
+
*/
|
|
298
|
+
variable (name, select) {
|
|
299
|
+
this.vars[name] = this.get(select, true);
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Log a message (for debugging).
|
|
304
|
+
* @param {*} json Any value
|
|
305
|
+
* @returns {void}
|
|
306
|
+
*/
|
|
307
|
+
static message (json) {
|
|
308
|
+
/* eslint-disable-next-line no-console -- Debug output */
|
|
309
|
+
console.log(json);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Append string.
|
|
313
|
+
* @param {string} str String to append
|
|
314
|
+
* @param {Function} [cb] Callback
|
|
315
|
+
* @returns {XPathTransformerContext}
|
|
316
|
+
*/
|
|
317
|
+
string (str, cb) {
|
|
318
|
+
this._getJoiningTransformer().string(str, cb);
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Append number.
|
|
323
|
+
* @param {number} num Number
|
|
324
|
+
* @returns {XPathTransformerContext}
|
|
325
|
+
*/
|
|
326
|
+
number (num) {
|
|
327
|
+
this._getJoiningTransformer().number(num);
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Append plain text (no escaping changes).
|
|
332
|
+
* @param {string} str Text
|
|
333
|
+
* @returns {XPathTransformerContext}
|
|
334
|
+
*/
|
|
335
|
+
plainText (str) {
|
|
336
|
+
this._getJoiningTransformer().plainText(str);
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Append property/value pair.
|
|
341
|
+
* @param {string} prop Property name
|
|
342
|
+
* @param {*} val Value
|
|
343
|
+
* @returns {XPathTransformerContext}
|
|
344
|
+
*/
|
|
345
|
+
propValue (prop, val) {
|
|
346
|
+
this._getJoiningTransformer().propValue(prop, val);
|
|
347
|
+
return this;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Append object.
|
|
351
|
+
* @param {...any} args Object args
|
|
352
|
+
* @returns {XPathTransformerContext}
|
|
353
|
+
*/
|
|
354
|
+
object (...args) {
|
|
355
|
+
this._getJoiningTransformer().object(...args);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Append array.
|
|
360
|
+
* @param {...any} args Array args
|
|
361
|
+
* @returns {XPathTransformerContext}
|
|
362
|
+
*/
|
|
363
|
+
array (...args) {
|
|
364
|
+
this._getJoiningTransformer().array(...args);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Append element.
|
|
369
|
+
* @param {string} name Tag name
|
|
370
|
+
* @param {object} [atts] Attributes
|
|
371
|
+
* @param {any[]} [children] Children
|
|
372
|
+
* @param {Function} [cb] Callback
|
|
373
|
+
* @returns {XPathTransformerContext}
|
|
374
|
+
*/
|
|
375
|
+
element (name, atts, children, cb) {
|
|
376
|
+
this._getJoiningTransformer().element(name, atts, children, cb);
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Append attribute.
|
|
381
|
+
* @param {string} name Attribute name
|
|
382
|
+
* @param {string|object} val Value
|
|
383
|
+
* @param {boolean} [avoid] Avoid duplicates
|
|
384
|
+
* @returns {XPathTransformerContext}
|
|
385
|
+
*/
|
|
386
|
+
attribute (name, val, avoid) {
|
|
387
|
+
this._getJoiningTransformer().attribute(name, val, avoid);
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Append text node content.
|
|
392
|
+
* @param {string} txt Text
|
|
393
|
+
* @returns {XPathTransformerContext}
|
|
394
|
+
*/
|
|
395
|
+
text (txt) {
|
|
396
|
+
this._getJoiningTransformer().text(txt);
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Define a property set (optionally composed from other sets).
|
|
401
|
+
* @param {string} name Property set name
|
|
402
|
+
* @param {object} obj Base properties
|
|
403
|
+
* @param {string[]} [use] Property set names to merge
|
|
404
|
+
* @returns {XPathTransformerContext}
|
|
405
|
+
*/
|
|
406
|
+
propertySet (name, obj, use) {
|
|
407
|
+
this.propertySets[name] = use
|
|
408
|
+
? ({
|
|
409
|
+
...obj,
|
|
410
|
+
...use.reduce((acc, psName) => this._usePropertySets(acc, psName), {})
|
|
411
|
+
})
|
|
412
|
+
: obj;
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Merge properties from a named property set into obj.
|
|
417
|
+
* @param {object} obj Target object
|
|
418
|
+
* @param {string} name Property set name
|
|
419
|
+
* @returns {object}
|
|
420
|
+
*/
|
|
421
|
+
_usePropertySets (obj, name) {
|
|
422
|
+
return Object.assign(obj, this.propertySets[name]);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Retrieve a key-mapped node matching a value or return context.
|
|
426
|
+
* @param {string} name Key name
|
|
427
|
+
* @param {*} value Value to match
|
|
428
|
+
* @returns {*}
|
|
429
|
+
*/
|
|
430
|
+
getKey (name, value) {
|
|
431
|
+
const key = this.keys[name];
|
|
432
|
+
const matches = this.get(key.match, true);
|
|
433
|
+
for (const m of matches) {
|
|
434
|
+
if (m && m.nodeType === 1) { // Element
|
|
435
|
+
if (m.getAttribute && m.getAttribute(key.use) === value) {
|
|
436
|
+
return m;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return this;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Register a key for later lookup.
|
|
444
|
+
* @param {string} name Key name
|
|
445
|
+
* @param {string} match XPath selecting nodes
|
|
446
|
+
* @param {string} use Attribute (or property) name to compare
|
|
447
|
+
* @returns {XPathTransformerContext}
|
|
448
|
+
*/
|
|
449
|
+
key (name, match, use) {
|
|
450
|
+
this.keys[name] = {match, use};
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* c8 ignore start -- static default rules object has spotty function
|
|
455
|
+
* attribution under coverage; behavior is exercised via applyTemplates */
|
|
456
|
+
static DefaultTemplateRules = {
|
|
457
|
+
transformRoot: {
|
|
458
|
+
/**
|
|
459
|
+
* @param {*} node Root node
|
|
460
|
+
* @param {{mode:string}} cfg Config
|
|
461
|
+
* @returns {void}
|
|
462
|
+
*/
|
|
463
|
+
template (node, cfg) {
|
|
464
|
+
/** @type {any} */ (this).applyTemplates('.', cfg.mode);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
transformElements: {
|
|
468
|
+
/**
|
|
469
|
+
* @param {*} node Element node
|
|
470
|
+
* @param {{mode:string}} cfg Config
|
|
471
|
+
* @returns {void}
|
|
472
|
+
*/
|
|
473
|
+
template (node, cfg) {
|
|
474
|
+
/** @type {any} */ (this).applyTemplates('*', cfg.mode);
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
transformTextNodes: {
|
|
478
|
+
/**
|
|
479
|
+
* @param {{nodeValue:string}} node Text node
|
|
480
|
+
* @returns {string}
|
|
481
|
+
*/
|
|
482
|
+
template (node) {
|
|
483
|
+
return node.nodeValue;
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
transformScalars: {
|
|
487
|
+
/** @returns {*} */
|
|
488
|
+
template () {
|
|
489
|
+
return /** @type {any} */ (this).valueOf({select: '.'});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
/* c8 ignore stop */
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export default XPathTransformerContext;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {JSONPath} from 'jsonpath-plus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Computes a simple specificity score for JSONPath selectors inspired by XSLT.
|
|
5
|
+
*
|
|
6
|
+
* Used by the engine to break ties between templates when multiple JSONPath
|
|
7
|
+
* expressions match the same node. Lower values indicate broader matches
|
|
8
|
+
* (e.g., wildcards), while higher values indicate more specific matches.
|
|
9
|
+
*/
|
|
10
|
+
class XSLTStyleJSONPathResolver {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string|string[]} path
|
|
13
|
+
* @returns {-0.5|0.5|0}
|
|
14
|
+
*/
|
|
15
|
+
// eslint-disable-next-line class-methods-use-this -- Convenient
|
|
16
|
+
getPriorityBySpecificity (path) {
|
|
17
|
+
if (typeof path === 'string') {
|
|
18
|
+
path = JSONPath.toPathArray(path);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const terminal = path.at(-1);
|
|
22
|
+
// *, ~, @string() (comparable to XSLT's *, @*, and node tests,
|
|
23
|
+
// respectively)
|
|
24
|
+
if (terminal && (/^(?:\*|~|@[a-z]*?\(\))$/vi).test(terminal)) {
|
|
25
|
+
return -0.5;
|
|
26
|
+
}
|
|
27
|
+
// ., .., [] or [()] or [(?)] (comparable to XSLT's /, //, or [],
|
|
28
|
+
// respectively)
|
|
29
|
+
if (terminal && (/^(?:\.+|\[.*?\])$/v).test(terminal)) {
|
|
30
|
+
return 0.5;
|
|
31
|
+
}
|
|
32
|
+
// single name (i.e., $..someName or someName if allowing such
|
|
33
|
+
// relative paths) (comparable to XSLT's identifying a particular
|
|
34
|
+
// element or attribute name)
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default XSLTStyleJSONPathResolver;
|