jtlt 0.4.0 → 0.5.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 +9 -0
- package/demo/index.css +4 -0
- package/demo/index.html +4 -4
- package/demo/index.js +6 -2
- package/demo/vendor/fontoxpath/dist/fontoxpath.esm.js +600 -0
- package/demo/vendor/prsc/dist/prsc.esm.js +2 -0
- package/demo/vendor/whynot/dist/whynot.esm.js +2 -0
- package/demo/vendor/xspattern/dist/xspattern.esm.js +2 -0
- package/dist/AbstractJoiningTransformer.d.ts +27 -0
- package/dist/AbstractJoiningTransformer.d.ts.map +1 -1
- package/dist/DOMJoiningTransformer.d.ts +60 -8
- package/dist/DOMJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONJoiningTransformer.d.ts +8 -9
- package/dist/JSONJoiningTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformer.d.ts.map +1 -1
- package/dist/JSONPathTransformerContext.d.ts +137 -2
- package/dist/JSONPathTransformerContext.d.ts.map +1 -1
- package/dist/StringJoiningTransformer.d.ts +7 -7
- package/dist/StringJoiningTransformer.d.ts.map +1 -1
- package/dist/XPathTransformer.d.ts.map +1 -1
- package/dist/XPathTransformerContext.d.ts +69 -0
- package/dist/XPathTransformerContext.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/docs/TO-DO.md +8 -6
- package/package.json +6 -2
- package/src/AbstractJoiningTransformer.js +42 -0
- package/src/DOMJoiningTransformer.js +115 -23
- package/src/JSONJoiningTransformer.js +50 -26
- package/src/JSONPathTransformer.js +2 -0
- package/src/JSONPathTransformerContext.js +520 -7
- package/src/StringJoiningTransformer.js +25 -21
- package/src/XPathTransformer.js +2 -0
- package/src/XPathTransformerContext.js +426 -8
- package/src/index.js +7 -1
|
@@ -13,7 +13,8 @@ import AbstractJoiningTransformer from './AbstractJoiningTransformer.js';
|
|
|
13
13
|
* mediaType?: string,
|
|
14
14
|
* version?: string,
|
|
15
15
|
* standalone?: boolean,
|
|
16
|
-
* method?: "xml"|"html"|"text"|"json"|"xhtml"
|
|
16
|
+
* method?: "xml"|"html"|"text"|"json"|"xhtml",
|
|
17
|
+
* useCharacterMaps?: string[]
|
|
17
18
|
* }} OutputConfig
|
|
18
19
|
*/
|
|
19
20
|
|
|
@@ -434,20 +435,6 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
434
435
|
return this;
|
|
435
436
|
}
|
|
436
437
|
|
|
437
|
-
/**
|
|
438
|
-
* @param {OutputConfig} cfg
|
|
439
|
-
* @returns {StringJoiningTransformer}
|
|
440
|
-
*/
|
|
441
|
-
output (cfg) {
|
|
442
|
-
// We wait until first element is set in `element()` to add
|
|
443
|
-
// XML declaration and DOCTYPE as latter depends on root element
|
|
444
|
-
this._outputConfig = cfg;
|
|
445
|
-
|
|
446
|
-
// Use for file extension if making downloadable?
|
|
447
|
-
this.mediaType = cfg.mediaType;
|
|
448
|
-
return this;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
438
|
/**
|
|
452
439
|
* @param {string|Element} elName - Element name or element object
|
|
453
440
|
* @param {ElementAttributes} [atts] - Element attributes
|
|
@@ -570,7 +557,9 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
570
557
|
}
|
|
571
558
|
if (childNodes && childNodes.length) {
|
|
572
559
|
this._openTagState = false;
|
|
573
|
-
this.append(
|
|
560
|
+
this.append(this._replaceCharacterMaps(
|
|
561
|
+
jml[method]({'#': childNodes})
|
|
562
|
+
));
|
|
574
563
|
}
|
|
575
564
|
cb.call(this);
|
|
576
565
|
|
|
@@ -585,6 +574,17 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
585
574
|
return this;
|
|
586
575
|
}
|
|
587
576
|
|
|
577
|
+
/**
|
|
578
|
+
* @param {string} prefix
|
|
579
|
+
* @param {string} namespaceURI
|
|
580
|
+
* @returns {StringJoiningTransformer}
|
|
581
|
+
*/
|
|
582
|
+
namespace (prefix, namespaceURI) {
|
|
583
|
+
this.append(' xmlns:' + prefix + '="' +
|
|
584
|
+
this._replaceCharacterMaps(namespaceURI) + '"');
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
588
|
/**
|
|
589
589
|
* @param {string} name - Attribute name
|
|
590
590
|
* @param {string|Record<string, unknown>|
|
|
@@ -648,7 +648,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
648
648
|
val = (this._cfg.preEscapedAttributes || avoidAttEscape)
|
|
649
649
|
? valStr
|
|
650
650
|
: valStr.replaceAll('&', '&').replaceAll('"', '"');
|
|
651
|
-
this.append(' ' + name + '="' + val + '"');
|
|
651
|
+
this.append(' ' + name + '="' + this._replaceCharacterMaps(val) + '"');
|
|
652
652
|
return this;
|
|
653
653
|
}
|
|
654
654
|
|
|
@@ -663,7 +663,11 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
663
663
|
this.append('>');
|
|
664
664
|
this._openTagState = false;
|
|
665
665
|
}
|
|
666
|
-
this.append(
|
|
666
|
+
this.append(
|
|
667
|
+
this._replaceCharacterMaps(
|
|
668
|
+
txt
|
|
669
|
+
).replaceAll('&', '&').replaceAll('<', '<')
|
|
670
|
+
);
|
|
667
671
|
return this;
|
|
668
672
|
}
|
|
669
673
|
|
|
@@ -734,7 +738,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
734
738
|
// Save current state
|
|
735
739
|
/** @type {any} */
|
|
736
740
|
const oldRoot = this.root;
|
|
737
|
-
/** @type {
|
|
741
|
+
/** @type {OutputConfig|undefined} */
|
|
738
742
|
const oldOutputConfig = this._outputConfig;
|
|
739
743
|
const oldStr = this._str;
|
|
740
744
|
/** @type {any} */
|
|
@@ -742,7 +746,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
742
746
|
|
|
743
747
|
// Reset state for new document
|
|
744
748
|
this.root = undefined;
|
|
745
|
-
/** @type {
|
|
749
|
+
/** @type {OutputConfig} */
|
|
746
750
|
this._outputConfig = cfg;
|
|
747
751
|
this._str = '';
|
|
748
752
|
this._openTagState = false;
|
|
@@ -795,7 +799,7 @@ class StringJoiningTransformer extends AbstractJoiningTransformer {
|
|
|
795
799
|
|
|
796
800
|
// Reset state for new document
|
|
797
801
|
this.root = undefined;
|
|
798
|
-
/** @type {
|
|
802
|
+
/** @type {OutputConfig} */
|
|
799
803
|
this._outputConfig = cfg;
|
|
800
804
|
this._str = '';
|
|
801
805
|
this._openTagState = false;
|
package/src/XPathTransformer.js
CHANGED
|
@@ -83,6 +83,8 @@ class XPathTransformer {
|
|
|
83
83
|
if (len > 1) {
|
|
84
84
|
this._triggerEqualPriorityError();
|
|
85
85
|
}
|
|
86
|
+
// Set up parameter context for valueOf() access in root template
|
|
87
|
+
xte._params = {0: xte._contextNode};
|
|
86
88
|
const ret = templateObj.template.call(xte, undefined, {mode});
|
|
87
89
|
if (typeof ret !== 'undefined') {
|
|
88
90
|
/** @type {any} */ (xte)._getJoiningTransformer().append(ret);
|
|
@@ -48,6 +48,11 @@ class XPathTransformerContext {
|
|
|
48
48
|
this.propertySets = {};
|
|
49
49
|
/** @type {Record<string, {match: string, use: string}>} */
|
|
50
50
|
this.keys = {};
|
|
51
|
+
/**
|
|
52
|
+
* @type {Record<string,
|
|
53
|
+
* import('./JSONPathTransformerContext.js').DecimalFormatSymbols>}
|
|
54
|
+
*/
|
|
55
|
+
this.decimalFormats = {};
|
|
51
56
|
/** @type {boolean|undefined} */
|
|
52
57
|
this._initialized = undefined;
|
|
53
58
|
/** @type {string|undefined} */
|
|
@@ -293,7 +298,16 @@ class XPathTransformerContext {
|
|
|
293
298
|
);
|
|
294
299
|
}
|
|
295
300
|
this._contextNode = node;
|
|
301
|
+
|
|
302
|
+
// Set up parameter context for valueOf() access in templates
|
|
303
|
+
const prevTemplateParams = this._params;
|
|
304
|
+
this._params = {0: node};
|
|
305
|
+
|
|
296
306
|
const ret = templateObj.template.call(this, node, {mode});
|
|
307
|
+
|
|
308
|
+
// Restore previous parameter context
|
|
309
|
+
this._params = prevTemplateParams;
|
|
310
|
+
|
|
297
311
|
if (typeof ret !== 'undefined') {
|
|
298
312
|
this._getJoiningTransformer().append(ret);
|
|
299
313
|
}
|
|
@@ -371,11 +385,269 @@ class XPathTransformerContext {
|
|
|
371
385
|
const nodesResult = this._evalXPath(select, true);
|
|
372
386
|
const nodes = /** @type {Node[]} */ (nodesResult);
|
|
373
387
|
for (const n of nodes) {
|
|
374
|
-
|
|
388
|
+
// Set up parameter context for valueOf() access
|
|
389
|
+
const prevParams = this._params;
|
|
390
|
+
const prevContext = this._contextNode;
|
|
391
|
+
this._params = {0: n};
|
|
392
|
+
this._contextNode = n;
|
|
393
|
+
try {
|
|
394
|
+
cb.call(this, n);
|
|
395
|
+
} finally {
|
|
396
|
+
// Restore previous parameter context
|
|
397
|
+
this._params = prevParams;
|
|
398
|
+
this._contextNode = prevContext;
|
|
399
|
+
}
|
|
375
400
|
}
|
|
376
401
|
return this;
|
|
377
402
|
}
|
|
378
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Groups items and executes callback for each group.
|
|
406
|
+
* Equivalent to XSLT's xsl:for-each-group.
|
|
407
|
+
* @param {string} select - XPath selector for items to group
|
|
408
|
+
* @param {object} options - Grouping options
|
|
409
|
+
* @param {string} [options.groupBy] - XPath expression to group by value
|
|
410
|
+
* @param {string} [options.groupAdjacent] - Groups adjacent items with
|
|
411
|
+
* same value
|
|
412
|
+
* @param {string} [options.groupStartingWith] - Starts new group when
|
|
413
|
+
* expression matches
|
|
414
|
+
* @param {string} [options.groupEndingWith] - Ends group when expression
|
|
415
|
+
* matches
|
|
416
|
+
* @param {(
|
|
417
|
+
* this: XPathTransformerContext, key: any, items: Node[], ctx: any
|
|
418
|
+
* ) => void} cb - Callback receives (groupingKey, groupItems, context)
|
|
419
|
+
* @returns {this}
|
|
420
|
+
*/
|
|
421
|
+
forEachGroup (select, options, cb) {
|
|
422
|
+
// eslint-disable-next-line unicorn/no-this-assignment -- Temporary
|
|
423
|
+
const that = this;
|
|
424
|
+
const {groupBy, groupAdjacent, groupStartingWith, groupEndingWith} =
|
|
425
|
+
options;
|
|
426
|
+
|
|
427
|
+
const nodesResult = this._evalXPath(select, true);
|
|
428
|
+
const nodes = /** @type {Node[]} */ (nodesResult);
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @param {string} expr
|
|
432
|
+
* @param {Node} node
|
|
433
|
+
* @returns {any}
|
|
434
|
+
*/
|
|
435
|
+
function evalInContext (expr, node) {
|
|
436
|
+
if (expr === '.' || expr === '@') {
|
|
437
|
+
return node.textContent;
|
|
438
|
+
}
|
|
439
|
+
// Temporarily set context to evaluate expression
|
|
440
|
+
const prevNode = that._contextNode;
|
|
441
|
+
that._contextNode = node;
|
|
442
|
+
try {
|
|
443
|
+
const result = that._evalXPath(expr, false);
|
|
444
|
+
// XPath v2/v3 always return arrays; handle NodeList/Array results
|
|
445
|
+
// by taking first item or its text content
|
|
446
|
+
const resultWithLength = /** @type {{length: number}} */ (result);
|
|
447
|
+
if (resultWithLength.length > 0) {
|
|
448
|
+
const firstItem = /** @type {any} */ (result)[0];
|
|
449
|
+
return firstItem?.textContent ?? firstItem;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
} finally {
|
|
453
|
+
that._contextNode = prevNode;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** @type {Map<string, Node[]>} */
|
|
458
|
+
const groups = new Map();
|
|
459
|
+
|
|
460
|
+
if (groupBy) {
|
|
461
|
+
// Group by computed value
|
|
462
|
+
for (const node of nodes) {
|
|
463
|
+
const key = evalInContext(groupBy, node);
|
|
464
|
+
// Handle undefined by converting to null for JSON serialization
|
|
465
|
+
const keyStr = JSON.stringify(key === undefined ? null : key);
|
|
466
|
+
if (!groups.has(keyStr)) {
|
|
467
|
+
groups.set(keyStr, []);
|
|
468
|
+
}
|
|
469
|
+
/** @type {Node[]} */ (groups.get(keyStr)).push(node);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
for (const [keyStr, items] of groups) {
|
|
473
|
+
const key = JSON.parse(keyStr);
|
|
474
|
+
// Convert null back to undefined if that was the original value
|
|
475
|
+
const actualKey = key === null && keyStr === 'null' ? undefined : key;
|
|
476
|
+
const prevParams = this._params;
|
|
477
|
+
const prevContext = this._contextNode;
|
|
478
|
+
try {
|
|
479
|
+
this._contextNode = items[0];
|
|
480
|
+
/** @type {any} */ (this)._currentGroup = items;
|
|
481
|
+
/** @type {any} */ (this)._currentGroupingKey = actualKey;
|
|
482
|
+
cb.call(this, actualKey, items, this);
|
|
483
|
+
} finally {
|
|
484
|
+
this._params = prevParams;
|
|
485
|
+
this._contextNode = prevContext;
|
|
486
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
487
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else if (groupAdjacent) {
|
|
491
|
+
// Group adjacent items with same value
|
|
492
|
+
/** @type {string|null} */
|
|
493
|
+
let currentKey = null;
|
|
494
|
+
/** @type {Node[]} */
|
|
495
|
+
let currentGroup = [];
|
|
496
|
+
|
|
497
|
+
for (const node of nodes) {
|
|
498
|
+
const key = evalInContext(groupAdjacent, node);
|
|
499
|
+
const keyStr = JSON.stringify(key);
|
|
500
|
+
|
|
501
|
+
if (currentKey === null || currentKey !== keyStr) {
|
|
502
|
+
if (currentGroup.length > 0) {
|
|
503
|
+
const prevParams = this._params;
|
|
504
|
+
const prevContext = this._contextNode;
|
|
505
|
+
try {
|
|
506
|
+
this._contextNode = currentGroup[0];
|
|
507
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
508
|
+
/** @type {any} */ (this)._currentGroupingKey =
|
|
509
|
+
JSON.parse(/** @type {string} */ (currentKey));
|
|
510
|
+
cb.call(
|
|
511
|
+
this,
|
|
512
|
+
JSON.parse(/** @type {string} */ (currentKey)),
|
|
513
|
+
currentGroup,
|
|
514
|
+
this
|
|
515
|
+
);
|
|
516
|
+
} finally {
|
|
517
|
+
this._params = prevParams;
|
|
518
|
+
this._contextNode = prevContext;
|
|
519
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
520
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
currentKey = keyStr;
|
|
524
|
+
currentGroup = [node];
|
|
525
|
+
} else {
|
|
526
|
+
currentGroup.push(node);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Process last group
|
|
531
|
+
if (currentGroup.length > 0) {
|
|
532
|
+
const prevParams = this._params;
|
|
533
|
+
const prevContext = this._contextNode;
|
|
534
|
+
try {
|
|
535
|
+
this._contextNode = currentGroup[0];
|
|
536
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
537
|
+
/** @type {any} */ (this)._currentGroupingKey =
|
|
538
|
+
JSON.parse(/** @type {string} */ (currentKey));
|
|
539
|
+
cb.call(
|
|
540
|
+
this,
|
|
541
|
+
JSON.parse(/** @type {string} */ (currentKey)),
|
|
542
|
+
currentGroup,
|
|
543
|
+
this
|
|
544
|
+
);
|
|
545
|
+
} finally {
|
|
546
|
+
this._params = prevParams;
|
|
547
|
+
this._contextNode = prevContext;
|
|
548
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
549
|
+
delete /** @type {any} */ (this)._currentGroupingKey;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} else if (groupStartingWith) {
|
|
553
|
+
// Start new group when expression matches
|
|
554
|
+
/** @type {Node[]} */
|
|
555
|
+
let currentGroup = [];
|
|
556
|
+
|
|
557
|
+
for (const node of nodes) {
|
|
558
|
+
const startMatch = evalInContext(groupStartingWith, node);
|
|
559
|
+
|
|
560
|
+
if (startMatch && currentGroup.length > 0) {
|
|
561
|
+
const prevParams = this._params;
|
|
562
|
+
const prevContext = this._contextNode;
|
|
563
|
+
try {
|
|
564
|
+
this._contextNode = currentGroup[0];
|
|
565
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
566
|
+
cb.call(this, null, currentGroup, this);
|
|
567
|
+
} finally {
|
|
568
|
+
this._params = prevParams;
|
|
569
|
+
this._contextNode = prevContext;
|
|
570
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
571
|
+
}
|
|
572
|
+
currentGroup = [];
|
|
573
|
+
}
|
|
574
|
+
currentGroup.push(node);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Process last group
|
|
578
|
+
if (currentGroup.length > 0) {
|
|
579
|
+
const prevParams = this._params;
|
|
580
|
+
const prevContext = this._contextNode;
|
|
581
|
+
try {
|
|
582
|
+
this._contextNode = currentGroup[0];
|
|
583
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
584
|
+
cb.call(this, null, currentGroup, this);
|
|
585
|
+
} finally {
|
|
586
|
+
this._params = prevParams;
|
|
587
|
+
this._contextNode = prevContext;
|
|
588
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} else if (groupEndingWith) {
|
|
592
|
+
// End group when expression matches
|
|
593
|
+
/** @type {Node[]} */
|
|
594
|
+
let currentGroup = [];
|
|
595
|
+
|
|
596
|
+
for (const node of nodes) {
|
|
597
|
+
currentGroup.push(node);
|
|
598
|
+
const endMatch = evalInContext(groupEndingWith, node);
|
|
599
|
+
|
|
600
|
+
if (endMatch) {
|
|
601
|
+
const prevParams = this._params;
|
|
602
|
+
const prevContext = this._contextNode;
|
|
603
|
+
try {
|
|
604
|
+
this._contextNode = currentGroup[0];
|
|
605
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
606
|
+
cb.call(this, null, currentGroup, this);
|
|
607
|
+
} finally {
|
|
608
|
+
this._params = prevParams;
|
|
609
|
+
this._contextNode = prevContext;
|
|
610
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
611
|
+
}
|
|
612
|
+
currentGroup = [];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Process last group if not ended
|
|
617
|
+
if (currentGroup.length > 0) {
|
|
618
|
+
const prevParams = this._params;
|
|
619
|
+
const prevContext = this._contextNode;
|
|
620
|
+
try {
|
|
621
|
+
this._contextNode = currentGroup[0];
|
|
622
|
+
/** @type {any} */ (this)._currentGroup = currentGroup;
|
|
623
|
+
cb.call(this, null, currentGroup, this);
|
|
624
|
+
} finally {
|
|
625
|
+
this._params = prevParams;
|
|
626
|
+
this._contextNode = prevContext;
|
|
627
|
+
delete /** @type {any} */ (this)._currentGroup;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return this;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Returns the current group (for use within forEachGroup callback).
|
|
637
|
+
* @returns {Node[]|undefined}
|
|
638
|
+
*/
|
|
639
|
+
currentGroup () {
|
|
640
|
+
return /** @type {any} */ (this)._currentGroup;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Returns the current grouping key (for use within forEachGroup callback).
|
|
645
|
+
* @returns {any}
|
|
646
|
+
*/
|
|
647
|
+
currentGroupingKey () {
|
|
648
|
+
return /** @type {any} */ (this)._currentGroupingKey;
|
|
649
|
+
}
|
|
650
|
+
|
|
379
651
|
/**
|
|
380
652
|
* Append the value from an XPath expression or the context node text.
|
|
381
653
|
* @param {string|object} [select]
|
|
@@ -389,11 +661,66 @@ class XPathTransformerContext {
|
|
|
389
661
|
? /** @type {{select?: string}} */ (select).select
|
|
390
662
|
: select;
|
|
391
663
|
|
|
664
|
+
// Check for format-number() function call
|
|
665
|
+
if (selectStr && selectStr.includes('format-number(')) {
|
|
666
|
+
const match = (/format-number\((?<value>[^,\)]+)(?:,\s*["'](?<format>[^"']+)["'])?(?:,\s*["'](?<decimalFormat>[^"']*)["'])?\)/v).exec(selectStr);
|
|
667
|
+
if (match && match.groups) {
|
|
668
|
+
const {
|
|
669
|
+
value: valueExpr,
|
|
670
|
+
format: formatStr,
|
|
671
|
+
decimalFormat: decimalFormatName
|
|
672
|
+
} = match.groups;
|
|
673
|
+
// Evaluate the value expression
|
|
674
|
+
let numValue;
|
|
675
|
+
if (valueExpr.trim().startsWith('$')) {
|
|
676
|
+
// Parameter reference
|
|
677
|
+
const paramName = valueExpr.trim().slice(1);
|
|
678
|
+
numValue = this._params && paramName in this._params
|
|
679
|
+
? this._params[paramName]
|
|
680
|
+
: 0;
|
|
681
|
+
} else {
|
|
682
|
+
// Try to parse as number or evaluate as XPath
|
|
683
|
+
const trimmed = valueExpr.trim();
|
|
684
|
+
numValue = Number.isNaN(Number(trimmed))
|
|
685
|
+
? this._evalXPath(trimmed, false)
|
|
686
|
+
: Number(trimmed);
|
|
687
|
+
}
|
|
688
|
+
const num = typeof numValue === 'string' ? Number(numValue) : numValue;
|
|
689
|
+
const format = formatStr || '1';
|
|
690
|
+
const formatted = this._formatNumber(
|
|
691
|
+
num,
|
|
692
|
+
format,
|
|
693
|
+
undefined,
|
|
694
|
+
undefined,
|
|
695
|
+
decimalFormatName || '',
|
|
696
|
+
'en'
|
|
697
|
+
);
|
|
698
|
+
jt.append(formatted);
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
392
703
|
// Check if this is a parameter reference (starts with $)
|
|
393
704
|
if (selectStr && selectStr.startsWith('$')) {
|
|
394
705
|
const paramName = selectStr.slice(1);
|
|
395
706
|
if (this._params && paramName in this._params) {
|
|
396
707
|
val = this._params[paramName];
|
|
708
|
+
// If val is a Node, extract its text content
|
|
709
|
+
if (val && typeof val === 'object' && 'nodeType' in val) {
|
|
710
|
+
if (val.nodeType === 3) {
|
|
711
|
+
// Text node: use nodeValue
|
|
712
|
+
val = val.nodeValue;
|
|
713
|
+
} else if (val.nodeType === 9) {
|
|
714
|
+
// Document node: use documentElement.textContent
|
|
715
|
+
const doc = /** @type {Document} */ (val);
|
|
716
|
+
val = doc.documentElement
|
|
717
|
+
? doc.documentElement.textContent
|
|
718
|
+
: null;
|
|
719
|
+
} else {
|
|
720
|
+
// Other nodes: use textContent
|
|
721
|
+
val = val.textContent;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
397
724
|
} else {
|
|
398
725
|
// Fall back to normal XPath evaluation
|
|
399
726
|
const resResult = this._evalXPath(selectStr, true);
|
|
@@ -406,9 +733,19 @@ class XPathTransformerContext {
|
|
|
406
733
|
/* c8 ignore stop */
|
|
407
734
|
}
|
|
408
735
|
} else if (!selectStr || selectStr === '.') {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
736
|
+
if (this._contextNode.nodeType === 3) {
|
|
737
|
+
// Text node: use nodeValue
|
|
738
|
+
val = this._contextNode.nodeValue;
|
|
739
|
+
} else if (this._contextNode.nodeType === 9) {
|
|
740
|
+
// Document node: use documentElement.textContent
|
|
741
|
+
const doc = /** @type {Document} */ (this._contextNode);
|
|
742
|
+
val = doc.documentElement
|
|
743
|
+
? doc.documentElement.textContent
|
|
744
|
+
: null;
|
|
745
|
+
} else {
|
|
746
|
+
// Other nodes: use textContent
|
|
747
|
+
val = this._contextNode.textContent;
|
|
748
|
+
}
|
|
412
749
|
} else {
|
|
413
750
|
const resResult = this._evalXPath(selectStr, true);
|
|
414
751
|
const res = /** @type {Node[]} */ (resResult);
|
|
@@ -549,6 +886,7 @@ class XPathTransformerContext {
|
|
|
549
886
|
* level?: 'single'|'multiple'|'any',
|
|
550
887
|
* from?: string,
|
|
551
888
|
* format?: string,
|
|
889
|
+
* decimalFormat?: string,
|
|
552
890
|
* groupingSeparator?: string,
|
|
553
891
|
* groupingSize?: number
|
|
554
892
|
* }} num - Number value, "position()" string, or options object
|
|
@@ -733,15 +1071,27 @@ class XPathTransformerContext {
|
|
|
733
1071
|
* @param {string} format - Format string (1, a, A, i, I, 01, etc.)
|
|
734
1072
|
* @param {string} [groupingSeparator] - Separator for grouping (e.g., ',')
|
|
735
1073
|
* @param {number} [groupingSize] - Size of groups (e.g., 3 for 1,000)
|
|
1074
|
+
* @param {string} [decimalFormatName] - Named decimal format to use
|
|
736
1075
|
* @param {string} [locale]
|
|
737
1076
|
* @returns {string}
|
|
738
1077
|
* @private
|
|
739
1078
|
*/
|
|
740
|
-
_formatNumber (
|
|
1079
|
+
_formatNumber (
|
|
1080
|
+
num, format, groupingSeparator, groupingSize, decimalFormatName, locale
|
|
1081
|
+
) {
|
|
741
1082
|
if (Number.isNaN(num)) {
|
|
742
1083
|
return String(num);
|
|
743
1084
|
}
|
|
744
1085
|
|
|
1086
|
+
// Get decimal format symbols if specified
|
|
1087
|
+
const symbols = decimalFormatName !== undefined &&
|
|
1088
|
+
decimalFormatName in this.decimalFormats
|
|
1089
|
+
? this.decimalFormats[decimalFormatName]
|
|
1090
|
+
: undefined;
|
|
1091
|
+
|
|
1092
|
+
// Default locale
|
|
1093
|
+
const loc = locale || 'en';
|
|
1094
|
+
|
|
745
1095
|
let result;
|
|
746
1096
|
const formatChar = format.charAt(0);
|
|
747
1097
|
|
|
@@ -768,7 +1118,8 @@ class XPathTransformerContext {
|
|
|
768
1118
|
}
|
|
769
1119
|
case '0': {
|
|
770
1120
|
const width = format.length;
|
|
771
|
-
|
|
1121
|
+
const zeroDigit = symbols?.zeroDigit || '0';
|
|
1122
|
+
result = String(num).padStart(width, zeroDigit);
|
|
772
1123
|
|
|
773
1124
|
break;
|
|
774
1125
|
}
|
|
@@ -782,8 +1133,26 @@ class XPathTransformerContext {
|
|
|
782
1133
|
};
|
|
783
1134
|
}
|
|
784
1135
|
try {
|
|
785
|
-
result = new Intl.NumberFormat(
|
|
786
|
-
|
|
1136
|
+
result = new Intl.NumberFormat(loc, options).format(num);
|
|
1137
|
+
|
|
1138
|
+
// Apply decimal format symbols if specified
|
|
1139
|
+
if (symbols) {
|
|
1140
|
+
// Use placeholders to avoid conflicts during replacement
|
|
1141
|
+
const TEMP_GROUP = '\u0000GROUPSEP\u0000';
|
|
1142
|
+
const TEMP_DECIMAL = '\u0000DECIMALSEP\u0000';
|
|
1143
|
+
|
|
1144
|
+
// Replace with temporary placeholders first
|
|
1145
|
+
result = result.replaceAll(',', TEMP_GROUP);
|
|
1146
|
+
result = result.replaceAll('.', TEMP_DECIMAL);
|
|
1147
|
+
|
|
1148
|
+
// Now replace with actual symbols
|
|
1149
|
+
const effectiveGroupingSep = groupingSeparator ||
|
|
1150
|
+
symbols.groupingSeparator || ',';
|
|
1151
|
+
const effectiveDecimalSep = symbols.decimalSeparator || '.';
|
|
1152
|
+
|
|
1153
|
+
result = result.replaceAll(TEMP_GROUP, effectiveGroupingSep);
|
|
1154
|
+
result = result.replaceAll(TEMP_DECIMAL, effectiveDecimalSep);
|
|
1155
|
+
} else if (groupingSeparator) {
|
|
787
1156
|
result = result.replaceAll(',', groupingSeparator);
|
|
788
1157
|
}
|
|
789
1158
|
} catch (e) {
|
|
@@ -910,6 +1279,17 @@ class XPathTransformerContext {
|
|
|
910
1279
|
return this;
|
|
911
1280
|
}
|
|
912
1281
|
|
|
1282
|
+
/**
|
|
1283
|
+
* @param {string} name
|
|
1284
|
+
* @param {import('./AbstractJoiningTransformer.js').
|
|
1285
|
+
* OutputCharacters} outputCharacters
|
|
1286
|
+
* @returns {this}
|
|
1287
|
+
*/
|
|
1288
|
+
characterMap (name, outputCharacters) {
|
|
1289
|
+
this._getJoiningTransformer().characterMap(name, outputCharacters);
|
|
1290
|
+
return this;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
913
1293
|
/**
|
|
914
1294
|
* Append element.
|
|
915
1295
|
* @param {string} name Tag name
|
|
@@ -925,6 +1305,44 @@ class XPathTransformerContext {
|
|
|
925
1305
|
this._getJoiningTransformer().element(name, atts, children, cb);
|
|
926
1306
|
return this;
|
|
927
1307
|
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Adds a prefixed namespace declaration to the most recently opened
|
|
1311
|
+
* element. Mirrors the joining
|
|
1312
|
+
* transformer API so templates can call `this.attribute()`.
|
|
1313
|
+
* @param {string} prefix - Prefix
|
|
1314
|
+
* @param {string} namespaceURI - Namespace
|
|
1315
|
+
* @returns {this}
|
|
1316
|
+
*/
|
|
1317
|
+
namespace (prefix, namespaceURI) {
|
|
1318
|
+
/** @type {any} */ (this._getJoiningTransformer()).namespace(
|
|
1319
|
+
prefix, namespaceURI
|
|
1320
|
+
);
|
|
1321
|
+
return this;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Define a decimal format with custom symbols for number formatting.
|
|
1326
|
+
* Equivalent to xsl:decimal-format. If no name is provided, defines
|
|
1327
|
+
* the default format.
|
|
1328
|
+
* @param {string|import('./JSONPathTransformerContext.js').
|
|
1329
|
+
* DecimalFormatSymbols} nameOrSymbols - Format name or symbols object
|
|
1330
|
+
* if defining default
|
|
1331
|
+
* @param {import('./JSONPathTransformerContext.js').
|
|
1332
|
+
* DecimalFormatSymbols} [symbols] - Format symbols
|
|
1333
|
+
* @returns {this}
|
|
1334
|
+
*/
|
|
1335
|
+
decimalFormat (nameOrSymbols, symbols) {
|
|
1336
|
+
if (typeof nameOrSymbols === 'string') {
|
|
1337
|
+
// Named format
|
|
1338
|
+
this.decimalFormats[nameOrSymbols] = symbols || {};
|
|
1339
|
+
} else {
|
|
1340
|
+
// Default format (unnamed)
|
|
1341
|
+
this.decimalFormats[''] = nameOrSymbols;
|
|
1342
|
+
}
|
|
1343
|
+
return this;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
928
1346
|
/**
|
|
929
1347
|
* Append attribute.
|
|
930
1348
|
* @param {string} name Attribute name
|
package/src/index.js
CHANGED
|
@@ -380,7 +380,13 @@ class JTLT {
|
|
|
380
380
|
);
|
|
381
381
|
// eslint-disable-next-line @stylistic/max-len -- Long
|
|
382
382
|
// eslint-disable-next-line unicorn/no-array-method-this-argument -- Not array
|
|
383
|
-
this.forEach(path,
|
|
383
|
+
this.forEach(path, (arg) => {
|
|
384
|
+
const ret = fn.call(this, arg);
|
|
385
|
+
if (typeof ret !== 'undefined') {
|
|
386
|
+
// @ts-expect-error Ok?
|
|
387
|
+
this._getJoiningTransformer().append(ret);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
384
390
|
}
|
|
385
391
|
: cfg.query || (
|
|
386
392
|
typeof cfg.templates === 'function'
|