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.
Files changed (43) hide show
  1. package/.editorconfig +16 -0
  2. package/CHANGES.md +5 -0
  3. package/LICENSE-MIT.txt +21 -0
  4. package/README.md +534 -0
  5. package/dist/AbstractJoiningTransformer.d.ts +42 -0
  6. package/dist/AbstractJoiningTransformer.d.ts.map +1 -0
  7. package/dist/DOMJoiningTransformer.d.ts +113 -0
  8. package/dist/DOMJoiningTransformer.d.ts.map +1 -0
  9. package/dist/JSONJoiningTransformer.d.ts +160 -0
  10. package/dist/JSONJoiningTransformer.d.ts.map +1 -0
  11. package/dist/JSONPathTransformer.d.ts +95 -0
  12. package/dist/JSONPathTransformer.d.ts.map +1 -0
  13. package/dist/JSONPathTransformerContext.d.ts +263 -0
  14. package/dist/JSONPathTransformerContext.d.ts.map +1 -0
  15. package/dist/StringJoiningTransformer.d.ts +168 -0
  16. package/dist/StringJoiningTransformer.d.ts.map +1 -0
  17. package/dist/XPathTransformer.d.ts +51 -0
  18. package/dist/XPathTransformer.d.ts.map +1 -0
  19. package/dist/XPathTransformerContext.d.ts +260 -0
  20. package/dist/XPathTransformerContext.d.ts.map +1 -0
  21. package/dist/XSLTStyleJSONPathResolver.d.ts +16 -0
  22. package/dist/XSLTStyleJSONPathResolver.d.ts.map +1 -0
  23. package/dist/index.d.ts +168 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/docs/API.expanded.md +263 -0
  26. package/docs/API.md +69 -0
  27. package/eslint.config.js +30 -0
  28. package/package.json +53 -0
  29. package/pnpm-workspace.yaml +3 -0
  30. package/src/AbstractJoiningTransformer.js +73 -0
  31. package/src/DOMJoiningTransformer.js +237 -0
  32. package/src/JSONJoiningTransformer.js +472 -0
  33. package/src/JSONPathTransformer.js +159 -0
  34. package/src/JSONPathTransformerContext.js +807 -0
  35. package/src/StringJoiningTransformer.js +589 -0
  36. package/src/XPathTransformer.js +94 -0
  37. package/src/XPathTransformerContext.js +496 -0
  38. package/src/XSLTStyleJSONPathResolver.js +39 -0
  39. package/src/index.js +299 -0
  40. package/src/types/xpath2-js.d.ts +2 -0
  41. package/tsconfig-prod.json +19 -0
  42. package/tsconfig.json +14 -0
  43. 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;