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,589 @@
1
+ import {jml} from 'jamilih';
2
+ import * as JHTML from 'jhtml';
3
+ import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
4
+
5
+ const camelCase = /[a-z][A-Z]/gv;
6
+
7
+ /**
8
+ * Type guard to detect DOM Elements.
9
+ * @param {*} item
10
+ * @returns {item is Element}
11
+ */
12
+ function _isElement (item) {
13
+ return item && typeof item === 'object' && item.nodeType === 1;
14
+ }
15
+
16
+ /**
17
+ * @param {string} n0
18
+ * @returns {string}
19
+ */
20
+ function _makeDatasetAttribute (n0) {
21
+ return n0.charAt(0) + '-' + n0.charAt(1).toLowerCase();
22
+ }
23
+
24
+ /**
25
+ *
26
+ */
27
+ /**
28
+ * Joining transformer that builds a string result.
29
+ *
30
+ * This transformer provides a fluent API to compose strings while supporting
31
+ * object/array-building semantics similar to template languages. Most methods
32
+ * funnel through append(), which is state-aware:
33
+ *
34
+ * - Inside object(): values go to object properties via propOnly()/propValue().
35
+ * - Inside array(): values are pushed to the current array.
36
+ * - Otherwise: values are concatenated into the internal string buffer.
37
+ *
38
+ * Escaping rules:
39
+ * - text(): escapes HTML special chars (& and <) and will close an open tag.
40
+ * - string(): no HTML escaping or JSON stringification; context-aware.
41
+ * - plainText(): raw append to the top-level string buffer (bypasses state).
42
+ * - rawAppend(): like plainText() but documented as lower-level.
43
+ *
44
+ * HTML/XML helpers:
45
+ * - element() and attribute() allow building tags with optional auto-escaping
46
+ * for attribute values unless cfg.preEscapedAttributes is set.
47
+ *
48
+ * Configuration hints (see joiningConfig in JTLT):
49
+ * - cfg.xmlElements: switch element() serializer to XML mode (self-closing,
50
+ * name mapping rules differ, etc.).
51
+ * - cfg.preEscapedAttributes: skip escaping attribute values.
52
+ * - cfg.JHTMLForJSON / cfg.mode: affect how object()/array() serialize.
53
+ */
54
+ class StringJoiningTransformer extends AbstractJoiningTransformer {
55
+ /**
56
+ * @param {string} s - Initial string
57
+ * @param {object} cfg - Configuration object
58
+ */
59
+ constructor (s, cfg) {
60
+ super(cfg); // Include this in any subclass of AbstractJoiningTransformer
61
+
62
+ this._str = s || '';
63
+ /** @type {any} */
64
+ this._objPropTemp = undefined;
65
+ /** @type {boolean | undefined} */
66
+ this.propOnlyState = undefined;
67
+ /** @type {boolean | undefined} */
68
+ this._arrItemState = undefined;
69
+ /** @type {boolean | undefined} */
70
+ this._objPropState = undefined;
71
+ /** @type {any} */
72
+ this._obj = undefined;
73
+ /** @type {any[]} */
74
+ this._arr = [];
75
+ /** @type {string | undefined} */
76
+ this._strTemp = undefined;
77
+ /** @type {Record<string, any>} */
78
+ this.propertySets = {};
79
+ }
80
+
81
+ /**
82
+ * @param {string|*} s - String or value to append
83
+ * @returns {StringJoiningTransformer}
84
+ */
85
+ append (s) {
86
+ // Todo: Could allow option to disallow elements within arrays, etc.
87
+ // (add states and state checking)
88
+
89
+ if (this.propOnlyState) {
90
+ this._obj[this._objPropTemp] = s;
91
+ this.propOnlyState = false;
92
+ this._objPropTemp = undefined;
93
+ } else if (this._arrItemState) {
94
+ this._arr.push(s);
95
+ } else if (this._objPropState) {
96
+ throw new Error(
97
+ 'Object values must be added via propValue() or after ' +
98
+ 'propOnly() when in an object state.'
99
+ );
100
+ } else {
101
+ this._str += s;
102
+ }
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * @returns {string}
108
+ */
109
+ get () {
110
+ return this._str;
111
+ }
112
+
113
+ /**
114
+ * @param {string} prop - Property name
115
+ * @param {*} val - Property value
116
+ * @returns {StringJoiningTransformer}
117
+ */
118
+ propValue (prop, val) {
119
+ if (!this._objPropState) {
120
+ throw new Error(
121
+ 'propValue() can only be called after an object state has been set up.'
122
+ );
123
+ }
124
+ this._obj[prop] = val;
125
+ return this;
126
+ }
127
+
128
+ /**
129
+ * @param {string} prop - Property name
130
+ * @param {Function} cb - Callback function
131
+ * @returns {StringJoiningTransformer}
132
+ */
133
+ propOnly (prop, cb) {
134
+ if (!this._objPropState) {
135
+ throw new Error(
136
+ 'propOnly() can only be called after an object state has been set up.'
137
+ );
138
+ }
139
+ if (this.propOnlyState) {
140
+ throw new Error(
141
+ 'propOnly() can only be called again after a value is set'
142
+ );
143
+ }
144
+ this.propOnlyState = true;
145
+ /** @type {any} */
146
+ const oldPropTemp = this._objPropTemp;
147
+ this._objPropTemp = prop;
148
+ cb.call(this);
149
+ this._objPropTemp = oldPropTemp;
150
+ if (this.propOnlyState) {
151
+ throw new Error('propOnly() must be followed up with setting a value.');
152
+ }
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * @param {object|Element} obj - Object to serialize
158
+ * @param {Function} cb - Callback function
159
+ * @param {any[]} [usePropertySets] - Property sets to use
160
+ * @param {object} [propSets] - Additional property sets
161
+ * @returns {StringJoiningTransformer}
162
+ */
163
+ object (obj, cb, usePropertySets, propSets) {
164
+ // Builds up an internal object (or converts a supplied Element via JHTML)
165
+ // and, depending on context, either appends the object to the current
166
+ // array/object or serializes it into the output string (JSON, JavaScript,
167
+ // or JHTML), based on cfg.
168
+ // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
169
+ const that = this;
170
+ this._requireSameChildren('string', 'object');
171
+ /** @type {any} */
172
+ const oldObjPropState = this._objPropState;
173
+ /** @type {any} */
174
+ const oldObj = this._obj;
175
+ this._obj = _isElement(obj)
176
+ ? JHTML.toJSONObject(obj, {mode: this._cfg?.mode})
177
+ : obj || {};
178
+
179
+ // Todo: Allow in this and subsequent JSON methods ability to create
180
+ // jml-based JHTML
181
+
182
+ if (usePropertySets !== undefined) {
183
+ this._obj = usePropertySets.reduce(function (o, psName) {
184
+ return that._usePropertySets(o, psName);
185
+ }, this._obj);
186
+ }
187
+ if (propSets !== undefined) {
188
+ Object.assign(this._obj, propSets);
189
+ }
190
+
191
+ if (cb) {
192
+ this._objPropState = true;
193
+ cb.call(this);
194
+ this._objPropState = oldObjPropState;
195
+ }
196
+
197
+ // Not ready to serialize yet as still inside another array or object
198
+ if (oldObjPropState || this._arrItemState) {
199
+ this.append(this._obj);
200
+ } else if (this._cfg && this._cfg.JHTMLForJSON) {
201
+ this.append(JHTML.toJHTMLString(this._obj));
202
+ } else if (this._cfg && this._cfg.mode !== 'JavaScript') {
203
+ // Allow this method to operate on non-finite numbers and functions
204
+ const stringifier = new JHTML.Stringifier({mode: 'JavaScript'});
205
+ this.append(stringifier.walkJSONObject(this._obj));
206
+ } else {
207
+ this.append(JSON.stringify(this._obj));
208
+ }
209
+ this._obj = oldObj;
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * @param {any[]|Element} [arr] - Array to serialize
215
+ * @param {Function} [cb] - Callback function
216
+ * @returns {StringJoiningTransformer}
217
+ */
218
+ array (arr, cb) {
219
+ // Similar to object(), but for arrays. Context determines whether to
220
+ // append the array structure or to serialize into the string.
221
+ this._requireSameChildren('string', 'array');
222
+ /** @type {any} */
223
+ const oldArr = this._arr;
224
+ // Todo: copy array?
225
+ this._arr = _isElement(arr)
226
+ ? /** @type {any[]} */ (JHTML.toJSONObject(arr, {mode: this._cfg?.mode}))
227
+ : arr || [];
228
+
229
+ /** @type {any} */
230
+ const oldArrItemState = this._arrItemState;
231
+
232
+ /* c8 ignore next 8 -- Callback handling for nested array building.
233
+ * Requires specific state combinations with nested objects/arrays. */
234
+ if (cb) {
235
+ const oldObjPropState = this._objPropState;
236
+ this._objPropState = false;
237
+ this._arrItemState = true;
238
+ cb.call(this);
239
+ this._arrItemState = oldArrItemState;
240
+ this._objPropState = oldObjPropState;
241
+ }
242
+
243
+ // Not ready to serialize yet as still inside another array or object
244
+ if (oldArrItemState || this._objPropState) {
245
+ this.append(this._arr);
246
+ /* c8 ignore next 2 -- JHTMLForJSON is a specialized output mode rarely used
247
+ * in combination with nested array building at the root level. */
248
+ } else if (this._cfg && this._cfg.JHTMLForJSON) {
249
+ this.append(JHTML.toJHTMLString(this._arr));
250
+ } else if (this._cfg && this._cfg.mode !== 'JavaScript') {
251
+ // Allow this method to operate on non-finite numbers and functions
252
+ const stringifier = new JHTML.Stringifier({mode: 'JavaScript'});
253
+ this.append(stringifier.walkJSONObject(this._arr));
254
+ } else {
255
+ this.append(JSON.stringify(this._arr));
256
+ }
257
+ this._arr = oldArr;
258
+ return this;
259
+ }
260
+
261
+ /**
262
+ * @param {string|Element|object} str - String value or element
263
+ * @param {Function} [cb] - Callback function
264
+ * @returns {StringJoiningTransformer}
265
+ */
266
+ string (str, cb) {
267
+ // Context-aware string emission. If inside object/array/propOnly states,
268
+ // the produced string participates in those structures via append().
269
+ // If a callback is provided, it composes a nested string value first.
270
+ if (_isElement(str)) {
271
+ str = /** @type {any} */ (
272
+ JHTML.toJSONObject(str, {mode: this._cfg?.mode})
273
+ );
274
+ }
275
+
276
+ let tmpStr = '';
277
+ /** @type {any} */
278
+ const _oldStrTemp = this._strTemp;
279
+ if (cb) {
280
+ this._strTemp = '';
281
+ cb.call(this);
282
+ tmpStr = this._strTemp;
283
+ this._strTemp = _oldStrTemp;
284
+ }
285
+ if (_oldStrTemp !== undefined) {
286
+ this._strTemp = (this._strTemp || '') + str;
287
+ } else {
288
+ // Append to the output (or current container via append()).
289
+ this.append(tmpStr + str);
290
+ }
291
+ return this;
292
+ }
293
+
294
+ /**
295
+ * @param {number|Element|object} num - Number value or element
296
+ * @returns {StringJoiningTransformer}
297
+ */
298
+ number (num) {
299
+ // Appends the number as a string; no localization/formatting is applied.
300
+ if (_isElement(num)) {
301
+ num = /** @type {any} */ (
302
+ JHTML.toJSONObject(num, {mode: this._cfg?.mode})
303
+ );
304
+ }
305
+ this.append(num.toString());
306
+ return this;
307
+ }
308
+
309
+ /**
310
+ * @param {boolean|Element|object} bool - Boolean value or element
311
+ * @returns {StringJoiningTransformer}
312
+ */
313
+ boolean (bool) {
314
+ // Appends 'true' or 'false'.
315
+ if (_isElement(bool)) {
316
+ bool = /** @type {any} */ (
317
+ JHTML.toJSONObject(bool, {mode: this._cfg?.mode})
318
+ );
319
+ }
320
+ this.append(bool ? 'true' : 'false');
321
+ return this;
322
+ }
323
+
324
+ /**
325
+ * @returns {StringJoiningTransformer}
326
+ */
327
+ null () {
328
+ // Appends the literal 'null'.
329
+ this.append('null');
330
+ return this;
331
+ }
332
+
333
+ /**
334
+ * @returns {StringJoiningTransformer}
335
+ */
336
+ undefined () {
337
+ // Appends the literal 'undefined' (only in JavaScript mode).
338
+ if (this._cfg && this._cfg.mode !== 'JavaScript') {
339
+ throw new Error(
340
+ 'undefined is not allowed unless added in JavaScript mode'
341
+ );
342
+ }
343
+ this.append('undefined');
344
+ return this;
345
+ }
346
+
347
+ /**
348
+ * @param {number|Element} num - Non-finite number (NaN, Infinity, -Infinity)
349
+ * @returns {StringJoiningTransformer}
350
+ */
351
+ nonfiniteNumber (num) {
352
+ // Appends NaN/Infinity/-Infinity as-is (only in JavaScript mode).
353
+ if (this._cfg && this._cfg.mode !== 'JavaScript') {
354
+ throw new Error(
355
+ 'Non-finite numbers are not allowed unless added in JavaScript mode'
356
+ );
357
+ }
358
+ if (_isElement(num)) {
359
+ num = /** @type {any} */ (
360
+ JHTML.toJSONObject(num, {mode: this._cfg?.mode})
361
+ );
362
+ }
363
+ this.append(num.toString());
364
+ return this;
365
+ }
366
+
367
+ /**
368
+ * @param {Function|Element} func - Function to stringify
369
+ * @returns {StringJoiningTransformer}
370
+ */
371
+ function (func) {
372
+ // Appends function source (only in JavaScript mode).
373
+ if (this._cfg && this._cfg.mode !== 'JavaScript') {
374
+ throw new Error(
375
+ 'function is not allowed unless added in JavaScript mode'
376
+ );
377
+ }
378
+ if (_isElement(func)) {
379
+ func = /** @type {any} */ (
380
+ JHTML.toJSONObject(func, {mode: this._cfg?.mode})
381
+ );
382
+ }
383
+ this.append(func.toString());
384
+ return this;
385
+ }
386
+
387
+ /**
388
+ * @param {string|object} elName - Element name or element object
389
+ * @param {object} [atts] - Element attributes
390
+ * @param {any[]} [childNodes] - Child nodes
391
+ * @param {Function} [cb] - Callback function
392
+ * @returns {StringJoiningTransformer}
393
+ */
394
+ element (elName, atts, childNodes, cb) {
395
+ // Emits an HTML/XML element using Jamilih under the hood, or allows a
396
+ // callback to build attributes/children incrementally. Attribute values
397
+ // are escaped unless cfg.preEscapedAttributes is true. When a callback is
398
+ // provided, this manages open-tag state so text() can close it safely.
399
+ // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
400
+ const that = this;
401
+ if (Array.isArray(atts)) {
402
+ cb = /** @type {Function} */ (/** @type {unknown} */ (childNodes));
403
+ childNodes = atts;
404
+ atts = {};
405
+ } else if (typeof atts === 'function') {
406
+ cb = atts;
407
+ childNodes = [];
408
+ atts = {};
409
+ }
410
+ if (typeof childNodes === 'function') {
411
+ cb = childNodes;
412
+ childNodes = [];
413
+ }
414
+
415
+ // Todo: allow for cfg to produce Jamilih string output or hXML
416
+ // string output
417
+ const method = this._cfg && this._cfg.xmlElements ? 'toXML' : 'toHTML';
418
+ if (!cb) {
419
+ // Note that Jamilih currently has an issue with 'selected', 'checked',
420
+ // 'value', 'defaultValue', 'for', 'on*', 'style' (workaround: pass
421
+ // an empty callback as the last argument to element())
422
+ this.append(
423
+ // Casts to satisfy TS when using JS + JSDoc
424
+ /** @type {any} */ (jml[method])(elName,
425
+ /** @type {any} */ (atts), /** @type {any} */ (childNodes))
426
+ );
427
+ return this;
428
+ }
429
+
430
+ if (typeof elName === 'object') {
431
+ /** @type {Record<string, any>} */
432
+ const objAtts = {};
433
+ /** @type {any} */
434
+ const elObj = elName;
435
+ [...elObj.attributes].forEach(function (att, i) {
436
+ objAtts[att.name] = att.value;
437
+ });
438
+ atts = Object.assign(objAtts, atts);
439
+ elName = elObj.nodeName;
440
+ }
441
+
442
+ this.append('<' + elName);
443
+ /** @type {any} */
444
+ const oldTagState = this._openTagState;
445
+ this._openTagState = true;
446
+ if (atts) {
447
+ const attsObj = /** @type {Record<string, any>} */ (atts);
448
+ Object.keys(attsObj).forEach((att) => {
449
+ that.attribute(att, attsObj[att], false);
450
+ });
451
+ }
452
+ if (childNodes && childNodes.length) {
453
+ this._openTagState = false;
454
+ this.append(jml[method]({'#': childNodes}));
455
+ }
456
+ cb.call(this);
457
+
458
+ // Todo: Depending on an this._cfg.xmlElements option, allow for
459
+ // XML self-closing when empty or as per the tag, HTML
460
+ // self-closing tags (or polyglot-friendly self-closing)
461
+ if (this._openTagState) {
462
+ this.append('>');
463
+ }
464
+ this.append('</' + elName + '>');
465
+ this._openTagState = oldTagState;
466
+ return this;
467
+ }
468
+
469
+ /**
470
+ * @param {string} name - Attribute name
471
+ * @param {string|object} val - Attribute value
472
+ * @param {boolean} [avoidAttEscape] - Whether to avoid escaping the
473
+ * attribute value
474
+ * @returns {StringJoiningTransformer}
475
+ */
476
+ attribute (name, val, avoidAttEscape) {
477
+ // Adds an attribute to the most recently opened start tag. Supports
478
+ // special objects for dataset and ordered attributes ($a). Escapes '&'
479
+ // and '"' unless cfg.preEscapedAttributes or avoidAttEscape are set.
480
+ // eslint-disable-next-line unicorn/no-this-assignment -- Temporary
481
+ const that = this;
482
+ if (!this._openTagState) {
483
+ throw new Error(
484
+ 'An attribute cannot be added after an opening tag has been closed ' +
485
+ '(name: ' + name + '; value: ' + val + ')'
486
+ );
487
+ }
488
+
489
+ if (!this._cfg || !this._cfg.xmlElements) {
490
+ if (typeof val === 'object') {
491
+ /** @type {Record<string, any>} */
492
+ const valObj = /** @type {any} */ (val);
493
+ switch (name) {
494
+ case 'dataset': {
495
+ Object.keys(valObj).forEach(function (att) {
496
+ that.attribute(
497
+ 'data-' + att.replaceAll(
498
+ camelCase, _makeDatasetAttribute
499
+ ), valObj[att], false
500
+ );
501
+ });
502
+ break;
503
+ }
504
+ case '$a': { // Ordered attributes
505
+ /** @type {any[]} */
506
+ const valArr = /** @type {any} */ (val);
507
+ valArr.forEach(function (attArr) {
508
+ that.attribute(attArr[0], attArr[1], false);
509
+ });
510
+ break;
511
+ }
512
+ default:
513
+ break;
514
+ }
515
+ return this;
516
+ }
517
+ name = {className: 'class', htmlFor: 'for'}[name] || name;
518
+ }
519
+
520
+ /** @type {string} */
521
+ const valStr = /** @type {any} */ (val);
522
+ val = ((this._cfg && this._cfg.preEscapedAttributes) || avoidAttEscape)
523
+ ? valStr
524
+ : valStr.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
525
+ this.append(' ' + name + '="' + val + '"');
526
+ return this;
527
+ }
528
+
529
+ /**
530
+ * @param {string} txt - Text content to escape and append
531
+ * @returns {StringJoiningTransformer}
532
+ */
533
+ text (txt) {
534
+ // Adds escaped text content. If currently within an unclosed start tag,
535
+ // it will first close the tag ('>'). Escapes '&' and '<'.
536
+ if (this._openTagState) {
537
+ this.append('>');
538
+ this._openTagState = false;
539
+ }
540
+ this.append(txt.replaceAll('&', '&amp;').replaceAll('<', '&lt;'));
541
+ return this;
542
+ }
543
+
544
+ /**
545
+ * Unlike text(), does not escape for HTML; unlike string(), does not perform
546
+ * JSON stringification; unlike append(), does not do other checks (but still
547
+ * varies in its role across transformers).
548
+ * @param {string} str
549
+ * @returns {StringJoiningTransformer}
550
+ */
551
+ rawAppend (str) {
552
+ // Lowest-level append: bypasses append() semantics and state checks.
553
+ this._str += str;
554
+ return this;
555
+ }
556
+
557
+ /**
558
+ * @param {string} str - Plain text to append without escaping
559
+ * @returns {StringJoiningTransformer}
560
+ */
561
+ plainText (str) {
562
+ // Bypasses append() semantics: always writes directly to the top-level
563
+ // string buffer with no escaping. Prefer string() when you want the value
564
+ // to participate in object/array contexts.
565
+ this._str += str;
566
+ return this;
567
+ }
568
+
569
+ /**
570
+ * Helper method to use property sets.
571
+ * @param {object} obj - Object to apply property set to
572
+ * @param {string} psName - Property set name
573
+ * @returns {object}
574
+ */
575
+ _usePropertySets (obj, psName) {
576
+ // Merge named property set from this.propertySets into obj
577
+ if (this.propertySets && this.propertySets[psName]) {
578
+ return {
579
+ ...obj,
580
+ ...this.propertySets[psName]
581
+ };
582
+ }
583
+ return obj;
584
+ }
585
+ }
586
+
587
+ // Todo: Implement comment(), processingInstruction(), etc.
588
+
589
+ export default StringJoiningTransformer;
@@ -0,0 +1,94 @@
1
+ import XPathTransformerContext from './XPathTransformerContext.js';
2
+
3
+ /**
4
+ * Applies named XPath-driven templates to XML/HTML DOM data.
5
+ *
6
+ * Finds templates whose `path` XPath matches the current node (plus optional
7
+ * `mode`), sorts by priority, and invokes the winning template.
8
+ * Falls back to default rules when no template matches.
9
+ */
10
+ class XPathTransformer {
11
+ /**
12
+ * @param {object} config Configuration
13
+ * @param {boolean} [config.errorOnEqualPriority] Throw on equal priority
14
+ * @param {any[]} config.templates Template objects
15
+ * @param {number} [config.xpathVersion] XPath version (1|2)
16
+ */
17
+ constructor (config) {
18
+ let map = /** @type {Record<string, boolean>} */ ({});
19
+ this._config = config;
20
+ /** @type {any[]} */
21
+ this.rootTemplates = [];
22
+ this.templates = config.templates;
23
+ this.templates = this.templates.map(function (template) {
24
+ if (Array.isArray(template)) {
25
+ return {path: template[0], template: template[1]};
26
+ }
27
+ return template;
28
+ });
29
+ this.templates.forEach((template) => {
30
+ if (template.name && map[template.name]) {
31
+ /* c8 ignore next 6 -- c8/Istanbul known limitation: arrow function
32
+ * predicates within filter assignments to instance properties are not
33
+ * instrumented. Functionality fully tested via direct assertions on
34
+ * rootTemplates and templates array lengths and contents in
35
+ * test suite. */
36
+ throw new Error('Templates must all have different names.');
37
+ }
38
+ map[template.name] = true;
39
+ });
40
+ // Collect root templates without mutating during iteration
41
+ this.rootTemplates = this.templates.filter((t) => t.path === '/');
42
+ this.templates = this.templates.filter((t) => t.path !== '/');
43
+ map = /** @type {any} */ (null);
44
+ }
45
+
46
+ /**
47
+ * @returns {void}
48
+ */
49
+ _triggerEqualPriorityError () {
50
+ if (this._config.errorOnEqualPriority) {
51
+ throw new Error(
52
+ 'You have configured XPathTransformer to throw errors on equal ' +
53
+ 'priority templates and these have been found.'
54
+ );
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @param {string} mode Transformation mode
60
+ * @returns {*} Result of transformation
61
+ */
62
+ transform (mode) {
63
+ const xte = new XPathTransformerContext(
64
+ /** @type {any} */ (this._config), this.templates
65
+ );
66
+ const len = this.rootTemplates.length;
67
+ const templateObj = len
68
+ ? this.rootTemplates.pop()
69
+ : XPathTransformer.DefaultTemplateRules.transformRoot;
70
+ if (len > 1) {
71
+ this._triggerEqualPriorityError();
72
+ }
73
+ const ret = templateObj.template.call(xte, undefined, {mode});
74
+ if (typeof ret !== 'undefined') {
75
+ /** @type {any} */ (xte)._getJoiningTransformer().append(ret);
76
+ }
77
+ return xte.getOutput();
78
+ }
79
+
80
+ static DefaultTemplateRules = {
81
+ transformRoot: {
82
+ /**
83
+ * @param {*} node Node
84
+ * @param {{mode:string}} cfg Config
85
+ * @returns {void}
86
+ */
87
+ template (node, cfg) {
88
+ /** @type {any} */ (this).applyTemplates('.', cfg.mode);
89
+ }
90
+ }
91
+ };
92
+ }
93
+
94
+ export default XPathTransformer;